comparison OrthancFramework/Sources/HttpServer/HttpServer.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents Core/HttpServer/HttpServer.cpp@0c16051dfd56
children bf7b9edf6b81
comparison
equal deleted inserted replaced
4043:6c6239aec462 4044:d25f4c0fa160
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * In addition, as a special exception, the copyright holders of this
13 * program give permission to link the code of its release with the
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
15 * that use the same license as the "OpenSSL" library), and distribute
16 * the linked executables. You must obey the GNU General Public License
17 * in all respects for all of the code used other than "OpenSSL". If you
18 * modify file(s) with this exception, you may extend this exception to
19 * your version of the file(s), but you are not obligated to do so. If
20 * you do not wish to do so, delete this exception statement from your
21 * version. If you delete this exception statement from all source files
22 * in the program, then also delete it here.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31 **/
32
33
34 // http://en.highscore.de/cpp/boost/stringhandling.html
35
36 #include "../PrecompiledHeaders.h"
37 #include "HttpServer.h"
38
39 #include "../ChunkedBuffer.h"
40 #include "../FileBuffer.h"
41 #include "../Logging.h"
42 #include "../OrthancException.h"
43 #include "../TemporaryFile.h"
44 #include "HttpToolbox.h"
45
46 #if ORTHANC_ENABLE_MONGOOSE == 1
47 # include <mongoose.h>
48
49 #elif ORTHANC_ENABLE_CIVETWEB == 1
50 # include <civetweb.h>
51 # define MONGOOSE_USE_CALLBACKS 1
52 # if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
53 # error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
54 # endif
55
56 #else
57 # error "Either Mongoose or Civetweb must be enabled to compile this file"
58 #endif
59
60 #include <algorithm>
61 #include <string.h>
62 #include <boost/lexical_cast.hpp>
63 #include <boost/algorithm/string.hpp>
64 #include <boost/filesystem.hpp>
65 #include <iostream>
66 #include <string.h>
67 #include <stdio.h>
68 #include <boost/thread.hpp>
69
70 #if !defined(ORTHANC_ENABLE_SSL)
71 # error The macro ORTHANC_ENABLE_SSL must be defined
72 #endif
73
74 #if ORTHANC_ENABLE_SSL == 1
75 # include <openssl/opensslv.h>
76 # include <openssl/err.h>
77 #endif
78
79 #define ORTHANC_REALM "Orthanc Secure Area"
80
81
82 namespace Orthanc
83 {
84 static const char MULTIPART_FORM[] = "multipart/form-data; boundary=";
85 static unsigned int MULTIPART_FORM_LENGTH = sizeof(MULTIPART_FORM) / sizeof(char) - 1;
86
87
88 namespace
89 {
90 // Anonymous namespace to avoid clashes between compilation modules
91 class MongooseOutputStream : public IHttpOutputStream
92 {
93 private:
94 struct mg_connection* connection_;
95
96 public:
97 MongooseOutputStream(struct mg_connection* connection) : connection_(connection)
98 {
99 }
100
101 virtual void Send(bool isHeader, const void* buffer, size_t length)
102 {
103 if (length > 0)
104 {
105 int status = mg_write(connection_, buffer, length);
106 if (status != static_cast<int>(length))
107 {
108 // status == 0 when the connection has been closed, -1 on error
109 throw OrthancException(ErrorCode_NetworkProtocol);
110 }
111 }
112 }
113
114 virtual void OnHttpStatusReceived(HttpStatus status)
115 {
116 // Ignore this
117 }
118
119 virtual void DisableKeepAlive()
120 {
121 #if ORTHANC_ENABLE_MONGOOSE == 1
122 throw OrthancException(ErrorCode_NotImplemented,
123 "Only available if using CivetWeb");
124
125 #elif ORTHANC_ENABLE_CIVETWEB == 1
126 # if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
127 mg_disable_keep_alive(connection_);
128 # else
129 # warning The function "mg_disable_keep_alive()" is not available, DICOMweb might run slowly
130 throw OrthancException(ErrorCode_NotImplemented,
131 "Only available if using a patched version of CivetWeb");
132 # endif
133
134 #else
135 # error Please support your embedded Web server here
136 #endif
137 }
138 };
139
140
141 enum PostDataStatus
142 {
143 PostDataStatus_Success,
144 PostDataStatus_NoLength,
145 PostDataStatus_Pending,
146 PostDataStatus_Failure
147 };
148 }
149
150
151 // TODO Move this to external file
152
153
154 class ChunkedFile : public ChunkedBuffer
155 {
156 private:
157 std::string filename_;
158
159 public:
160 ChunkedFile(const std::string& filename) :
161 filename_(filename)
162 {
163 }
164
165 const std::string& GetFilename() const
166 {
167 return filename_;
168 }
169 };
170
171
172
173 class ChunkStore : public boost::noncopyable
174 {
175 private:
176 typedef std::list<ChunkedFile*> Content;
177 Content content_;
178 unsigned int numPlaces_;
179
180 boost::mutex mutex_;
181 std::set<std::string> discardedFiles_;
182
183 void Clear()
184 {
185 for (Content::iterator it = content_.begin();
186 it != content_.end(); ++it)
187 {
188 delete *it;
189 }
190 }
191
192 Content::iterator Find(const std::string& filename)
193 {
194 for (Content::iterator it = content_.begin();
195 it != content_.end(); ++it)
196 {
197 if ((*it)->GetFilename() == filename)
198 {
199 return it;
200 }
201 }
202
203 return content_.end();
204 }
205
206 void Remove(const std::string& filename)
207 {
208 Content::iterator it = Find(filename);
209 if (it != content_.end())
210 {
211 delete *it;
212 content_.erase(it);
213 }
214 }
215
216 public:
217 ChunkStore()
218 {
219 numPlaces_ = 10;
220 }
221
222 ~ChunkStore()
223 {
224 Clear();
225 }
226
227 PostDataStatus Store(std::string& completed,
228 const char* chunkData,
229 size_t chunkSize,
230 const std::string& filename,
231 size_t filesize)
232 {
233 boost::mutex::scoped_lock lock(mutex_);
234
235 std::set<std::string>::iterator wasDiscarded = discardedFiles_.find(filename);
236 if (wasDiscarded != discardedFiles_.end())
237 {
238 discardedFiles_.erase(wasDiscarded);
239 return PostDataStatus_Failure;
240 }
241
242 ChunkedFile* f;
243 Content::iterator it = Find(filename);
244 if (it == content_.end())
245 {
246 f = new ChunkedFile(filename);
247
248 // Make some room
249 if (content_.size() >= numPlaces_)
250 {
251 discardedFiles_.insert(content_.front()->GetFilename());
252 delete content_.front();
253 content_.pop_front();
254 }
255
256 content_.push_back(f);
257 }
258 else
259 {
260 f = *it;
261 }
262
263 f->AddChunk(chunkData, chunkSize);
264
265 if (f->GetNumBytes() > filesize)
266 {
267 Remove(filename);
268 }
269 else if (f->GetNumBytes() == filesize)
270 {
271 f->Flatten(completed);
272 Remove(filename);
273 return PostDataStatus_Success;
274 }
275
276 return PostDataStatus_Pending;
277 }
278
279 /*void Print()
280 {
281 boost::mutex::scoped_lock lock(mutex_);
282
283 printf("ChunkStore status:\n");
284 for (Content::const_iterator i = content_.begin();
285 i != content_.end(); i++)
286 {
287 printf(" [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes());
288 }
289 printf("-----\n");
290 }*/
291 };
292
293
294 struct HttpServer::PImpl
295 {
296 struct mg_context *context_;
297 ChunkStore chunkStore_;
298 };
299
300
301 ChunkStore& HttpServer::GetChunkStore()
302 {
303 return pimpl_->chunkStore_;
304 }
305
306
307 static PostDataStatus ReadBodyWithContentLength(std::string& body,
308 struct mg_connection *connection,
309 const std::string& contentLength)
310 {
311 int length;
312 try
313 {
314 length = boost::lexical_cast<int>(contentLength);
315 }
316 catch (boost::bad_lexical_cast&)
317 {
318 return PostDataStatus_NoLength;
319 }
320
321 if (length < 0)
322 {
323 length = 0;
324 }
325
326 body.resize(length);
327
328 size_t pos = 0;
329 while (length > 0)
330 {
331 int r = mg_read(connection, &body[pos], length);
332 if (r <= 0)
333 {
334 return PostDataStatus_Failure;
335 }
336
337 assert(r <= length);
338 length -= r;
339 pos += r;
340 }
341
342 return PostDataStatus_Success;
343 }
344
345
346 static PostDataStatus ReadBodyToString(std::string& body,
347 struct mg_connection *connection,
348 const IHttpHandler::Arguments& headers)
349 {
350 IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length");
351
352 if (contentLength != headers.end())
353 {
354 // "Content-Length" is available
355 return ReadBodyWithContentLength(body, connection, contentLength->second);
356 }
357 else
358 {
359 // No Content-Length. Store the individual chunks in a temporary
360 // file, then read it back into the memory buffer "body"
361 FileBuffer buffer;
362
363 std::string tmp(1024 * 1024, 0);
364
365 for (;;)
366 {
367 int r = mg_read(connection, &tmp[0], tmp.size());
368 if (r < 0)
369 {
370 return PostDataStatus_Failure;
371 }
372 else if (r == 0)
373 {
374 break;
375 }
376 else
377 {
378 buffer.Append(tmp.c_str(), r);
379 }
380 }
381
382 buffer.Read(body);
383
384 return PostDataStatus_Success;
385 }
386 }
387
388
389 static PostDataStatus ReadBodyToStream(IHttpHandler::IChunkedRequestReader& stream,
390 struct mg_connection *connection,
391 const IHttpHandler::Arguments& headers)
392 {
393 IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length");
394
395 if (contentLength != headers.end())
396 {
397 // "Content-Length" is available
398 std::string body;
399 PostDataStatus status = ReadBodyWithContentLength(body, connection, contentLength->second);
400
401 if (status == PostDataStatus_Success &&
402 !body.empty())
403 {
404 stream.AddBodyChunk(body.c_str(), body.size());
405 }
406
407 return status;
408 }
409 else
410 {
411 // No Content-Length: This is a chunked transfer. Stream the HTTP connection.
412 std::string tmp(1024 * 1024, 0);
413
414 for (;;)
415 {
416 int r = mg_read(connection, &tmp[0], tmp.size());
417 if (r < 0)
418 {
419 return PostDataStatus_Failure;
420 }
421 else if (r == 0)
422 {
423 break;
424 }
425 else
426 {
427 stream.AddBodyChunk(tmp.c_str(), r);
428 }
429 }
430
431 return PostDataStatus_Success;
432 }
433 }
434
435
436 static PostDataStatus ParseMultipartForm(std::string &completedFile,
437 struct mg_connection *connection,
438 const IHttpHandler::Arguments& headers,
439 const std::string& contentType,
440 ChunkStore& chunkStore)
441 {
442 std::string boundary = "--" + contentType.substr(MULTIPART_FORM_LENGTH);
443
444 std::string body;
445 PostDataStatus status = ReadBodyToString(body, connection, headers);
446
447 if (status != PostDataStatus_Success)
448 {
449 return status;
450 }
451
452 /*for (IHttpHandler::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++)
453 {
454 std::cout << "Header [" << i->first << "] = " << i->second << "\n";
455 }
456 printf("CHUNK\n");*/
457
458 typedef IHttpHandler::Arguments::const_iterator ArgumentIterator;
459
460 ArgumentIterator requestedWith = headers.find("x-requested-with");
461 ArgumentIterator fileName = headers.find("x-file-name");
462 ArgumentIterator fileSizeStr = headers.find("x-file-size");
463
464 if (requestedWith != headers.end() &&
465 requestedWith->second != "XMLHttpRequest")
466 {
467 return PostDataStatus_Failure;
468 }
469
470 size_t fileSize = 0;
471 if (fileSizeStr != headers.end())
472 {
473 try
474 {
475 fileSize = boost::lexical_cast<size_t>(fileSizeStr->second);
476 }
477 catch (boost::bad_lexical_cast&)
478 {
479 return PostDataStatus_Failure;
480 }
481 }
482
483 typedef boost::find_iterator<std::string::iterator> FindIterator;
484 typedef boost::iterator_range<char*> Range;
485
486 //chunkStore.Print();
487
488 // TODO - Refactor using class "MultipartStreamReader"
489 try
490 {
491 FindIterator last;
492 for (FindIterator it =
493 make_find_iterator(body, boost::first_finder(boundary));
494 it!=FindIterator();
495 ++it)
496 {
497 if (last != FindIterator())
498 {
499 Range part(&last->back(), &it->front());
500 Range content = boost::find_first(part, "\r\n\r\n");
501 if (/*content != Range()*/!content.empty())
502 {
503 Range c(&content.back() + 1, &it->front() - 2);
504 size_t chunkSize = c.size();
505
506 if (chunkSize > 0)
507 {
508 const char* chunkData = &c.front();
509
510 if (fileName == headers.end())
511 {
512 // This file is stored in a single chunk
513 completedFile.resize(chunkSize);
514 if (chunkSize > 0)
515 {
516 memcpy(&completedFile[0], chunkData, chunkSize);
517 }
518 return PostDataStatus_Success;
519 }
520 else
521 {
522 return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize);
523 }
524 }
525 }
526 }
527
528 last = it;
529 }
530 }
531 catch (std::length_error&)
532 {
533 return PostDataStatus_Failure;
534 }
535
536 return PostDataStatus_Pending;
537 }
538
539
540 static bool IsAccessGranted(const HttpServer& that,
541 const IHttpHandler::Arguments& headers)
542 {
543 bool granted = false;
544
545 IHttpHandler::Arguments::const_iterator auth = headers.find("authorization");
546 if (auth != headers.end())
547 {
548 std::string s = auth->second;
549 if (s.size() > 6 &&
550 s.substr(0, 6) == "Basic ")
551 {
552 std::string b64 = s.substr(6);
553 granted = that.IsValidBasicHttpAuthentication(b64);
554 }
555 }
556
557 return granted;
558 }
559
560
561 static std::string GetAuthenticatedUsername(const IHttpHandler::Arguments& headers)
562 {
563 IHttpHandler::Arguments::const_iterator auth = headers.find("authorization");
564
565 if (auth == headers.end())
566 {
567 return "";
568 }
569
570 std::string s = auth->second;
571 if (s.size() <= 6 ||
572 s.substr(0, 6) != "Basic ")
573 {
574 return "";
575 }
576
577 std::string b64 = s.substr(6);
578 std::string decoded;
579 Toolbox::DecodeBase64(decoded, b64);
580 size_t semicolons = decoded.find(':');
581
582 if (semicolons == std::string::npos)
583 {
584 // Bad-formatted request
585 return "";
586 }
587 else
588 {
589 return decoded.substr(0, semicolons);
590 }
591 }
592
593
594 static bool ExtractMethod(HttpMethod& method,
595 const struct mg_request_info *request,
596 const IHttpHandler::Arguments& headers,
597 const IHttpHandler::GetArguments& argumentsGET)
598 {
599 std::string overriden;
600
601 // Check whether some PUT/DELETE faking is done
602
603 // 1. Faking with Google's approach
604 IHttpHandler::Arguments::const_iterator methodOverride =
605 headers.find("x-http-method-override");
606
607 if (methodOverride != headers.end())
608 {
609 overriden = methodOverride->second;
610 }
611 else if (!strcmp(request->request_method, "GET"))
612 {
613 // 2. Faking with Ruby on Rail's approach
614 // GET /my/resource?_method=delete <=> DELETE /my/resource
615 for (size_t i = 0; i < argumentsGET.size(); i++)
616 {
617 if (argumentsGET[i].first == "_method")
618 {
619 overriden = argumentsGET[i].second;
620 break;
621 }
622 }
623 }
624
625 if (overriden.size() > 0)
626 {
627 // A faking has been done within this request
628 Toolbox::ToUpperCase(overriden);
629
630 LOG(INFO) << "HTTP method faking has been detected for " << overriden;
631
632 if (overriden == "PUT")
633 {
634 method = HttpMethod_Put;
635 return true;
636 }
637 else if (overriden == "DELETE")
638 {
639 method = HttpMethod_Delete;
640 return true;
641 }
642 else
643 {
644 return false;
645 }
646 }
647
648 // No PUT/DELETE faking was present
649 if (!strcmp(request->request_method, "GET"))
650 {
651 method = HttpMethod_Get;
652 }
653 else if (!strcmp(request->request_method, "POST"))
654 {
655 method = HttpMethod_Post;
656 }
657 else if (!strcmp(request->request_method, "DELETE"))
658 {
659 method = HttpMethod_Delete;
660 }
661 else if (!strcmp(request->request_method, "PUT"))
662 {
663 method = HttpMethod_Put;
664 }
665 else
666 {
667 return false;
668 }
669
670 return true;
671 }
672
673
674 static void ConfigureHttpCompression(HttpOutput& output,
675 const IHttpHandler::Arguments& headers)
676 {
677 // Look if the client wishes HTTP compression
678 // https://en.wikipedia.org/wiki/HTTP_compression
679 IHttpHandler::Arguments::const_iterator it = headers.find("accept-encoding");
680 if (it != headers.end())
681 {
682 std::vector<std::string> encodings;
683 Toolbox::TokenizeString(encodings, it->second, ',');
684
685 for (size_t i = 0; i < encodings.size(); i++)
686 {
687 std::string s = Toolbox::StripSpaces(encodings[i]);
688
689 if (s == "deflate")
690 {
691 output.SetDeflateAllowed(true);
692 }
693 else if (s == "gzip")
694 {
695 output.SetGzipAllowed(true);
696 }
697 }
698 }
699 }
700
701
702 static void InternalCallback(HttpOutput& output /* out */,
703 HttpMethod& method /* out */,
704 HttpServer& server,
705 struct mg_connection *connection,
706 const struct mg_request_info *request)
707 {
708 bool localhost;
709
710 #if ORTHANC_ENABLE_MONGOOSE == 1
711 static const long LOCALHOST = (127ll << 24) + 1ll;
712 localhost = (request->remote_ip == LOCALHOST);
713 #elif ORTHANC_ENABLE_CIVETWEB == 1
714 // The "remote_ip" field of "struct mg_request_info" is tagged as
715 // deprecated in Civetweb, using "remote_addr" instead.
716 localhost = (std::string(request->remote_addr) == "127.0.0.1");
717 #else
718 # error
719 #endif
720
721 // Check remote calls
722 if (!server.IsRemoteAccessAllowed() &&
723 !localhost)
724 {
725 output.SendUnauthorized(server.GetRealm());
726 return;
727 }
728
729
730 // Extract the HTTP headers
731 IHttpHandler::Arguments headers;
732 for (int i = 0; i < request->num_headers; i++)
733 {
734 std::string name = request->http_headers[i].name;
735 std::string value = request->http_headers[i].value;
736
737 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
738 headers.insert(std::make_pair(name, value));
739 VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]";
740 }
741
742 if (server.IsHttpCompressionEnabled())
743 {
744 ConfigureHttpCompression(output, headers);
745 }
746
747
748 // Extract the GET arguments
749 IHttpHandler::GetArguments argumentsGET;
750 if (!strcmp(request->request_method, "GET"))
751 {
752 HttpToolbox::ParseGetArguments(argumentsGET, request->query_string);
753 }
754
755
756 // Compute the HTTP method, taking method faking into consideration
757 method = HttpMethod_Get;
758 if (!ExtractMethod(method, request, headers, argumentsGET))
759 {
760 output.SendStatus(HttpStatus_400_BadRequest);
761 return;
762 }
763
764
765 // Authenticate this connection
766 if (server.IsAuthenticationEnabled() &&
767 !IsAccessGranted(server, headers))
768 {
769 output.SendUnauthorized(server.GetRealm());
770 return;
771 }
772
773
774 #if ORTHANC_ENABLE_MONGOOSE == 1
775 // Apply the filter, if it is installed
776 char remoteIp[24];
777 sprintf(remoteIp, "%d.%d.%d.%d",
778 reinterpret_cast<const uint8_t*>(&request->remote_ip) [3],
779 reinterpret_cast<const uint8_t*>(&request->remote_ip) [2],
780 reinterpret_cast<const uint8_t*>(&request->remote_ip) [1],
781 reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]);
782
783 const char* requestUri = request->uri;
784
785 #elif ORTHANC_ENABLE_CIVETWEB == 1
786 const char* remoteIp = request->remote_addr;
787 const char* requestUri = request->local_uri;
788 #else
789 # error
790 #endif
791
792 if (requestUri == NULL)
793 {
794 requestUri = "";
795 }
796
797 std::string username = GetAuthenticatedUsername(headers);
798
799 IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter();
800 if (filter != NULL)
801 {
802 if (!filter->IsAllowed(method, requestUri, remoteIp,
803 username.c_str(), headers, argumentsGET))
804 {
805 //output.SendUnauthorized(server.GetRealm());
806 output.SendStatus(HttpStatus_403_Forbidden);
807 return;
808 }
809 }
810
811
812 // Decompose the URI into its components
813 UriComponents uri;
814 try
815 {
816 Toolbox::SplitUriComponents(uri, requestUri);
817 }
818 catch (OrthancException&)
819 {
820 output.SendStatus(HttpStatus_400_BadRequest);
821 return;
822 }
823
824 LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
825
826
827 bool found = false;
828
829 // Extract the body of the request for PUT and POST, or process
830 // the body as a stream
831
832 // TODO Avoid unneccessary memcopy of the body
833
834 std::string body;
835 if (method == HttpMethod_Post ||
836 method == HttpMethod_Put)
837 {
838 PostDataStatus status;
839
840 bool isMultipartForm = false;
841
842 IHttpHandler::Arguments::const_iterator ct = headers.find("content-type");
843 if (ct != headers.end() &&
844 ct->second.size() >= MULTIPART_FORM_LENGTH &&
845 !memcmp(ct->second.c_str(), MULTIPART_FORM, MULTIPART_FORM_LENGTH))
846 {
847 /**
848 * The user uses the "upload" form of Orthanc Explorer, for
849 * file uploads through a HTML form.
850 **/
851 status = ParseMultipartForm(body, connection, headers, ct->second, server.GetChunkStore());
852 isMultipartForm = true;
853 }
854
855 if (!isMultipartForm)
856 {
857 std::unique_ptr<IHttpHandler::IChunkedRequestReader> stream;
858
859 if (server.HasHandler())
860 {
861 found = server.GetHandler().CreateChunkedRequestReader
862 (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers);
863 }
864
865 if (found)
866 {
867 if (stream.get() == NULL)
868 {
869 throw OrthancException(ErrorCode_InternalError);
870 }
871
872 status = ReadBodyToStream(*stream, connection, headers);
873
874 if (status == PostDataStatus_Success)
875 {
876 stream->Execute(output);
877 }
878 }
879 else
880 {
881 status = ReadBodyToString(body, connection, headers);
882 }
883 }
884
885 switch (status)
886 {
887 case PostDataStatus_NoLength:
888 output.SendStatus(HttpStatus_411_LengthRequired);
889 return;
890
891 case PostDataStatus_Failure:
892 output.SendStatus(HttpStatus_400_BadRequest);
893 return;
894
895 case PostDataStatus_Pending:
896 output.AnswerEmpty();
897 return;
898
899 case PostDataStatus_Success:
900 break;
901
902 default:
903 throw OrthancException(ErrorCode_InternalError);
904 }
905 }
906
907 if (!found &&
908 server.HasHandler())
909 {
910 found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(),
911 method, uri, headers, argumentsGET, body.c_str(), body.size());
912 }
913
914 if (!found)
915 {
916 throw OrthancException(ErrorCode_UnknownResource);
917 }
918 }
919
920
921 static void ProtectedCallback(struct mg_connection *connection,
922 const struct mg_request_info *request)
923 {
924 try
925 {
926 #if ORTHANC_ENABLE_MONGOOSE == 1
927 void *that = request->user_data;
928 const char* requestUri = request->uri;
929 #elif ORTHANC_ENABLE_CIVETWEB == 1
930 // https://github.com/civetweb/civetweb/issues/409
931 void *that = mg_get_user_data(mg_get_context(connection));
932 const char* requestUri = request->local_uri;
933 #else
934 # error
935 #endif
936
937 if (requestUri == NULL)
938 {
939 requestUri = "";
940 }
941
942 HttpServer* server = reinterpret_cast<HttpServer*>(that);
943
944 if (server == NULL)
945 {
946 MongooseOutputStream stream(connection);
947 HttpOutput output(stream, false /* assume no keep-alive */);
948 output.SendStatus(HttpStatus_500_InternalServerError);
949 return;
950 }
951
952 MongooseOutputStream stream(connection);
953 HttpOutput output(stream, server->IsKeepAliveEnabled());
954 HttpMethod method = HttpMethod_Get;
955
956 try
957 {
958 try
959 {
960 InternalCallback(output, method, *server, connection, request);
961 }
962 catch (OrthancException&)
963 {
964 throw; // Pass the exception to the main handler below
965 }
966 // Now convert native exceptions as OrthancException
967 catch (boost::bad_lexical_cast&)
968 {
969 throw OrthancException(ErrorCode_BadParameterType,
970 "Syntax error in some user-supplied data");
971 }
972 catch (boost::filesystem::filesystem_error& e)
973 {
974 throw OrthancException(ErrorCode_InternalError,
975 "Error while accessing the filesystem: " + e.path1().string());
976 }
977 catch (std::runtime_error&)
978 {
979 throw OrthancException(ErrorCode_BadRequest,
980 "Presumably an error while parsing the JSON body");
981 }
982 catch (std::bad_alloc&)
983 {
984 throw OrthancException(ErrorCode_NotEnoughMemory,
985 "The server hosting Orthanc is running out of memory");
986 }
987 catch (...)
988 {
989 throw OrthancException(ErrorCode_InternalError,
990 "An unhandled exception was generated inside the HTTP server");
991 }
992 }
993 catch (OrthancException& e)
994 {
995 assert(server != NULL);
996
997 // Using this candidate handler results in an exception
998 try
999 {
1000 if (server->GetExceptionFormatter() == NULL)
1001 {
1002 LOG(ERROR) << "Exception in the HTTP handler: " << e.What();
1003 output.SendStatus(e.GetHttpStatus());
1004 }
1005 else
1006 {
1007 server->GetExceptionFormatter()->Format(output, e, method, requestUri);
1008 }
1009 }
1010 catch (OrthancException&)
1011 {
1012 // An exception here reflects the fact that the status code
1013 // was already set by the HTTP handler.
1014 }
1015 }
1016 }
1017 catch (...)
1018 {
1019 // We should never arrive at this point, where it is even impossible to send an answer
1020 LOG(ERROR) << "Catastrophic error inside the HTTP server, giving up";
1021 }
1022 }
1023
1024
1025 #if MONGOOSE_USE_CALLBACKS == 0
1026 static void* Callback(enum mg_event event,
1027 struct mg_connection *connection,
1028 const struct mg_request_info *request)
1029 {
1030 if (event == MG_NEW_REQUEST)
1031 {
1032 ProtectedCallback(connection, request);
1033
1034 // Mark as processed
1035 return (void*) "";
1036 }
1037 else
1038 {
1039 return NULL;
1040 }
1041 }
1042
1043 #elif MONGOOSE_USE_CALLBACKS == 1
1044 static int Callback(struct mg_connection *connection)
1045 {
1046 const struct mg_request_info *request = mg_get_request_info(connection);
1047
1048 ProtectedCallback(connection, request);
1049
1050 return 1; // Do not let Mongoose handle the request by itself
1051 }
1052
1053 #else
1054 # error Please set MONGOOSE_USE_CALLBACKS
1055 #endif
1056
1057
1058
1059
1060
1061 bool HttpServer::IsRunning() const
1062 {
1063 return (pimpl_->context_ != NULL);
1064 }
1065
1066
1067 HttpServer::HttpServer() : pimpl_(new PImpl)
1068 {
1069 pimpl_->context_ = NULL;
1070 handler_ = NULL;
1071 remoteAllowed_ = false;
1072 authentication_ = false;
1073 ssl_ = false;
1074 port_ = 8000;
1075 filter_ = NULL;
1076 keepAlive_ = false;
1077 httpCompression_ = true;
1078 exceptionFormatter_ = NULL;
1079 realm_ = ORTHANC_REALM;
1080 threadsCount_ = 50; // Default value in mongoose
1081 tcpNoDelay_ = true;
1082 requestTimeout_ = 30; // Default value in mongoose/civetweb (30 seconds)
1083
1084 #if ORTHANC_ENABLE_MONGOOSE == 1
1085 LOG(INFO) << "This Orthanc server uses Mongoose as its embedded HTTP server";
1086 #endif
1087
1088 #if ORTHANC_ENABLE_CIVETWEB == 1
1089 LOG(INFO) << "This Orthanc server uses CivetWeb as its embedded HTTP server";
1090 #endif
1091
1092 #if ORTHANC_ENABLE_SSL == 1
1093 // Check for the Heartbleed exploit
1094 // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug
1095 if (OPENSSL_VERSION_NUMBER < 0x1000107fL /* openssl-1.0.1g */ &&
1096 OPENSSL_VERSION_NUMBER >= 0x1000100fL /* openssl-1.0.1 */)
1097 {
1098 LOG(WARNING) << "This version of OpenSSL is vulnerable to the Heartbleed exploit";
1099 }
1100 #endif
1101 }
1102
1103
1104 HttpServer::~HttpServer()
1105 {
1106 Stop();
1107 }
1108
1109
1110 void HttpServer::SetPortNumber(uint16_t port)
1111 {
1112 Stop();
1113 port_ = port;
1114 }
1115
1116 void HttpServer::Start()
1117 {
1118 #if ORTHANC_ENABLE_MONGOOSE == 1
1119 LOG(INFO) << "Starting embedded Web server using Mongoose";
1120 #elif ORTHANC_ENABLE_CIVETWEB == 1
1121 LOG(INFO) << "Starting embedded Web server using Civetweb";
1122 #else
1123 # error
1124 #endif
1125
1126 if (!IsRunning())
1127 {
1128 std::string port = boost::lexical_cast<std::string>(port_);
1129 std::string numThreads = boost::lexical_cast<std::string>(threadsCount_);
1130 std::string requestTimeoutMilliseconds = boost::lexical_cast<std::string>(requestTimeout_ * 1000);
1131
1132 if (ssl_)
1133 {
1134 port += "s";
1135 }
1136
1137 const char *options[] = {
1138 // Set the TCP port for the HTTP server
1139 "listening_ports", port.c_str(),
1140
1141 // Optimization reported by Chris Hafey
1142 // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ
1143 "enable_keep_alive", (keepAlive_ ? "yes" : "no"),
1144
1145 #if ORTHANC_ENABLE_CIVETWEB == 1
1146 // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no
1147 "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"),
1148 #endif
1149
1150 #if ORTHANC_ENABLE_CIVETWEB == 1
1151 // Disable TCP Nagle's algorithm to maximize speed (this
1152 // option is not available in Mongoose).
1153 // https://groups.google.com/d/topic/civetweb/35HBR9seFjU/discussion
1154 // https://eklitzke.org/the-caveats-of-tcp-nodelay
1155 "tcp_nodelay", (tcpNoDelay_ ? "1" : "0"),
1156 #endif
1157
1158 // Set the number of threads
1159 "num_threads", numThreads.c_str(),
1160
1161 // Set the timeout for the HTTP server
1162 "request_timeout_ms", requestTimeoutMilliseconds.c_str(),
1163
1164 // Set the SSL certificate, if any. This must be the last option.
1165 ssl_ ? "ssl_certificate" : NULL,
1166 certificate_.c_str(),
1167 NULL
1168 };
1169
1170 #if MONGOOSE_USE_CALLBACKS == 0
1171 pimpl_->context_ = mg_start(&Callback, this, options);
1172
1173 #elif MONGOOSE_USE_CALLBACKS == 1
1174 struct mg_callbacks callbacks;
1175 memset(&callbacks, 0, sizeof(callbacks));
1176 callbacks.begin_request = Callback;
1177 pimpl_->context_ = mg_start(&callbacks, this, options);
1178
1179 #else
1180 # error Please set MONGOOSE_USE_CALLBACKS
1181 #endif
1182
1183 if (!pimpl_->context_)
1184 {
1185 bool isSslError = false;
1186
1187 #if ORTHANC_ENABLE_SSL == 1
1188 for (;;)
1189 {
1190 unsigned long code = ERR_get_error();
1191 if (code == 0)
1192 {
1193 break;
1194 }
1195 else
1196 {
1197 isSslError = true;
1198 char message[1024];
1199 ERR_error_string_n(code, message, sizeof(message) - 1);
1200 LOG(ERROR) << "OpenSSL error: " << message;
1201 }
1202 }
1203 #endif
1204
1205 if (isSslError)
1206 {
1207 throw OrthancException(ErrorCode_SslInitialization);
1208 }
1209 else
1210 {
1211 throw OrthancException(ErrorCode_HttpPortInUse,
1212 " (port = " + boost::lexical_cast<std::string>(port_) + ")");
1213 }
1214 }
1215
1216 LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber()
1217 << " (HTTPS encryption is "
1218 << (IsSslEnabled() ? "enabled" : "disabled")
1219 << ", remote access is "
1220 << (IsRemoteAccessAllowed() ? "" : "not ")
1221 << "allowed)";
1222 }
1223 }
1224
1225 void HttpServer::Stop()
1226 {
1227 if (IsRunning())
1228 {
1229 mg_stop(pimpl_->context_);
1230 pimpl_->context_ = NULL;
1231 }
1232 }
1233
1234
1235 void HttpServer::ClearUsers()
1236 {
1237 Stop();
1238 registeredUsers_.clear();
1239 }
1240
1241
1242 void HttpServer::RegisterUser(const char* username,
1243 const char* password)
1244 {
1245 Stop();
1246
1247 std::string tag = std::string(username) + ":" + std::string(password);
1248 std::string encoded;
1249 Toolbox::EncodeBase64(encoded, tag);
1250 registeredUsers_.insert(encoded);
1251 }
1252
1253 void HttpServer::SetSslEnabled(bool enabled)
1254 {
1255 Stop();
1256
1257 #if ORTHANC_ENABLE_SSL == 0
1258 if (enabled)
1259 {
1260 throw OrthancException(ErrorCode_SslDisabled);
1261 }
1262 else
1263 {
1264 ssl_ = false;
1265 }
1266 #else
1267 ssl_ = enabled;
1268 #endif
1269 }
1270
1271
1272 void HttpServer::SetKeepAliveEnabled(bool enabled)
1273 {
1274 Stop();
1275 keepAlive_ = enabled;
1276 LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled");
1277
1278 #if ORTHANC_ENABLE_MONGOOSE == 1
1279 if (enabled)
1280 {
1281 LOG(WARNING) << "You should disable HTTP keep alive, as you are using Mongoose";
1282 }
1283 #endif
1284 }
1285
1286
1287 void HttpServer::SetAuthenticationEnabled(bool enabled)
1288 {
1289 Stop();
1290 authentication_ = enabled;
1291 }
1292
1293 void HttpServer::SetSslCertificate(const char* path)
1294 {
1295 Stop();
1296 certificate_ = path;
1297 }
1298
1299 void HttpServer::SetRemoteAccessAllowed(bool allowed)
1300 {
1301 Stop();
1302 remoteAllowed_ = allowed;
1303 }
1304
1305 void HttpServer::SetHttpCompressionEnabled(bool enabled)
1306 {
1307 Stop();
1308 httpCompression_ = enabled;
1309 LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled");
1310 }
1311
1312 void HttpServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter)
1313 {
1314 Stop();
1315 filter_ = &filter;
1316 }
1317
1318
1319 void HttpServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter)
1320 {
1321 Stop();
1322 exceptionFormatter_ = &formatter;
1323 }
1324
1325
1326 bool HttpServer::IsValidBasicHttpAuthentication(const std::string& basic) const
1327 {
1328 return registeredUsers_.find(basic) != registeredUsers_.end();
1329 }
1330
1331
1332 void HttpServer::Register(IHttpHandler& handler)
1333 {
1334 Stop();
1335 handler_ = &handler;
1336 }
1337
1338
1339 IHttpHandler& HttpServer::GetHandler() const
1340 {
1341 if (handler_ == NULL)
1342 {
1343 throw OrthancException(ErrorCode_InternalError);
1344 }
1345
1346 return *handler_;
1347 }
1348
1349
1350 void HttpServer::SetThreadsCount(unsigned int threads)
1351 {
1352 if (threads <= 0)
1353 {
1354 throw OrthancException(ErrorCode_ParameterOutOfRange);
1355 }
1356
1357 Stop();
1358 threadsCount_ = threads;
1359
1360 LOG(INFO) << "The embedded HTTP server will use " << threads << " threads";
1361 }
1362
1363
1364 void HttpServer::SetTcpNoDelay(bool tcpNoDelay)
1365 {
1366 Stop();
1367 tcpNoDelay_ = tcpNoDelay;
1368 LOG(INFO) << "TCP_NODELAY for the HTTP sockets is set to "
1369 << (tcpNoDelay ? "true" : "false");
1370 }
1371
1372
1373 void HttpServer::SetRequestTimeout(unsigned int seconds)
1374 {
1375 if (seconds <= 0)
1376 {
1377 throw OrthancException(ErrorCode_ParameterOutOfRange,
1378 "Request timeout must be a stricly positive integer");
1379 }
1380
1381 Stop();
1382 requestTimeout_ = seconds;
1383 LOG(INFO) << "Request timeout in the HTTP server is set to " << seconds << " seconds";
1384 }
1385 }