1 //////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (c) 2004-2021 musikcube team
4 //
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 //    * Redistributions of source code must retain the above copyright notice,
11 //      this list of conditions and the following disclaimer.
12 //
13 //    * Redistributions in binary form must reproduce the above copyright
14 //      notice, this list of conditions and the following disclaimer in the
15 //      documentation and/or other materials provided with the distribution.
16 //
17 //    * Neither the name of the author nor the names of other contributors may
18 //      be used to endorse or promote products derived from this software
19 //      without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 //
33 //////////////////////////////////////////////////////////////////////////////
34 
35 #include "HttpServer.h"
36 #include "Constants.h"
37 #include "Util.h"
38 #include "Transcoder.h"
39 #include "TranscodingAudioDataStream.h"
40 
41 #include <musikcore/sdk/ITrack.h>
42 
43 #pragma warning(push, 0)
44 #include <boost/filesystem.hpp>
45 #include <boost/algorithm/string.hpp>
46 #include <websocketpp/base64/base64.hpp>
47 #pragma warning(pop, 0)
48 
49 #include <iostream>
50 #include <unordered_map>
51 #include <string>
52 #include <cstdlib>
53 
54 #include <fcntl.h>
55 #include <stdio.h>
56 
57 #ifdef WIN32
58 #include <io.h>
59 #endif
60 
61 #include <vector>
62 
63 #define HTTP_416_DISABLED true
64 
65 static const char* ENVIRONMENT_DISABLE_HTTP_SERVER_AUTH = "MUSIKCUBE_DISABLE_HTTP_SERVER_AUTH";
66 
67 using namespace musik::core::sdk;
68 
69 std::unordered_map<std::string, std::string> CONTENT_TYPE_MAP = {
70     { ".mp3", "audio/mpeg" },
71     { ".ogg", "audio/ogg" },
72     { ".opus", "audio/ogg" },
73     { ".oga", "audio/ogg" },
74     { ".spx", "audio/ogg" },
75     { ".flac", "audio/flac" },
76     { ".aac", "audio/aac" },
77     { ".mp4", "audio/mp4" },
78     { ".m4a", "audio/mp4" },
79     { ".wav", "audio/wav" },
80     { ".mpc", "audio/x-musepack" },
81     { ".mp+", "audio/x-musepack" },
82     { ".mpp", "audio/x-musepack" },
83     { ".ape", "audio/monkeys-audio" },
84     { ".wma", "audio/x-ms-wma" },
85     { ".jpg", "image/jpeg" }
86 };
87 
88 struct Range {
89     size_t from;
90     size_t to;
91     size_t total;
92     IDataStream* file;
93 
HeaderValueRange94     std::string HeaderValue() {
95         return "bytes " + std::to_string(from) + "-" + std::to_string(to) + "/" + std::to_string(total);
96     }
97 };
98 
contentType(const std::string & fn)99 static std::string contentType(const std::string& fn) {
100     try {
101         boost::filesystem::path p(fn);
102         std::string ext = boost::trim_copy(p.extension().string());
103         boost::to_lower(ext);
104 
105         auto it = CONTENT_TYPE_MAP.find(ext);
106         if (it != CONTENT_TYPE_MAP.end()) {
107             return it->second;
108         }
109     }
110     catch (...) {
111     }
112 
113     return "application/octet-stream";
114 }
115 
fileExtension(const std::string & fn)116 static std::string fileExtension(const std::string& fn) {
117     try {
118         boost::filesystem::path p(fn);
119         std::string ext = boost::trim_copy(p.extension().string());
120         if (ext.size()) {
121             boost::to_lower(ext);
122             return ext[0] == '.' ? ext.substr(1) : ext;
123         }
124     }
125     catch (...) {
126     }
127 
128     return "mp3";
129 }
130 
fileReadCallback(void * cls,uint64_t pos,char * buf,size_t max)131 static ssize_t fileReadCallback(void *cls, uint64_t pos, char *buf, size_t max) {
132     Range* range = static_cast<Range*>(cls);
133 
134     size_t offset = (size_t) pos + range->from;
135     offset = std::min(range->to ? range->to : (size_t) SIZE_MAX, offset);
136 
137     size_t avail = range->total ? (range->total - offset) : SIZE_MAX;
138     size_t count = std::min(avail, max);
139 
140     if (range->file->Seekable()) {
141         if (!range->file->SetPosition(offset)) {
142             return MHD_CONTENT_READER_END_OF_STREAM;
143         }
144     }
145 
146     count = range->file->Read(buf, count);
147     if (count > 0) {
148         return count;
149     }
150 
151     return MHD_CONTENT_READER_END_OF_STREAM;
152 }
153 
fileFreeCallback(void * cls)154 static void fileFreeCallback(void *cls) {
155     Range* range = static_cast<Range*>(cls);
156     if (range->file) {
157 #ifdef ENABLE_DEBUG
158         std::cerr << "******** REQUEST CLOSE: " << range->file << " ********\n\n";
159 #endif
160 
161         range->file->Close(); /* lazy destroy */
162         range->file = nullptr;
163     }
164     delete range;
165 }
166 
parseRange(IDataStream * file,const char * range)167 static Range* parseRange(IDataStream* file, const char* range) {
168     Range* result = new Range();
169 
170     size_t size = file ? file->Length() : 0;
171 
172     result->file = file;
173     result->total = size;
174     result->from = 0;
175     result->to = (size <= 0) ? 0 : size - 1;
176 
177     if (range) {
178         std::string str(range);
179 
180         if (str.substr(0, 6) == "bytes=") {
181             str = str.substr(6);
182 
183             std::vector<std::string> parts;
184             boost::split(parts, str, boost::is_any_of("-"));
185 
186             if (parts.size() == 2) {
187                 try {
188                     size_t from = (size_t) std::max(0, std::stoi(boost::algorithm::trim_copy(parts[0])));
189                     size_t to = size;
190 
191                     if (parts.at(1).size()) {
192                         to = (size_t) std::min((int) size, std::stoi(boost::algorithm::trim_copy(parts[1])));
193                     }
194 
195                     if (to > from) {
196                         result->from = from;
197                         if (to == 0) {
198                             result->to = 0;
199                         }
200                         else if (to >= size) {
201                             result->to = (size == 0) ? 0 : size - 1;
202                         }
203                         else {
204                             result->to = (to == 0) ? 0 : to - 1;
205                         }
206                     }
207                 }
208                 catch (...) {
209                     /* return false below */
210                 }
211             }
212         }
213     }
214 
215     return result;
216 }
217 
getUnsignedUrlParam(struct MHD_Connection * connection,const std::string & argument,size_t defaultValue)218 static size_t getUnsignedUrlParam(
219     struct MHD_Connection *connection,
220     const std::string& argument,
221     size_t defaultValue)
222 {
223     const char* stringValue =
224         MHD_lookup_connection_value(
225             connection,
226             MHD_GET_ARGUMENT_KIND,
227             argument.c_str());
228 
229     if (stringValue != 0) {
230         try {
231             return std::stoul(urlDecode(stringValue));
232         }
233         catch (...) {
234             /* invalid bitrate */
235         }
236     }
237 
238     return defaultValue;
239 }
240 
getStringUrlParam(struct MHD_Connection * connection,const std::string & argument,std::string defaultValue)241 static std::string getStringUrlParam(
242     struct MHD_Connection *connection,
243     const std::string& argument,
244     std::string defaultValue)
245 {
246     const char* stringValue =
247         MHD_lookup_connection_value(
248             connection,
249             MHD_GET_ARGUMENT_KIND,
250             argument.c_str());
251 
252     return stringValue ? std::string(stringValue) : defaultValue;
253 }
254 
isAuthenticated(MHD_Connection * connection,Context & context)255 static bool isAuthenticated(MHD_Connection *connection, Context& context) {
256     const char* disableAuth = std::getenv(ENVIRONMENT_DISABLE_HTTP_SERVER_AUTH);
257     if (disableAuth && std::string(disableAuth) == "1") {
258         return true;
259     }
260 
261     const char* authPtr = MHD_lookup_connection_value(
262         connection, MHD_HEADER_KIND, "Authorization");
263 
264     if (authPtr && strlen(authPtr)) {
265         std::string auth(authPtr);
266         if (auth.find("Basic ") == 0) {
267             std::string encoded = auth.substr(6);
268             if (encoded.size()) {
269                 std::string decoded = websocketpp::base64_decode(encoded);
270 
271                 std::vector<std::string> userPass;
272                 boost::split(userPass, decoded, boost::is_any_of(":"));
273 
274                 if (userPass.size() == 2) {
275                     std::string password = GetPreferenceString(context.prefs, key::password, defaults::password);
276                     return userPass[0] == "default" && userPass[1] == password;
277                 }
278             }
279         }
280     }
281 
282     return false;
283 }
284 
HttpServer(Context & context)285 HttpServer::HttpServer(Context& context)
286 : context(context)
287 , running(false) {
288     this->httpServer = nullptr;
289 }
290 
~HttpServer()291 HttpServer::~HttpServer() {
292     this->Stop();
293 }
294 
Wait()295 void HttpServer::Wait() {
296     std::unique_lock<std::mutex> lock(this->exitMutex);
297     while (this->running) {
298         this->exitCondition.wait(lock);
299     }
300 }
301 
Start()302 bool HttpServer::Start() {
303     if (this->Stop()) {
304         Transcoder::RemoveTempTranscodeFiles(this->context);
305 
306         MHD_FLAG ipVersion = MHD_NO_FLAG;
307         if (context.prefs->GetBool(prefs::use_ipv6.c_str(), defaults::use_ipv6)) {
308             ipVersion = MHD_USE_IPv6;
309         }
310 
311         int serverFlags =
312 #if MHD_VERSION >= 0x00095300
313             MHD_USE_AUTO | MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_THREAD_PER_CONNECTION | ipVersion;
314 #else
315             MHD_USE_SELECT_INTERNALLY | MHD_USE_THREAD_PER_CONNECTION | ipVersion;
316 #endif
317 
318         int serverPort =
319             context.prefs->GetInt(prefs::http_server_port.c_str(), defaults::http_server_port);
320 
321         httpServer = MHD_start_daemon(
322             serverFlags,
323             serverPort,
324             nullptr,                                    /* accept() policy callback */
325             nullptr,                                    /* accept() policy callback data */
326             &HttpServer::HandleRequest,                 /* request handler callback */
327             this,                                       /* request handler callback data */
328             MHD_OPTION_UNESCAPE_CALLBACK,               /* option to configure unescaping */
329             &HttpServer::HandleUnescape,                /* callback to be called for unescaping data */
330             this,                                       /* unescape data callback data */
331             MHD_OPTION_LISTENING_ADDRESS_REUSE,         /* option to configure address reuse */
332             1,                                          /* enable address reuse */
333             MHD_OPTION_END);                            /* terminal option */
334 
335         this->running = (httpServer != nullptr);
336         return running;
337     }
338 
339     return false;
340 }
341 
Stop()342 bool HttpServer::Stop() {
343     if (httpServer) {
344         MHD_stop_daemon(this->httpServer);
345         this->httpServer = nullptr;
346     }
347 
348     this->running = false;
349     this->exitCondition.notify_all();
350 
351     return true;
352 }
353 
HandleUnescape(void * cls,struct MHD_Connection * c,char * s)354 size_t HttpServer::HandleUnescape(void * cls, struct MHD_Connection *c, char *s) {
355     /* don't do anything. the default implementation will decode the
356     entire path, which breaks if we have individually decoded segments. */
357     return strlen(s);
358 }
359 
HandleRequest(void * cls,struct MHD_Connection * connection,const char * url,const char * method,const char * version,const char * upload_data,size_t * upload_data_size,void ** con_cls)360 MHD_Result HttpServer::HandleRequest(
361     void *cls,
362     struct MHD_Connection *connection,
363     const char *url,
364     const char *method,
365     const char *version,
366     const char *upload_data,
367     size_t *upload_data_size,
368     void **con_cls)
369 {
370 #ifdef ENABLE_DEBUG
371     std::cerr << "******** REQUEST START ********\n";
372 #endif
373 
374     HttpServer* server = static_cast<HttpServer*>(cls);
375 
376     struct MHD_Response* response = nullptr;
377     int ret = MHD_NO;
378     int status = MHD_HTTP_NOT_FOUND;
379 
380     try {
381         if (method && std::string(method) == "GET") {
382             if (!isAuthenticated(connection, server->context)) {
383                 status = 401; /* unauthorized */
384                 static const char* error = "unauthorized";
385                 response = MHD_create_response_from_buffer(strlen(error), (void*)error, MHD_RESPMEM_PERSISTENT);
386 
387 #ifdef ENABLE_DEBUG
388                 std::cerr << "unauthorized\n";
389 #endif
390             }
391             else {
392                 /* if we get here we're authenticated */
393                 std::string urlStr(url);
394 
395                 if (urlStr[0] == '/') {
396                     urlStr = urlStr.substr(1);
397                 }
398 
399                 std::vector<std::string> parts;
400                 boost::split(parts, urlStr, boost::is_any_of("/"));
401                 if (parts.size() > 0) {
402                     /* /audio/id/<id> OR /audio/external_id/<external_id> */
403                     if (parts.at(0) == fragment::audio && parts.size() == 3) {
404                         status = HandleAudioTrackRequest(server, response, connection, parts);
405                     }
406                     /* /thumbnail/<id> */
407                     else if (parts.at(0) == fragment::thumbnail && parts.size() == 2) {
408                         status = HandleThumbnailRequest(server, response, connection, parts);
409                     }
410                 }
411             }
412         }
413     }
414     catch (...) {
415     }
416 
417     if (response) {
418 #ifdef ENABLE_DEBUG
419         std::cerr << "returning with http code: " << status << std::endl;
420 #endif
421 
422         ret = MHD_queue_response(connection, status, response);
423         MHD_destroy_response(response);
424     }
425 
426 #ifdef ENABLE_DEBUG
427     std::cerr << "*******************************\n\n";
428 #endif
429 
430     return (MHD_Result) ret;
431 }
432 
HandleAudioTrackRequest(HttpServer * server,MHD_Response * & response,MHD_Connection * connection,std::vector<std::string> & pathParts)433 int HttpServer::HandleAudioTrackRequest(
434     HttpServer* server,
435     MHD_Response*& response,
436     MHD_Connection *connection,
437     std::vector<std::string>& pathParts)
438 {
439     size_t bitrate = getUnsignedUrlParam(connection, "bitrate", 0);
440     int maxActiveTranscoders = server->context.prefs->GetInt(
441         prefs::transcoder_max_active_count.c_str(),
442         defaults::transcoder_max_active_count);
443 
444     if (bitrate != 0 && Transcoder::GetActiveCount() >= maxActiveTranscoders) {
445         response = MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_PERSISTENT);
446         return MHD_HTTP_TOO_MANY_REQUESTS;
447     }
448 
449     int status = MHD_HTTP_OK;
450 
451     ITrack* track = nullptr;
452     bool byExternalId = (pathParts.at(1) == fragment::external_id);
453 
454     if (byExternalId) {
455         std::string externalId = urlDecode(pathParts.at(2));
456         track = server->context.metadataProxy->QueryTrackByExternalId(externalId.c_str());
457 
458 #ifdef ENABLE_DEBUG
459         std::cerr << "externalId: " << externalId << "\n";
460         std::cerr << "title: " << GetMetadataString(track, "title") << std::endl;
461 #endif
462     }
463     else if (pathParts.at(1) == fragment::id) {
464         uint64_t id = std::stoull(urlDecode(pathParts.at(2)));
465         track = server->context.metadataProxy->QueryTrackById(id);
466     }
467 
468     if (track) {
469         const std::string duration = GetMetadataString(track, key::duration);
470         const std::string filename = GetMetadataString(track, key::filename);
471         const std::string title = GetMetadataString(track, key::title, "");
472         const std::string externalId = GetMetadataString(track, key::external_id, "");
473 
474         track->Release();
475 
476         std::string format = "";
477 
478         if (bitrate != 0) {
479             format = getStringUrlParam(connection, "format", "mp3");
480         }
481 
482         IDataStream* file = (bitrate == 0)
483             ? server->context.environment->GetDataStream(filename.c_str(), OpenFlags::Read)
484             : Transcoder::Transcode(server->context, filename, bitrate, format);
485 
486         const char* rangeVal = MHD_lookup_connection_value(
487             connection, MHD_HEADER_KIND, "Range");
488 
489 #ifdef ENABLE_DEBUG
490         if (rangeVal) {
491             std::cerr << "range header: " << rangeVal << "\n";
492         }
493 #endif
494 
495         Range* range = parseRange(file, rangeVal);
496 
497 #ifdef ENABLE_DEBUG
498         std::cerr << "potential response header : " << range->HeaderValue() << std::endl;
499 #endif
500 
501         /* ehh... */
502         bool isOnDemandTranscoder = !!dynamic_cast<TranscodingAudioDataStream*>(file);
503 
504 #ifdef ENABLE_DEBUG
505         std::cerr << "on demand? " << isOnDemandTranscoder << std::endl;
506 #endif
507 
508         /* gotta be careful with request ranges if we're transcoding. don't
509         allow any custom ranges other than from 0 to end. */
510         if (isOnDemandTranscoder && rangeVal && strlen(rangeVal)) {
511             if (range->from != 0 || range->to != range->total - 1) {
512                 delete range;
513 
514 #ifdef ENABLE_DEBUG
515                 std::cerr << "removing range header, seek requested with ondemand transcoder\n";
516 #endif
517 
518                 if (HTTP_416_DISABLED) {
519                     rangeVal = nullptr; /* ignore the header from here on out. */
520 
521                     /* lots of clients don't seem to be to deal with 416 properly;
522                     instead, ignore the range header and return the whole file,
523                     and a 200 (not 206) */
524                     if (file) {
525                         range = parseRange(file, nullptr);
526                     }
527                 }
528                 else {
529                     if (file) {
530                         file->Release();
531                         file = nullptr;
532                     }
533 
534                     if (server->context.prefs->GetBool(
535                         prefs::transcoder_synchronous_fallback.c_str(),
536                         defaults::transcoder_synchronous_fallback))
537                     {
538                         /* if we're allowed, fall back to synchronous transcoding. we'll block
539                         here until the entire file has been converted and cached */
540                         file = Transcoder::TranscodeAndWait(server->context, nullptr, filename, bitrate, format);
541                         range = parseRange(file, rangeVal);
542                     }
543                     else {
544                         /* otherwise fail with a "range not satisfiable" status */
545                         status = 416;
546                         char empty[1];
547                         response = MHD_create_response_from_buffer(0, empty, MHD_RESPMEM_PERSISTENT);
548                     }
549                 }
550             }
551         }
552 
553         if (file) {
554             size_t length = (range->to - range->from);
555 
556             response = MHD_create_response_from_callback(
557                 length == 0 ? MHD_SIZE_UNKNOWN : length + 1,
558                 4096,
559                 &fileReadCallback,
560                 range,
561                 &fileFreeCallback);
562 
563 #ifdef ENABLE_DEBUG
564             std::cerr << "response length: " << ((length == 0) ? 0 : length + 1) << "\n";
565             std::cerr << "id: " << file << "\n";
566 #endif
567 
568             if (response) {
569                 /* 'format' will be valid if we're transcoding. otherwise, extract the extension
570                 from the filename. the client can use this as a hint when naming downloaded files */
571                 std::string extension = format.size() ? format : fileExtension(filename);
572                 MHD_add_response_header(response, "X-musikcube-File-Extension", extension.c_str());
573 
574                 if (!isOnDemandTranscoder) {
575                     MHD_add_response_header(response, "Accept-Ranges", "bytes");
576 
577                     if (boost::filesystem::exists(title)) {
578                         MHD_add_response_header(response, "X-musikcube-Filename-Override", externalId.c_str());
579                     }
580                 }
581                 else {
582                     MHD_add_response_header(response, "X-musikcube-Estimated-Content-Length", "true");
583                 }
584 
585                 if (duration.size()) {
586                     MHD_add_response_header(response, "X-Content-Duration", duration.c_str());
587                     MHD_add_response_header(response, "Content-Duration", duration.c_str());
588                 }
589 
590                 if (byExternalId) {
591                     /* if we're using an on-demand transcoder, ensure the client does not cache the
592                     result because we have to guess the content length. */
593                     std::string value = isOnDemandTranscoder ? "no-cache" : "public, max-age=31536000";
594                     MHD_add_response_header(response, "Cache-Control", value.c_str());
595                 }
596 
597                 std::string type = (isOnDemandTranscoder || format.size())
598                     ? contentType("." + format) : contentType(filename);
599 
600                 MHD_add_response_header(response, "Content-Type", type.c_str());
601                 MHD_add_response_header(response, "Server", "musikcube server");
602 
603                 if ((rangeVal && strlen(rangeVal)) || range->from > 0) {
604                     if (range->total > 0) {
605                         MHD_add_response_header(response, "Content-Range", range->HeaderValue().c_str());
606                         status = MHD_HTTP_PARTIAL_CONTENT;
607 #ifdef ENABLE_DEBUG
608                         if (rangeVal) {
609                             std::cerr << "actual range header: " << range->HeaderValue() << "\n";
610                         }
611 #endif
612                     }
613                 }
614             }
615             else {
616                 file->Release();
617                 file = nullptr;
618             }
619         }
620         else {
621             status = MHD_HTTP_NOT_FOUND;
622         }
623     }
624     else {
625         status = MHD_HTTP_NOT_FOUND;
626     }
627 
628     return status;
629 }
630 
HandleThumbnailRequest(HttpServer * server,MHD_Response * & response,MHD_Connection * connection,std::vector<std::string> & pathParts)631 int HttpServer::HandleThumbnailRequest(
632     HttpServer* server,
633     MHD_Response*& response,
634     MHD_Connection* connection,
635     std::vector<std::string>& pathParts)
636 {
637     int status = MHD_HTTP_NOT_FOUND;
638 
639     char pathBuffer[4096];
640     server->context.environment->GetPath(PathType::Library, pathBuffer, sizeof(pathBuffer));
641 
642     if (strlen(pathBuffer)) {
643         std::string path = std::string(pathBuffer) + "thumbs/" + pathParts.at(1) + ".jpg";
644         IDataStream* file = server->context.environment->GetDataStream(path.c_str(), OpenFlags::Read);
645 
646         if (file) {
647             long length = file->Length();
648 
649             response = MHD_create_response_from_callback(
650                 length == 0 ? MHD_SIZE_UNKNOWN : length + 1,
651                 4096,
652                 &fileReadCallback,
653                 parseRange(file, nullptr),
654                 &fileFreeCallback);
655 
656             if (response) {
657                 MHD_add_response_header(response, "Cache-Control", "public, max-age=31536000");
658                 MHD_add_response_header(response, "Content-Type", contentType(path).c_str());
659                 MHD_add_response_header(response, "Server", "musikcube server");
660                 status = MHD_HTTP_OK;
661             }
662             else {
663                 file->Release();
664             }
665         }
666     }
667 
668     return status;
669 }
670