diff OrthancFramework/Sources/HttpServer/HttpOutput.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/HttpOutput.cpp@94f4a18a79cc
children bf7b9edf6b81
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,772 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 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 "../ChunkedBuffer.h"
+#include "../Compression/GzipCompressor.h"
+#include "../Compression/ZlibCompressor.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <iostream>
+#include <vector>
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
+#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
+#  endif
+#endif
+
+
+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";
+      }
+      else
+      {
+        s += "Connection: close\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::CheckHeadersCompatibilityWithMultipart() const
+  {
+    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)");
+      }
+    }
+  }
+
+
+  static void PrepareMultipartMainHeader(std::string& boundary,
+                                         std::string& contentTypeHeader,
+                                         const std::string& subType,
+                                         const std::string& contentType)
+  {
+    if (subType != "mixed" &&
+        subType != "related")
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    /**
+     * 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 + "\"";
+    }
+
+    boundary = Toolbox::GenerateUuid() + "-" + Toolbox::GenerateUuid();
+
+    /**
+     * Fix for issue #165: "Encapsulation boundaries must not appear
+     * within the encapsulations, and must be no longer than 70
+     * characters, not counting the two leading hyphens."
+     * https://tools.ietf.org/html/rfc1521
+     * https://bitbucket.org/sjodogne/orthanc/issues/165/
+     **/
+    if (boundary.size() != 36 + 1 + 36)  // one UUID contains 36 characters
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    boundary = boundary.substr(0, 70);
+    
+    contentTypeHeader = ("multipart/" + subType + "; type=" + tmp + "; boundary=" + boundary);
+  }
+
+
+  void HttpOutput::StateMachine::StartMultipart(const std::string& subType,
+                                                const std::string& contentType)
+  {
+    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";
+
+    if (keepAlive_)
+    {
+#if ORTHANC_ENABLE_MONGOOSE == 1
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "Multipart answers are not implemented together "
+                             "with keep-alive connections if using Mongoose");
+      
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
+      // Turn off Keep-Alive for multipart answers
+      // https://github.com/civetweb/civetweb/issues/727
+      stream_.DisableKeepAlive();
+      header += "Connection: close\r\n";
+#  else
+      // The function "mg_disable_keep_alive()" is not available,
+      // let's continue with Keep-Alive. Performance of WADO-RS will
+      // decrease.
+      header += "Connection: keep-alive\r\n";
+#  endif   
+
+#else
+#  error Please support your embedded Web server here
+#endif
+    }
+    else
+    {
+      header += "Connection: close\r\n";
+    }
+
+    // Possibly add the cookies
+    CheckHeadersCompatibilityWithMultipart();
+
+    for (std::list<std::string>::const_iterator
+           it = headers_.begin(); it != headers_.end(); ++it)
+    {
+      header += *it;
+    }
+
+    std::string contentTypeHeader;
+    PrepareMultipartMainHeader(multipartBoundary_, contentTypeHeader, subType, contentType);
+    multipartContentType_ = contentType;
+    header += ("Content-Type: " + contentTypeHeader + "\r\n\r\n");
+
+    stream_.Send(true, header.c_str(), header.size());
+    state_ = State_WritingMultipart;
+  }
+
+
+  static void PrepareMultipartItemHeader(std::string& target,
+                                         size_t length,
+                                         const std::map<std::string, std::string>& headers,
+                                         const std::string& boundary,
+                                         const std::string& contentType)
+  {
+    target = "--" + boundary + "\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)
+    {
+      target += 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)
+    {
+      target += "Content-Type: " + contentType + "\r\n";
+    }
+
+    if (!hasContentLength)
+    {
+      target += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n";
+    }
+
+    if (!hasMimeVersion)
+    {
+      target += "MIME-Version: 1.0\r\n\r\n";
+    }
+  }
+
+
+  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;
+    PrepareMultipartItemHeader(header, length, headers, multipartBoundary_, multipartContentType_);
+    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;
+  }
+
+
+  static void AnswerStreamAsBuffer(HttpOutput& output,
+                                   IHttpStreamAnswer& stream)
+  {
+    ChunkedBuffer buffer;
+
+    while (stream.ReadNextChunk())
+    {
+      if (stream.GetChunkSize() > 0)
+      {
+        buffer.AddChunk(stream.GetChunkContent(), stream.GetChunkSize());
+      }
+    }
+
+    std::string s;
+    buffer.Flatten(s);
+
+    output.SetContentType(stream.GetContentType());
+    
+    std::string filename;
+    if (stream.HasContentFilename(filename))
+    {
+      output.SetContentFilename(filename.c_str());
+    }
+
+    output.Answer(s);
+  }
+
+
+  void HttpOutput::Answer(IHttpStreamAnswer& stream)
+  {
+    HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_);
+
+    switch (compression)
+    {
+      case HttpCompression_None:
+      {
+        if (isGzipAllowed_ || isDeflateAllowed_)
+        {
+          // New in Orthanc 1.5.7: Compress streams without built-in
+          // compression, if requested by the "Accept-Encoding" HTTP
+          // header
+          AnswerStreamAsBuffer(*this, stream);
+          return;
+        }
+        
+        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();
+  }
+
+
+  void HttpOutput::AnswerMultipartWithoutChunkedTransfer(
+    const std::string& subType,
+    const std::string& contentType,
+    const std::vector<const void*>& parts,
+    const std::vector<size_t>& sizes,
+    const std::vector<const std::map<std::string, std::string>*>& headers)
+  {
+    if (parts.size() != sizes.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    stateMachine_.CheckHeadersCompatibilityWithMultipart();
+
+    std::string boundary, contentTypeHeader;
+    PrepareMultipartMainHeader(boundary, contentTypeHeader, subType, contentType);
+    SetContentType(contentTypeHeader);
+
+    std::map<std::string, std::string> empty;
+
+    ChunkedBuffer chunked;
+    for (size_t i = 0; i < parts.size(); i++)
+    {
+      std::string partHeader;
+      PrepareMultipartItemHeader(partHeader, sizes[i], headers[i] == NULL ? empty : *headers[i], 
+                                 boundary, contentType);
+
+      chunked.AddChunk(partHeader);
+      chunked.AddChunk(parts[i], sizes[i]);
+      chunked.AddChunk("\r\n");    
+    }
+
+    chunked.AddChunk("--" + boundary + "--\r\n");
+
+    std::string body;
+    chunked.Flatten(body);
+    Answer(body);
+  }
+}