Mercurial > hg > orthanc
view Core/HttpServer/HttpOutput.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 | 4e43e67f8ecf |
children | 6e8822be2f08 |
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/>. **/ #include "../PrecompiledHeaders.h" #include "HttpOutput.h" #include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" #include "../Compression/GzipCompressor.h" #include "../Compression/ZlibCompressor.h" #include <iostream> #include <vector> #include <stdio.h> #include <boost/lexical_cast.hpp> namespace Orthanc { HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream, bool isKeepAlive) : stream_(stream), state_(State_WritingHeader), status_(HttpStatus_200_Ok), hasContentLength_(false), contentPosition_(0), keepAlive_(isKeepAlive) { } HttpOutput::StateMachine::~StateMachine() { if (state_ != State_Done) { //asm volatile ("int3;"); //LOG(ERROR) << "This HTTP answer does not contain any body"; } if (hasContentLength_ && contentPosition_ != contentLength_) { LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body"; } } void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status) { if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } status_ = status; } void HttpOutput::StateMachine::SetContentLength(uint64_t length) { if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } hasContentLength_ = true; contentLength_ = length; } void HttpOutput::StateMachine::SetContentType(const char* contentType) { AddHeader("Content-Type", contentType); } void HttpOutput::StateMachine::SetContentFilename(const char* filename) { // TODO Escape double quotes AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\""); } void HttpOutput::StateMachine::SetCookie(const std::string& cookie, const std::string& value) { if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } // TODO Escape "=" characters AddHeader("Set-Cookie", cookie + "=" + value); } void HttpOutput::StateMachine::AddHeader(const std::string& header, const std::string& value) { if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } headers_.push_back(header + ": " + value + "\r\n"); } void HttpOutput::StateMachine::ClearHeaders() { if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } headers_.clear(); } void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length) { if (state_ == State_Done) { if (length == 0) { return; } else { throw OrthancException(ErrorCode_BadSequenceOfCalls, "Because of keep-alive connections, the entire body must " "be sent at once or Content-Length must be given"); } } if (state_ == State_WritingMultipart) { throw OrthancException(ErrorCode_InternalError); } if (state_ == State_WritingHeader) { // Send the HTTP header before writing the body stream_.OnHttpStatusReceived(status_); std::string s = "HTTP/1.1 " + boost::lexical_cast<std::string>(status_) + " " + std::string(EnumerationToString(status_)) + "\r\n"; if (keepAlive_) { s += "Connection: keep-alive\r\n"; } for (std::list<std::string>::const_iterator it = headers_.begin(); it != headers_.end(); ++it) { s += *it; } if (status_ != HttpStatus_200_Ok) { hasContentLength_ = false; } uint64_t contentLength = (hasContentLength_ ? contentLength_ : length); s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n\r\n"; stream_.Send(true, s.c_str(), s.size()); state_ = State_WritingBody; } if (hasContentLength_ && contentPosition_ + length > contentLength_) { throw OrthancException(ErrorCode_BadSequenceOfCalls, "The body size exceeds what was declared with SetContentSize()"); } if (length > 0) { stream_.Send(false, buffer, length); contentPosition_ += length; } if (!hasContentLength_ || contentPosition_ == contentLength_) { state_ = State_Done; } } void HttpOutput::StateMachine::CloseBody() { switch (state_) { case State_WritingHeader: SetContentLength(0); SendBody(NULL, 0); break; case State_WritingBody: if (!hasContentLength_ || contentPosition_ == contentLength_) { state_ = State_Done; } else { throw OrthancException(ErrorCode_BadSequenceOfCalls, "The body size has not reached what was declared with SetContentSize()"); } break; case State_WritingMultipart: throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot invoke CloseBody() with multipart outputs"); case State_Done: return; // Ignore default: throw OrthancException(ErrorCode_InternalError); } } HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const { #if 0 // TODO Do not compress small files? if (bodySize < 512) { return HttpCompression_None; } #endif // Prefer "gzip" over "deflate" if the choice is offered if (isGzipAllowed_) { return HttpCompression_Gzip; } else if (isDeflateAllowed_) { return HttpCompression_Deflate; } else { return HttpCompression_None; } } void HttpOutput::SendMethodNotAllowed(const std::string& allowed) { stateMachine_.ClearHeaders(); stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed); stateMachine_.AddHeader("Allow", allowed); stateMachine_.SendBody(NULL, 0); } void HttpOutput::SendStatus(HttpStatus status, const char* message, size_t messageSize) { if (status == HttpStatus_301_MovedPermanently || status == HttpStatus_401_Unauthorized || status == HttpStatus_405_MethodNotAllowed) { throw OrthancException(ErrorCode_ParameterOutOfRange, "Please use the dedicated methods to this HTTP status code in HttpOutput"); } stateMachine_.SetHttpStatus(status); stateMachine_.SendBody(message, messageSize); } void HttpOutput::Redirect(const std::string& path) { stateMachine_.ClearHeaders(); stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently); stateMachine_.AddHeader("Location", path); stateMachine_.SendBody(NULL, 0); } void HttpOutput::SendUnauthorized(const std::string& realm) { stateMachine_.ClearHeaders(); stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized); stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\""); stateMachine_.SendBody(NULL, 0); } void HttpOutput::Answer(const void* buffer, size_t length) { if (length == 0) { AnswerEmpty(); return; } HttpCompression compression = GetPreferredCompression(length); if (compression == HttpCompression_None) { stateMachine_.SetContentLength(length); stateMachine_.SendBody(buffer, length); return; } std::string compressed, encoding; switch (compression) { case HttpCompression_Deflate: { encoding = "deflate"; ZlibCompressor compressor; // Do not prefix the buffer with its uncompressed size, to be compatible with "deflate" compressor.SetPrefixWithUncompressedSize(false); compressor.Compress(compressed, buffer, length); break; } case HttpCompression_Gzip: { encoding = "gzip"; GzipCompressor compressor; compressor.Compress(compressed, buffer, length); break; } default: throw OrthancException(ErrorCode_InternalError); } LOG(TRACE) << "Compressing a HTTP answer using " << encoding; // The body is empty, do not use HTTP compression if (compressed.size() == 0) { AnswerEmpty(); } else { stateMachine_.AddHeader("Content-Encoding", encoding); stateMachine_.SetContentLength(compressed.size()); stateMachine_.SendBody(compressed.c_str(), compressed.size()); } stateMachine_.CloseBody(); } void HttpOutput::Answer(const std::string& str) { Answer(str.size() == 0 ? NULL : str.c_str(), str.size()); } void HttpOutput::AnswerEmpty() { stateMachine_.CloseBody(); } void HttpOutput::StateMachine::StartMultipart(const std::string& subType, const std::string& contentType) { if (subType != "mixed" && subType != "related") { throw OrthancException(ErrorCode_ParameterOutOfRange); } if (keepAlive_) { throw OrthancException(ErrorCode_NotImplemented, "Multipart answers are not implemented together with keep-alive connections"); } if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } if (status_ != HttpStatus_200_Ok) { SendBody(NULL, 0); return; } stream_.OnHttpStatusReceived(status_); std::string header = "HTTP/1.1 200 OK\r\n"; // Possibly add the cookies for (std::list<std::string>::const_iterator it = headers_.begin(); it != headers_.end(); ++it) { if (!Toolbox::StartsWith(*it, "Set-Cookie: ")) { throw OrthancException(ErrorCode_BadSequenceOfCalls, "The only headers that can be set in multipart answers " "are Set-Cookie (here: " + *it + " is set)"); } header += *it; } /** * Fix for issue 54 ("Decide what to do wrt. quoting of multipart * answers"). The "type" parameter in the "Content-Type" HTTP * header must be quoted if it contains a forward slash "/". This * is necessary for DICOMweb compatibility with OsiriX, but breaks * compatibility with old releases of the client in the Orthanc * DICOMweb plugin <= 0.3 (releases >= 0.4 work fine). * * Full history is available at the following locations: * - In changeset 2248:69b0f4e8a49b: * # hg history -v -r 2248 * - https://bitbucket.org/sjodogne/orthanc/issues/54/ * - https://groups.google.com/d/msg/orthanc-users/65zhIM5xbKI/TU5Q1_LhAwAJ **/ std::string tmp; if (contentType.find('/') == std::string::npos) { // No forward slash in the content type tmp = contentType; } else { // Quote the content type because of the forward slash tmp = "\"" + contentType + "\""; } multipartBoundary_ = Toolbox::GenerateUuid(); multipartContentType_ = contentType; header += ("Content-Type: multipart/" + subType + "; type=" + tmp + "; boundary=" + multipartBoundary_ + "\r\n\r\n"); stream_.Send(true, header.c_str(), header.size()); state_ = State_WritingMultipart; } void HttpOutput::StateMachine::SendMultipartItem(const void* item, size_t length, const std::map<std::string, std::string>& headers) { if (state_ != State_WritingMultipart) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } std::string header = "--" + multipartBoundary_ + "\r\n"; bool hasContentType = false; bool hasContentLength = false; bool hasMimeVersion = false; for (std::map<std::string, std::string>::const_iterator it = headers.begin(); it != headers.end(); ++it) { header += it->first + ": " + it->second + "\r\n"; std::string tmp; Toolbox::ToLowerCase(tmp, it->first); if (tmp == "content-type") { hasContentType = true; } if (tmp == "content-length") { hasContentLength = true; } if (tmp == "mime-version") { hasMimeVersion = true; } } if (!hasContentType) { header += "Content-Type: " + multipartContentType_ + "\r\n"; } if (!hasContentLength) { header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n"; } if (!hasMimeVersion) { header += "MIME-Version: 1.0\r\n\r\n"; } stream_.Send(false, header.c_str(), header.size()); if (length > 0) { stream_.Send(false, item, length); } stream_.Send(false, "\r\n", 2); } void HttpOutput::StateMachine::CloseMultipart() { if (state_ != State_WritingMultipart) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } // The two lines below might throw an exception, if the client has // closed the connection. Such an error is ignored. try { std::string header = "--" + multipartBoundary_ + "--\r\n"; stream_.Send(false, header.c_str(), header.size()); } catch (OrthancException&) { } state_ = State_Done; } void HttpOutput::Answer(IHttpStreamAnswer& stream) { HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_); switch (compression) { case HttpCompression_None: break; case HttpCompression_Gzip: stateMachine_.AddHeader("Content-Encoding", "gzip"); break; case HttpCompression_Deflate: stateMachine_.AddHeader("Content-Encoding", "deflate"); break; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } stateMachine_.SetContentLength(stream.GetContentLength()); std::string contentType = stream.GetContentType(); if (contentType.empty()) { contentType = MIME_BINARY; } stateMachine_.SetContentType(contentType.c_str()); std::string filename; if (stream.HasContentFilename(filename)) { SetContentFilename(filename.c_str()); } while (stream.ReadNextChunk()) { stateMachine_.SendBody(stream.GetChunkContent(), stream.GetChunkSize()); } stateMachine_.CloseBody(); } }