]> git.lizzy.rs Git - dragonfireclient.git/blob - src/clientmedia.cpp
Remove blank default values for emergequeue_limit_* settings
[dragonfireclient.git] / src / clientmedia.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "clientmedia.h"
21 #include "httpfetch.h"
22 #include "client.h"
23 #include "clientserver.h"
24 #include "filecache.h"
25 #include "hex.h"
26 #include "sha1.h"
27 #include "debug.h"
28 #include "log.h"
29 #include "porting.h"
30 #include "settings.h"
31 #include "main.h"
32 #include "util/serialize.h"
33 #include "util/string.h"
34
35 static std::string getMediaCacheDir()
36 {
37         return porting::path_user + DIR_DELIM + "cache" + DIR_DELIM + "media";
38 }
39
40 /*
41         ClientMediaDownloader
42 */
43
44 ClientMediaDownloader::ClientMediaDownloader():
45         m_media_cache(getMediaCacheDir())
46 {
47         m_initial_step_done = false;
48         m_name_bound = "";  // works because "" is an invalid file name
49         m_uncached_count = 0;
50         m_uncached_received_count = 0;
51         m_httpfetch_caller = HTTPFETCH_DISCARD;
52         m_httpfetch_active = 0;
53         m_httpfetch_active_limit = 0;
54         m_httpfetch_next_id = 0;
55         m_httpfetch_timeout = 0;
56         m_outstanding_hash_sets = 0;
57 }
58
59 ClientMediaDownloader::~ClientMediaDownloader()
60 {
61         if (m_httpfetch_caller != HTTPFETCH_DISCARD)
62                 httpfetch_caller_free(m_httpfetch_caller);
63
64         for (std::map<std::string, FileStatus*>::iterator it = m_files.begin();
65                         it != m_files.end(); ++it)
66                 delete it->second;
67
68         for (u32 i = 0; i < m_remotes.size(); ++i)
69                 delete m_remotes[i];
70 }
71
72 void ClientMediaDownloader::addFile(std::string name, std::string sha1)
73 {
74         assert(!m_initial_step_done);
75
76         // if name was already announced, ignore the new announcement
77         if (m_files.count(name) != 0) {
78                 errorstream << "Client: ignoring duplicate media announcement "
79                                 << "sent by server: \"" << name << "\""
80                                 << std::endl;
81                 return;
82         }
83
84         // if name is empty or contains illegal characters, ignore the file
85         if (name.empty() || !string_allowed(name, TEXTURENAME_ALLOWED_CHARS)) {
86                 errorstream << "Client: ignoring illegal file name "
87                                 << "sent by server: \"" << name << "\""
88                                 << std::endl;
89                 return;
90         }
91
92         // length of sha1 must be exactly 20 (160 bits), else ignore the file
93         if (sha1.size() != 20) {
94                 errorstream << "Client: ignoring illegal SHA1 sent by server: "
95                                 << hex_encode(sha1) << " \"" << name << "\""
96                                 << std::endl;
97                 return;
98         }
99
100         FileStatus *filestatus = new FileStatus;
101         filestatus->received = false;
102         filestatus->sha1 = sha1;
103         filestatus->current_remote = -1;
104         m_files.insert(std::make_pair(name, filestatus));
105 }
106
107 void ClientMediaDownloader::addRemoteServer(std::string baseurl)
108 {
109         assert(!m_initial_step_done);
110
111         #ifdef USE_CURL
112
113         infostream << "Client: Adding remote server \""
114                 << baseurl << "\" for media download" << std::endl;
115
116         RemoteServerStatus *remote = new RemoteServerStatus;
117         remote->baseurl = baseurl;
118         remote->active_count = 0;
119         remote->request_by_filename = false;
120         m_remotes.push_back(remote);
121
122         #else
123
124         infostream << "Client: Ignoring remote server \""
125                 << baseurl << "\" because cURL support is not compiled in"
126                 << std::endl;
127
128         #endif
129 }
130
131 void ClientMediaDownloader::step(Client *client)
132 {
133         if (!m_initial_step_done) {
134                 initialStep(client);
135                 m_initial_step_done = true;
136         }
137
138         // Remote media: check for completion of fetches
139         if (m_httpfetch_active) {
140                 bool fetched_something = false;
141                 HTTPFetchResult fetchresult;
142
143                 while (httpfetch_async_get(m_httpfetch_caller, fetchresult)) {
144                         m_httpfetch_active--;
145                         fetched_something = true;
146
147                         // Is this a hashset (index.mth) or a media file?
148                         if (fetchresult.request_id < m_remotes.size())
149                                 remoteHashSetReceived(fetchresult);
150                         else
151                                 remoteMediaReceived(fetchresult, client);
152                 }
153
154                 if (fetched_something)
155                         startRemoteMediaTransfers();
156
157                 // Did all remote transfers end and no new ones can be started?
158                 // If so, request still missing files from the minetest server
159                 // (Or report that we have all files.)
160                 if (m_httpfetch_active == 0) {
161                         if (m_uncached_received_count < m_uncached_count) {
162                                 infostream << "Client: Failed to remote-fetch "
163                                         << (m_uncached_count-m_uncached_received_count)
164                                         << " files. Requesting them"
165                                         << " the usual way." << std::endl;
166                         }
167                         startConventionalTransfers(client);
168                 }
169         }
170 }
171
172 void ClientMediaDownloader::initialStep(Client *client)
173 {
174         // Check media cache
175         m_uncached_count = m_files.size();
176         for (std::map<std::string, FileStatus*>::iterator
177                         it = m_files.begin();
178                         it != m_files.end(); ++it) {
179                 std::string name = it->first;
180                 FileStatus *filestatus = it->second;
181                 const std::string &sha1 = filestatus->sha1;
182
183                 std::ostringstream tmp_os(std::ios_base::binary);
184                 bool found_in_cache = m_media_cache.load(hex_encode(sha1), tmp_os);
185
186                 // If found in cache, try to load it from there
187                 if (found_in_cache) {
188                         bool success = checkAndLoad(name, sha1,
189                                         tmp_os.str(), true, client);
190                         if (success) {
191                                 filestatus->received = true;
192                                 m_uncached_count--;
193                         }
194                 }
195         }
196
197         assert(m_uncached_received_count == 0);
198
199         // Create the media cache dir if we are likely to write to it
200         if (m_uncached_count != 0) {
201                 bool did = fs::CreateAllDirs(getMediaCacheDir());
202                 if (!did) {
203                         errorstream << "Client: "
204                                 << "Could not create media cache directory: "
205                                 << getMediaCacheDir()
206                                 << std::endl;
207                 }
208         }
209
210         // If we found all files in the cache, report this fact to the server.
211         // If the server reported no remote servers, immediately start
212         // conventional transfers. Note: if cURL support is not compiled in,
213         // m_remotes is always empty, so "!USE_CURL" is redundant but may
214         // reduce the size of the compiled code
215         if (!USE_CURL || m_uncached_count == 0 || m_remotes.empty()) {
216                 startConventionalTransfers(client);
217         }
218         else {
219                 // Otherwise start off by requesting each server's sha1 set
220
221                 // This is the first time we use httpfetch, so alloc a caller ID
222                 m_httpfetch_caller = httpfetch_caller_alloc();
223                 m_httpfetch_timeout = g_settings->getS32("curl_timeout");
224
225                 // Set the active fetch limit to curl_parallel_limit or 84,
226                 // whichever is greater. This gives us some leeway so that
227                 // inefficiencies in communicating with the httpfetch thread
228                 // don't slow down fetches too much. (We still want some limit
229                 // so that when the first remote server returns its hash set,
230                 // not all files are requested from that server immediately.)
231                 // One such inefficiency is that ClientMediaDownloader::step()
232                 // is only called a couple times per second, while httpfetch
233                 // might return responses much faster than that.
234                 // Note that httpfetch strictly enforces curl_parallel_limit
235                 // but at no inter-thread communication cost. This however
236                 // doesn't help with the aforementioned inefficiencies.
237                 // The signifance of 84 is that it is 2*6*9 in base 13.
238                 m_httpfetch_active_limit = g_settings->getS32("curl_parallel_limit");
239                 m_httpfetch_active_limit = MYMAX(m_httpfetch_active_limit, 84);
240
241                 // Write a list of hashes that we need. This will be POSTed
242                 // to the server using Content-Type: application/octet-stream
243                 std::string required_hash_set = serializeRequiredHashSet();
244
245                 // minor fixme: this loop ignores m_httpfetch_active_limit
246
247                 // another minor fixme, unlikely to matter in normal usage:
248                 // these index.mth fetches do (however) count against
249                 // m_httpfetch_active_limit when starting actual media file
250                 // requests, so if there are lots of remote servers that are
251                 // not responding, those will stall new media file transfers.
252
253                 for (u32 i = 0; i < m_remotes.size(); ++i) {
254                         assert(m_httpfetch_next_id == i);
255
256                         RemoteServerStatus *remote = m_remotes[i];
257                         actionstream << "Client: Contacting remote server \""
258                                 << remote->baseurl << "\"" << std::endl;
259
260                         HTTPFetchRequest fetchrequest;
261                         fetchrequest.url =
262                                 remote->baseurl + MTHASHSET_FILE_NAME;
263                         fetchrequest.caller = m_httpfetch_caller;
264                         fetchrequest.request_id = m_httpfetch_next_id; // == i
265                         fetchrequest.timeout = m_httpfetch_timeout;
266                         fetchrequest.connect_timeout = m_httpfetch_timeout;
267                         fetchrequest.post_fields = required_hash_set;
268                         fetchrequest.extra_headers.push_back(
269                                 "Content-Type: application/octet-stream");
270                         httpfetch_async(fetchrequest);
271
272                         m_httpfetch_active++;
273                         m_httpfetch_next_id++;
274                         m_outstanding_hash_sets++;
275                 }
276         }
277 }
278
279 void ClientMediaDownloader::remoteHashSetReceived(
280                 const HTTPFetchResult &fetchresult)
281 {
282         u32 remote_id = fetchresult.request_id;
283         assert(remote_id < m_remotes.size());
284         RemoteServerStatus *remote = m_remotes[remote_id];
285
286         m_outstanding_hash_sets--;
287
288         if (fetchresult.succeeded) {
289                 try {
290                         // Server sent a list of file hashes that are
291                         // available on it, try to parse the list
292
293                         std::set<std::string> sha1_set;
294                         deSerializeHashSet(fetchresult.data, sha1_set);
295
296                         // Parsing succeeded: For every file that is
297                         // available on this server, add this server
298                         // to the available_remotes array
299
300                         for(std::map<std::string, FileStatus*>::iterator
301                                         it = m_files.upper_bound(m_name_bound);
302                                         it != m_files.end(); ++it) {
303                                 FileStatus *f = it->second;
304                                 if (!f->received && sha1_set.count(f->sha1))
305                                         f->available_remotes.push_back(remote_id);
306                         }
307                 }
308                 catch (SerializationError &e) {
309                         infostream << "Client: Remote server \""
310                                 << remote->baseurl << "\" sent invalid hash set: "
311                                 << e.what() << std::endl;
312                 }
313         }
314
315         // For compatibility: If index.mth is not found, assume that the
316         // server contains files named like the original files (not their sha1)
317
318         if (!fetchresult.succeeded && !fetchresult.timeout &&
319                         fetchresult.response_code == 404) {
320                 infostream << "Client: Enabling compatibility mode for remote "
321                         << "server \"" << remote->baseurl << "\"" << std::endl;
322                 remote->request_by_filename = true;
323
324                 // Assume every file is available on this server
325
326                 for(std::map<std::string, FileStatus*>::iterator
327                                 it = m_files.upper_bound(m_name_bound);
328                                 it != m_files.end(); ++it) {
329                         FileStatus *f = it->second;
330                         if (!f->received)
331                                 f->available_remotes.push_back(remote_id);
332                 }
333         }
334 }
335
336 void ClientMediaDownloader::remoteMediaReceived(
337                 const HTTPFetchResult &fetchresult,
338                 Client *client)
339 {
340         // Some remote server sent us a file.
341         // -> decrement number of active fetches
342         // -> mark file as received if fetch succeeded
343         // -> try to load media
344
345         std::string name;
346         {
347                 std::map<unsigned long, std::string>::iterator it =
348                         m_remote_file_transfers.find(fetchresult.request_id);
349                 assert(it != m_remote_file_transfers.end());
350                 name = it->second;
351                 m_remote_file_transfers.erase(it);
352         }
353
354         assert(m_files.count(name) != 0);
355
356         FileStatus *filestatus = m_files[name];
357         assert(!filestatus->received);
358         assert(filestatus->current_remote >= 0);
359
360         RemoteServerStatus *remote = m_remotes[filestatus->current_remote];
361
362         filestatus->current_remote = -1;
363         remote->active_count--;
364
365         // If fetch succeeded, try to load media file
366
367         if (fetchresult.succeeded) {
368                 bool success = checkAndLoad(name, filestatus->sha1,
369                                 fetchresult.data, false, client);
370                 if (success) {
371                         filestatus->received = true;
372                         assert(m_uncached_received_count < m_uncached_count);
373                         m_uncached_received_count++;
374                 }
375         }
376 }
377
378 s32 ClientMediaDownloader::selectRemoteServer(FileStatus *filestatus)
379 {
380         assert(filestatus != NULL);
381         assert(!filestatus->received);
382         assert(filestatus->current_remote < 0);
383
384         if (filestatus->available_remotes.empty())
385                 return -1;
386         else {
387                 // Of all servers that claim to provide the file (and haven't
388                 // been unsuccessfully tried before), find the one with the
389                 // smallest number of currently active transfers
390
391                 s32 best = 0;
392                 s32 best_remote_id = filestatus->available_remotes[best];
393                 s32 best_active_count = m_remotes[best_remote_id]->active_count;
394
395                 for (u32 i = 1; i < filestatus->available_remotes.size(); ++i) {
396                         s32 remote_id = filestatus->available_remotes[i];
397                         s32 active_count = m_remotes[remote_id]->active_count;
398                         if (active_count < best_active_count) {
399                                 best = i;
400                                 best_remote_id = remote_id;
401                                 best_active_count = active_count;
402                         }
403                 }
404
405                 filestatus->available_remotes.erase(
406                                 filestatus->available_remotes.begin() + best);
407
408                 return best_remote_id;
409         }
410 }
411
412 void ClientMediaDownloader::startRemoteMediaTransfers()
413 {
414         bool changing_name_bound = true;
415
416         for (std::map<std::string, FileStatus*>::iterator
417                         files_iter = m_files.upper_bound(m_name_bound);
418                         files_iter != m_files.end(); ++files_iter) {
419
420                 // Abort if active fetch limit is exceeded
421                 if (m_httpfetch_active >= m_httpfetch_active_limit)
422                         break;
423
424                 const std::string &name = files_iter->first;
425                 FileStatus *filestatus = files_iter->second;
426
427                 if (!filestatus->received && filestatus->current_remote < 0) {
428                         // File has not been received yet and is not currently
429                         // being transferred. Choose a server for it.
430                         s32 remote_id = selectRemoteServer(filestatus);
431                         if (remote_id >= 0) {
432                                 // Found a server, so start fetching
433                                 RemoteServerStatus *remote =
434                                         m_remotes[remote_id];
435
436                                 std::string url = remote->baseurl +
437                                         (remote->request_by_filename ? name :
438                                         hex_encode(filestatus->sha1));
439                                 verbosestream << "Client: "
440                                         << "Requesting remote media file "
441                                         << "\"" << name << "\" "
442                                         << "\"" << url << "\"" << std::endl;
443
444                                 HTTPFetchRequest fetchrequest;
445                                 fetchrequest.url = url;
446                                 fetchrequest.caller = m_httpfetch_caller;
447                                 fetchrequest.request_id = m_httpfetch_next_id;
448                                 fetchrequest.timeout = 0; // no data timeout!
449                                 fetchrequest.connect_timeout =
450                                         m_httpfetch_timeout;
451                                 httpfetch_async(fetchrequest);
452
453                                 m_remote_file_transfers.insert(std::make_pair(
454                                                         m_httpfetch_next_id,
455                                                         name));
456
457                                 filestatus->current_remote = remote_id;
458                                 remote->active_count++;
459                                 m_httpfetch_active++;
460                                 m_httpfetch_next_id++;
461                         }
462                 }
463
464                 if (filestatus->received ||
465                                 (filestatus->current_remote < 0 &&
466                                  !m_outstanding_hash_sets)) {
467                         // If we arrive here, we conclusively know that we
468                         // won't fetch this file from a remote server in the
469                         // future. So update the name bound if possible.
470                         if (changing_name_bound)
471                                 m_name_bound = name;
472                 }
473                 else
474                         changing_name_bound = false;
475         }
476
477 }
478
479 void ClientMediaDownloader::startConventionalTransfers(Client *client)
480 {
481         assert(m_httpfetch_active == 0);
482
483         if (m_uncached_received_count != m_uncached_count) {
484                 // Some media files have not been received yet, use the
485                 // conventional slow method (minetest protocol) to get them
486                 std::list<std::string> file_requests;
487                 for (std::map<std::string, FileStatus*>::iterator
488                                 it = m_files.begin();
489                                 it != m_files.end(); ++it) {
490                         if (!it->second->received)
491                                 file_requests.push_back(it->first);
492                 }
493                 assert((s32) file_requests.size() ==
494                                 m_uncached_count - m_uncached_received_count);
495                 client->request_media(file_requests);
496         }
497 }
498
499 void ClientMediaDownloader::conventionalTransferDone(
500                 const std::string &name,
501                 const std::string &data,
502                 Client *client)
503 {
504         // Check that file was announced
505         std::map<std::string, FileStatus*>::iterator
506                 file_iter = m_files.find(name);
507         if (file_iter == m_files.end()) {
508                 errorstream << "Client: server sent media file that was"
509                         << "not announced, ignoring it: \"" << name << "\""
510                         << std::endl;
511                 return;
512         }
513         FileStatus *filestatus = file_iter->second;
514         assert(filestatus != NULL);
515
516         // Check that file hasn't already been received
517         if (filestatus->received) {
518                 errorstream << "Client: server sent media file that we already"
519                         << "received, ignoring it: \"" << name << "\""
520                         << std::endl;
521                 return;
522         }
523
524         // Mark file as received, regardless of whether loading it works and
525         // whether the checksum matches (because at this point there is no
526         // other server that could send a replacement)
527         filestatus->received = true;
528         assert(m_uncached_received_count < m_uncached_count);
529         m_uncached_received_count++;
530
531         // Check that received file matches announced checksum
532         // If so, load it
533         checkAndLoad(name, filestatus->sha1, data, false, client);
534 }
535
536 bool ClientMediaDownloader::checkAndLoad(
537                 const std::string &name, const std::string &sha1,
538                 const std::string &data, bool is_from_cache, Client *client)
539 {
540         const char *cached_or_received = is_from_cache ? "cached" : "received";
541         const char *cached_or_received_uc = is_from_cache ? "Cached" : "Received";
542         std::string sha1_hex = hex_encode(sha1);
543
544         // Compute actual checksum of data
545         std::string data_sha1;
546         {
547                 SHA1 data_sha1_calculator;
548                 data_sha1_calculator.addBytes(data.c_str(), data.size());
549                 unsigned char *data_tmpdigest = data_sha1_calculator.getDigest();
550                 data_sha1.assign((char*) data_tmpdigest, 20);
551                 free(data_tmpdigest);
552         }
553
554         // Check that received file matches announced checksum
555         if (data_sha1 != sha1) {
556                 std::string data_sha1_hex = hex_encode(data_sha1);
557                 infostream << "Client: "
558                         << cached_or_received_uc << " media file "
559                         << sha1_hex << " \"" << name << "\" "
560                         << "mismatches actual checksum " << data_sha1_hex
561                         << std::endl;
562                 return false;
563         }
564
565         // Checksum is ok, try loading the file
566         bool success = client->loadMedia(data, name);
567         if (!success) {
568                 infostream << "Client: "
569                         << "Failed to load " << cached_or_received << " media: "
570                         << sha1_hex << " \"" << name << "\""
571                         << std::endl;
572                 return false;
573         }
574
575         verbosestream << "Client: "
576                 << "Loaded " << cached_or_received << " media: "
577                 << sha1_hex << " \"" << name << "\""
578                 << std::endl;
579
580         // Update cache (unless we just loaded the file from the cache)
581         if (!is_from_cache)
582                 m_media_cache.update(sha1_hex, data);
583
584         return true;
585 }
586
587
588 /*
589         Minetest Hashset File Format
590
591         All values are stored in big-endian byte order.
592         [u32] signature: 'MTHS'
593         [u16] version: 1
594         For each hash in set:
595                 [u8*20] SHA1 hash
596
597         Version changes:
598         1 - Initial version
599 */
600
601 std::string ClientMediaDownloader::serializeRequiredHashSet()
602 {
603         std::ostringstream os(std::ios::binary);
604
605         writeU32(os, MTHASHSET_FILE_SIGNATURE); // signature
606         writeU16(os, 1);                        // version
607
608         // Write list of hashes of files that have not been
609         // received (found in cache) yet
610         for (std::map<std::string, FileStatus*>::iterator
611                         it = m_files.begin();
612                         it != m_files.end(); ++it) {
613                 if (!it->second->received) {
614                         assert(it->second->sha1.size() == 20);
615                         os << it->second->sha1;
616                 }
617         }
618
619         return os.str();
620 }
621
622 void ClientMediaDownloader::deSerializeHashSet(const std::string &data,
623                 std::set<std::string> &result)
624 {
625         if (data.size() < 6 || data.size() % 20 != 6) {
626                 throw SerializationError(
627                                 "ClientMediaDownloader::deSerializeHashSet: "
628                                 "invalid hash set file size");
629         }
630
631         const u8 *data_cstr = (const u8*) data.c_str();
632
633         u32 signature = readU32(&data_cstr[0]);
634         if (signature != MTHASHSET_FILE_SIGNATURE) {
635                 throw SerializationError(
636                                 "ClientMediaDownloader::deSerializeHashSet: "
637                                 "invalid hash set file signature");
638         }
639
640         u16 version = readU16(&data_cstr[4]);
641         if (version != 1) {
642                 throw SerializationError(
643                                 "ClientMediaDownloader::deSerializeHashSet: "
644                                 "unsupported hash set file version");
645         }
646
647         for (u32 pos = 6; pos < data.size(); pos += 20) {
648                 result.insert(data.substr(pos, 20));
649         }
650 }