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