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