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