Mercurial > hg > orthanc
view Core/HttpServer/MongooseServer.cpp @ 3103:81b58b549845
back to using 'var' instead of 'let' since let is not supported by many old browsers. All variables declaration have been moved to the top of the function to better show that their scope is the function
author | Alain Mazy <alain@mazy.be> |
---|---|
date | Thu, 10 Jan 2019 10:51:36 +0100 |
parents | 3db9697a0a58 |
children | f948deef53d9 |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2019 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * In addition, as a special exception, the copyright holders of this * program give permission to link the code of its release with the * OpenSSL project's "OpenSSL" library (or with modified versions of it * that use the same license as the "OpenSSL" library), and distribute * the linked executables. You must obey the GNU General Public License * in all respects for all of the code used other than "OpenSSL". If you * modify file(s) with this exception, you may extend this exception to * your version of the file(s), but you are not obligated to do so. If * you do not wish to do so, delete this exception statement from your * version. If you delete this exception statement from all source files * in the program, then also delete it here. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ // http://en.highscore.de/cpp/boost/stringhandling.html #include "../PrecompiledHeaders.h" #include "MongooseServer.h" #include "../Logging.h" #include "../ChunkedBuffer.h" #include "../OrthancException.h" #include "HttpToolbox.h" #if ORTHANC_ENABLE_MONGOOSE == 1 # include <mongoose.h> #elif ORTHANC_ENABLE_CIVETWEB == 1 # include <civetweb.h> # define MONGOOSE_USE_CALLBACKS 1 #else # error "Either Mongoose or Civetweb must be enabled to compile this file" #endif #include <algorithm> #include <string.h> #include <boost/lexical_cast.hpp> #include <boost/algorithm/string.hpp> #include <iostream> #include <string.h> #include <stdio.h> #include <boost/thread.hpp> #if !defined(ORTHANC_ENABLE_SSL) # error The macro ORTHANC_ENABLE_SSL must be defined #endif #if ORTHANC_ENABLE_SSL == 1 #include <openssl/opensslv.h> #endif #define ORTHANC_REALM "Orthanc Secure Area" namespace Orthanc { static const char multipart[] = "multipart/form-data; boundary="; static unsigned int multipartLength = sizeof(multipart) / sizeof(char) - 1; namespace { // Anonymous namespace to avoid clashes between compilation modules class MongooseOutputStream : public IHttpOutputStream { private: struct mg_connection* connection_; public: MongooseOutputStream(struct mg_connection* connection) : connection_(connection) { } virtual void Send(bool isHeader, const void* buffer, size_t length) { if (length > 0) { int status = mg_write(connection_, buffer, length); if (status != static_cast<int>(length)) { // status == 0 when the connection has been closed, -1 on error throw OrthancException(ErrorCode_NetworkProtocol); } } } virtual void OnHttpStatusReceived(HttpStatus status) { // Ignore this } }; enum PostDataStatus { PostDataStatus_Success, PostDataStatus_NoLength, PostDataStatus_Pending, PostDataStatus_Failure }; } // TODO Move this to external file class ChunkedFile : public ChunkedBuffer { private: std::string filename_; public: ChunkedFile(const std::string& filename) : filename_(filename) { } const std::string& GetFilename() const { return filename_; } }; class ChunkStore { private: typedef std::list<ChunkedFile*> Content; Content content_; unsigned int numPlaces_; boost::mutex mutex_; std::set<std::string> discardedFiles_; void Clear() { for (Content::iterator it = content_.begin(); it != content_.end(); ++it) { delete *it; } } Content::iterator Find(const std::string& filename) { for (Content::iterator it = content_.begin(); it != content_.end(); ++it) { if ((*it)->GetFilename() == filename) { return it; } } return content_.end(); } void Remove(const std::string& filename) { Content::iterator it = Find(filename); if (it != content_.end()) { delete *it; content_.erase(it); } } public: ChunkStore() { numPlaces_ = 10; } ~ChunkStore() { Clear(); } PostDataStatus Store(std::string& completed, const char* chunkData, size_t chunkSize, const std::string& filename, size_t filesize) { boost::mutex::scoped_lock lock(mutex_); std::set<std::string>::iterator wasDiscarded = discardedFiles_.find(filename); if (wasDiscarded != discardedFiles_.end()) { discardedFiles_.erase(wasDiscarded); return PostDataStatus_Failure; } ChunkedFile* f; Content::iterator it = Find(filename); if (it == content_.end()) { f = new ChunkedFile(filename); // Make some room if (content_.size() >= numPlaces_) { discardedFiles_.insert(content_.front()->GetFilename()); delete content_.front(); content_.pop_front(); } content_.push_back(f); } else { f = *it; } f->AddChunk(chunkData, chunkSize); if (f->GetNumBytes() > filesize) { Remove(filename); } else if (f->GetNumBytes() == filesize) { f->Flatten(completed); Remove(filename); return PostDataStatus_Success; } return PostDataStatus_Pending; } /*void Print() { boost::mutex::scoped_lock lock(mutex_); printf("ChunkStore status:\n"); for (Content::const_iterator i = content_.begin(); i != content_.end(); i++) { printf(" [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes()); } printf("-----\n"); }*/ }; struct MongooseServer::PImpl { struct mg_context *context_; ChunkStore chunkStore_; }; ChunkStore& MongooseServer::GetChunkStore() { return pimpl_->chunkStore_; } static PostDataStatus ReadBody(std::string& postData, struct mg_connection *connection, const IHttpHandler::Arguments& headers) { IHttpHandler::Arguments::const_iterator cs = headers.find("content-length"); if (cs == headers.end()) { return PostDataStatus_NoLength; } int length; try { length = boost::lexical_cast<int>(cs->second); } catch (boost::bad_lexical_cast&) { return PostDataStatus_NoLength; } if (length < 0) { length = 0; } postData.resize(length); size_t pos = 0; while (length > 0) { int r = mg_read(connection, &postData[pos], length); if (r <= 0) { return PostDataStatus_Failure; } assert(r <= length); length -= r; pos += r; } return PostDataStatus_Success; } static PostDataStatus ParseMultipartPost(std::string &completedFile, struct mg_connection *connection, const IHttpHandler::Arguments& headers, const std::string& contentType, ChunkStore& chunkStore) { std::string boundary = "--" + contentType.substr(multipartLength); std::string postData; PostDataStatus status = ReadBody(postData, connection, headers); if (status != PostDataStatus_Success) { return status; } /*for (IHttpHandler::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++) { std::cout << "Header [" << i->first << "] = " << i->second << "\n"; } printf("CHUNK\n");*/ typedef IHttpHandler::Arguments::const_iterator ArgumentIterator; ArgumentIterator requestedWith = headers.find("x-requested-with"); ArgumentIterator fileName = headers.find("x-file-name"); ArgumentIterator fileSizeStr = headers.find("x-file-size"); if (requestedWith != headers.end() && requestedWith->second != "XMLHttpRequest") { return PostDataStatus_Failure; } size_t fileSize = 0; if (fileSizeStr != headers.end()) { try { fileSize = boost::lexical_cast<size_t>(fileSizeStr->second); } catch (boost::bad_lexical_cast&) { return PostDataStatus_Failure; } } typedef boost::find_iterator<std::string::iterator> FindIterator; typedef boost::iterator_range<char*> Range; //chunkStore.Print(); try { FindIterator last; for (FindIterator it = make_find_iterator(postData, boost::first_finder(boundary)); it!=FindIterator(); ++it) { if (last != FindIterator()) { Range part(&last->back(), &it->front()); Range content = boost::find_first(part, "\r\n\r\n"); if (/*content != Range()*/!content.empty()) { Range c(&content.back() + 1, &it->front() - 2); size_t chunkSize = c.size(); if (chunkSize > 0) { const char* chunkData = &c.front(); if (fileName == headers.end()) { // This file is stored in a single chunk completedFile.resize(chunkSize); if (chunkSize > 0) { memcpy(&completedFile[0], chunkData, chunkSize); } return PostDataStatus_Success; } else { return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize); } } } } last = it; } } catch (std::length_error&) { return PostDataStatus_Failure; } return PostDataStatus_Pending; } static bool IsAccessGranted(const MongooseServer& that, const IHttpHandler::Arguments& headers) { bool granted = false; IHttpHandler::Arguments::const_iterator auth = headers.find("authorization"); if (auth != headers.end()) { std::string s = auth->second; if (s.size() > 6 && s.substr(0, 6) == "Basic ") { std::string b64 = s.substr(6); granted = that.IsValidBasicHttpAuthentication(b64); } } return granted; } static std::string GetAuthenticatedUsername(const IHttpHandler::Arguments& headers) { IHttpHandler::Arguments::const_iterator auth = headers.find("authorization"); if (auth == headers.end()) { return ""; } std::string s = auth->second; if (s.size() <= 6 || s.substr(0, 6) != "Basic ") { return ""; } std::string b64 = s.substr(6); std::string decoded; Toolbox::DecodeBase64(decoded, b64); size_t semicolons = decoded.find(':'); if (semicolons == std::string::npos) { // Bad-formatted request return ""; } else { return decoded.substr(0, semicolons); } } static bool ExtractMethod(HttpMethod& method, const struct mg_request_info *request, const IHttpHandler::Arguments& headers, const IHttpHandler::GetArguments& argumentsGET) { std::string overriden; // Check whether some PUT/DELETE faking is done // 1. Faking with Google's approach IHttpHandler::Arguments::const_iterator methodOverride = headers.find("x-http-method-override"); if (methodOverride != headers.end()) { overriden = methodOverride->second; } else if (!strcmp(request->request_method, "GET")) { // 2. Faking with Ruby on Rail's approach // GET /my/resource?_method=delete <=> DELETE /my/resource for (size_t i = 0; i < argumentsGET.size(); i++) { if (argumentsGET[i].first == "_method") { overriden = argumentsGET[i].second; break; } } } if (overriden.size() > 0) { // A faking has been done within this request Toolbox::ToUpperCase(overriden); LOG(INFO) << "HTTP method faking has been detected for " << overriden; if (overriden == "PUT") { method = HttpMethod_Put; return true; } else if (overriden == "DELETE") { method = HttpMethod_Delete; return true; } else { return false; } } // No PUT/DELETE faking was present if (!strcmp(request->request_method, "GET")) { method = HttpMethod_Get; } else if (!strcmp(request->request_method, "POST")) { method = HttpMethod_Post; } else if (!strcmp(request->request_method, "DELETE")) { method = HttpMethod_Delete; } else if (!strcmp(request->request_method, "PUT")) { method = HttpMethod_Put; } else { return false; } return true; } static void ConfigureHttpCompression(HttpOutput& output, const IHttpHandler::Arguments& headers) { // Look if the client wishes HTTP compression // https://en.wikipedia.org/wiki/HTTP_compression IHttpHandler::Arguments::const_iterator it = headers.find("accept-encoding"); if (it != headers.end()) { std::vector<std::string> encodings; Toolbox::TokenizeString(encodings, it->second, ','); for (size_t i = 0; i < encodings.size(); i++) { std::string s = Toolbox::StripSpaces(encodings[i]); if (s == "deflate") { output.SetDeflateAllowed(true); } else if (s == "gzip") { output.SetGzipAllowed(true); } } } } static void InternalCallback(HttpOutput& output /* out */, HttpMethod& method /* out */, MongooseServer& server, struct mg_connection *connection, const struct mg_request_info *request) { bool localhost; #if ORTHANC_ENABLE_MONGOOSE == 1 static const long LOCALHOST = (127ll << 24) + 1ll; localhost = (request->remote_ip == LOCALHOST); #elif ORTHANC_ENABLE_CIVETWEB == 1 // The "remote_ip" field of "struct mg_request_info" is tagged as // deprecated in Civetweb, using "remote_addr" instead. localhost = (std::string(request->remote_addr) == "127.0.0.1"); #else # error #endif // Check remote calls if (!server.IsRemoteAccessAllowed() && !localhost) { output.SendUnauthorized(server.GetRealm()); return; } // Extract the HTTP headers IHttpHandler::Arguments headers; for (int i = 0; i < request->num_headers; i++) { std::string name = request->http_headers[i].name; std::string value = request->http_headers[i].value; std::transform(name.begin(), name.end(), name.begin(), ::tolower); headers.insert(std::make_pair(name, value)); VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]"; } if (server.IsHttpCompressionEnabled()) { ConfigureHttpCompression(output, headers); } // Extract the GET arguments IHttpHandler::GetArguments argumentsGET; if (!strcmp(request->request_method, "GET")) { HttpToolbox::ParseGetArguments(argumentsGET, request->query_string); } // Compute the HTTP method, taking method faking into consideration method = HttpMethod_Get; if (!ExtractMethod(method, request, headers, argumentsGET)) { output.SendStatus(HttpStatus_400_BadRequest); return; } // Authenticate this connection if (server.IsAuthenticationEnabled() && !IsAccessGranted(server, headers)) { output.SendUnauthorized(server.GetRealm()); return; } #if ORTHANC_ENABLE_MONGOOSE == 1 // Apply the filter, if it is installed char remoteIp[24]; sprintf(remoteIp, "%d.%d.%d.%d", reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]); const char* requestUri = request->uri; #elif ORTHANC_ENABLE_CIVETWEB == 1 const char* remoteIp = request->remote_addr; const char* requestUri = request->local_uri; #else # error #endif if (requestUri == NULL) { requestUri = ""; } std::string username = GetAuthenticatedUsername(headers); IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); if (filter != NULL) { if (!filter->IsAllowed(method, requestUri, remoteIp, username.c_str(), headers, argumentsGET)) { //output.SendUnauthorized(server.GetRealm()); output.SendStatus(HttpStatus_403_Forbidden); return; } } // Extract the body of the request for PUT and POST // TODO Avoid unneccessary memcopy of the body std::string body; if (method == HttpMethod_Post || method == HttpMethod_Put) { PostDataStatus status; IHttpHandler::Arguments::const_iterator ct = headers.find("content-type"); if (ct == headers.end()) { // No content-type specified. Assume no multi-part content occurs at this point. status = ReadBody(body, connection, headers); } else { std::string contentType = ct->second; if (contentType.size() >= multipartLength && !memcmp(contentType.c_str(), multipart, multipartLength)) { status = ParseMultipartPost(body, connection, headers, contentType, server.GetChunkStore()); } else { status = ReadBody(body, connection, headers); } } switch (status) { case PostDataStatus_NoLength: output.SendStatus(HttpStatus_411_LengthRequired); return; case PostDataStatus_Failure: output.SendStatus(HttpStatus_400_BadRequest); return; case PostDataStatus_Pending: output.AnswerEmpty(); return; default: break; } } // Decompose the URI into its components UriComponents uri; try { Toolbox::SplitUriComponents(uri, requestUri); } catch (OrthancException&) { output.SendStatus(HttpStatus_400_BadRequest); return; } LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); bool found = false; if (server.HasHandler()) { found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers, argumentsGET, body.c_str(), body.size()); } if (!found) { throw OrthancException(ErrorCode_UnknownResource); } } static void ProtectedCallback(struct mg_connection *connection, const struct mg_request_info *request) { try { #if ORTHANC_ENABLE_MONGOOSE == 1 void *that = request->user_data; const char* requestUri = request->uri; #elif ORTHANC_ENABLE_CIVETWEB == 1 // https://github.com/civetweb/civetweb/issues/409 void *that = mg_get_user_data(mg_get_context(connection)); const char* requestUri = request->local_uri; #else # error #endif if (requestUri == NULL) { requestUri = ""; } MongooseServer* server = reinterpret_cast<MongooseServer*>(that); if (server == NULL) { MongooseOutputStream stream(connection); HttpOutput output(stream, false /* assume no keep-alive */); output.SendStatus(HttpStatus_500_InternalServerError); return; } MongooseOutputStream stream(connection); HttpOutput output(stream, server->IsKeepAliveEnabled()); HttpMethod method = HttpMethod_Get; try { try { InternalCallback(output, method, *server, connection, request); } catch (OrthancException&) { throw; // Pass the exception to the main handler below } // Now convert native exceptions as OrthancException catch (boost::bad_lexical_cast&) { throw OrthancException(ErrorCode_BadParameterType, "Syntax error in some user-supplied data"); } catch (std::runtime_error&) { // Presumably an error while parsing the JSON body throw OrthancException(ErrorCode_BadRequest); } catch (std::bad_alloc&) { throw OrthancException(ErrorCode_NotEnoughMemory, "The server hosting Orthanc is running out of memory"); } catch (...) { throw OrthancException(ErrorCode_InternalError, "An unhandled exception was generated inside the HTTP server"); } } catch (OrthancException& e) { assert(server != NULL); // Using this candidate handler results in an exception try { if (server->GetExceptionFormatter() == NULL) { LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); output.SendStatus(e.GetHttpStatus()); } else { server->GetExceptionFormatter()->Format(output, e, method, requestUri); } } catch (OrthancException&) { // An exception here reflects the fact that the status code // was already set by the HTTP handler. } } } catch (...) { // We should never arrive at this point, where it is even impossible to send an answer LOG(ERROR) << "Catastrophic error inside the HTTP server, giving up"; } } #if MONGOOSE_USE_CALLBACKS == 0 static void* Callback(enum mg_event event, struct mg_connection *connection, const struct mg_request_info *request) { if (event == MG_NEW_REQUEST) { ProtectedCallback(connection, request); // Mark as processed return (void*) ""; } else { return NULL; } } #elif MONGOOSE_USE_CALLBACKS == 1 static int Callback(struct mg_connection *connection) { const struct mg_request_info *request = mg_get_request_info(connection); ProtectedCallback(connection, request); return 1; // Do not let Mongoose handle the request by itself } #else # error Please set MONGOOSE_USE_CALLBACKS #endif bool MongooseServer::IsRunning() const { return (pimpl_->context_ != NULL); } MongooseServer::MongooseServer() : pimpl_(new PImpl) { pimpl_->context_ = NULL; handler_ = NULL; remoteAllowed_ = false; authentication_ = false; ssl_ = false; port_ = 8000; filter_ = NULL; keepAlive_ = false; httpCompression_ = true; exceptionFormatter_ = NULL; realm_ = ORTHANC_REALM; threadsCount_ = 50; // Default value in mongoose #if ORTHANC_ENABLE_SSL == 1 // Check for the Heartbleed exploit // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug if (OPENSSL_VERSION_NUMBER < 0x1000107fL /* openssl-1.0.1g */ && OPENSSL_VERSION_NUMBER >= 0x1000100fL /* openssl-1.0.1 */) { LOG(WARNING) << "This version of OpenSSL is vulnerable to the Heartbleed exploit"; } #endif } MongooseServer::~MongooseServer() { Stop(); } void MongooseServer::SetPortNumber(uint16_t port) { Stop(); port_ = port; } void MongooseServer::Start() { #if ORTHANC_ENABLE_MONGOOSE == 1 LOG(INFO) << "Starting embedded Web server using Mongoose"; #elif ORTHANC_ENABLE_CIVETWEB == 1 LOG(INFO) << "Starting embedded Web server using Civetweb"; #else # error #endif if (!IsRunning()) { std::string port = boost::lexical_cast<std::string>(port_); std::string numThreads = boost::lexical_cast<std::string>(threadsCount_); if (ssl_) { port += "s"; } const char *options[] = { // Set the TCP port for the HTTP server "listening_ports", port.c_str(), // Optimization reported by Chris Hafey // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ "enable_keep_alive", (keepAlive_ ? "yes" : "no"), #if ORTHANC_ENABLE_CIVETWEB == 1 // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"), #endif // Set the number of threads "num_threads", numThreads.c_str(), // Set the SSL certificate, if any. This must be the last option. ssl_ ? "ssl_certificate" : NULL, certificate_.c_str(), NULL }; #if MONGOOSE_USE_CALLBACKS == 0 pimpl_->context_ = mg_start(&Callback, this, options); #elif MONGOOSE_USE_CALLBACKS == 1 struct mg_callbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.begin_request = Callback; pimpl_->context_ = mg_start(&callbacks, this, options); #else # error Please set MONGOOSE_USE_CALLBACKS #endif if (!pimpl_->context_) { throw OrthancException(ErrorCode_HttpPortInUse); } LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber() << " (HTTPS encryption is " << (IsSslEnabled() ? "enabled" : "disabled") << ", remote access is " << (IsRemoteAccessAllowed() ? "" : "not ") << "allowed)"; } } void MongooseServer::Stop() { if (IsRunning()) { mg_stop(pimpl_->context_); pimpl_->context_ = NULL; } } void MongooseServer::ClearUsers() { Stop(); registeredUsers_.clear(); } void MongooseServer::RegisterUser(const char* username, const char* password) { Stop(); std::string tag = std::string(username) + ":" + std::string(password); std::string encoded; Toolbox::EncodeBase64(encoded, tag); registeredUsers_.insert(encoded); } void MongooseServer::SetSslEnabled(bool enabled) { Stop(); #if ORTHANC_ENABLE_SSL == 0 if (enabled) { throw OrthancException(ErrorCode_SslDisabled); } else { ssl_ = false; } #else ssl_ = enabled; #endif } void MongooseServer::SetKeepAliveEnabled(bool enabled) { Stop(); keepAlive_ = enabled; LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); } void MongooseServer::SetAuthenticationEnabled(bool enabled) { Stop(); authentication_ = enabled; } void MongooseServer::SetSslCertificate(const char* path) { Stop(); certificate_ = path; } void MongooseServer::SetRemoteAccessAllowed(bool allowed) { Stop(); remoteAllowed_ = allowed; } void MongooseServer::SetHttpCompressionEnabled(bool enabled) { Stop(); httpCompression_ = enabled; LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled"); } void MongooseServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter) { Stop(); filter_ = &filter; } void MongooseServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter) { Stop(); exceptionFormatter_ = &formatter; } bool MongooseServer::IsValidBasicHttpAuthentication(const std::string& basic) const { return registeredUsers_.find(basic) != registeredUsers_.end(); } void MongooseServer::Register(IHttpHandler& handler) { Stop(); handler_ = &handler; } IHttpHandler& MongooseServer::GetHandler() const { if (handler_ == NULL) { throw OrthancException(ErrorCode_InternalError); } return *handler_; } void MongooseServer::SetThreadsCount(unsigned int threads) { if (threads <= 0) { throw OrthancException(ErrorCode_ParameterOutOfRange); } Stop(); threadsCount_ = threads; } }