Commit fd1245b9 authored by Sebastien Jodogne's avatar Sebastien Jodogne
Browse files

New upstream version 1.0+dfsg

parent 0d17bc9e
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
repo: d5f45924411123cfd02d035fd50b8e37536eadef
node: ce4e5f0769c81584a8851cff3d1b0fd56283574f
branch: OrthancDicomWeb-0.6
node: 1e052095bb1d3e3ac5d0c3a5253699867d784579
branch: OrthancDicomWeb-1.0
latesttag: null
latesttagdistance: 250
changessincelatesttag: 264
latesttagdistance: 323
changessincelatesttag: 339
+29 −4
Original line number Diff line number Diff line
@@ -21,13 +21,13 @@ cmake_minimum_required(VERSION 2.8)

project(OrthancDicomWeb)

set(ORTHANC_DICOM_WEB_VERSION "0.6")
set(ORTHANC_DICOM_WEB_VERSION "1.0")

if (ORTHANC_DICOM_WEB_VERSION STREQUAL "mainline")
  set(ORTHANC_FRAMEWORK_VERSION "mainline")
  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
else()
  set(ORTHANC_FRAMEWORK_VERSION "1.5.5")
  set(ORTHANC_FRAMEWORK_VERSION "1.5.7")
  set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
endif()

@@ -42,7 +42,11 @@ set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory
# Advanced parameters to fine-tune linking against system libraries
set(USE_SYSTEM_GDCM ON CACHE BOOL "Use the system version of Grassroot DICOM (GDCM)")
set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
set(ORTHANC_SDK_VERSION "1.5.4" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"1.5.4\", or \"framework\")")
set(ORTHANC_SDK_VERSION "1.5.7" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"1.5.4\", \"1.5.7\", or \"framework\")")


set(BUILD_BOOTSTRAP_VUE OFF CACHE BOOL "Compile Bootstrap-Vue from sources")
set(BUILD_BABEL_POLYFILL OFF CACHE BOOL "Retrieve babel-polyfill from npm")



@@ -60,13 +64,15 @@ set(USE_BOOST_ICONV ON)
include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake)
include_directories(${ORTHANC_ROOT})


include(${CMAKE_SOURCE_DIR}/Resources/CMake/GdcmConfiguration.cmake)
include(${CMAKE_SOURCE_DIR}/Resources/CMake/JavaScriptLibraries.cmake)


if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
  if (ORTHANC_SDK_VERSION STREQUAL "1.5.4")
    include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-1.5.4)
  elseif (ORTHANC_SDK_VERSION STREQUAL "1.5.7")
    include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-1.5.7)
  elseif (ORTHANC_SDK_VERSION STREQUAL "framework")
    include_directories(${ORTHANC_ROOT}/Plugins/Include)
  else()
@@ -110,11 +116,30 @@ if (APPLE)
endif()



if (STANDALONE_BUILD)
  add_definitions(-DORTHANC_STANDALONE=1)
  set(ADDITIONAL_RESOURCES
    ORTHANC_EXPLORER  ${CMAKE_SOURCE_DIR}/Plugin/OrthancExplorer.js
    WEB_APPLICATION   ${CMAKE_SOURCE_DIR}/WebApplication/
    )
else()
  add_definitions(-DORTHANC_STANDALONE=0)
endif()

EmbedResources(
  --no-upcase-check
  ${ADDITIONAL_RESOURCES}
  JAVASCRIPT_LIBS   ${JAVASCRIPT_LIBS_DIR}
  )


include_directories(${ORTHANC_ROOT}/Core)  # To access "OrthancException.h"

add_definitions(
  -DHAS_ORTHANC_EXCEPTION=1
  -DORTHANC_ENABLE_LOGGING_PLUGIN=1
  -DDICOMWEB_CLIENT_PATH="${CMAKE_SOURCE_DIR}/WebApplication/"
  )

set(CORE_SOURCES
+17 −0
Original line number Diff line number Diff line
@@ -2,6 +2,23 @@ Pending changes in the mainline
===============================


Version 1.0 (2019-06-26)
========================

=> Recommended SDK version: 1.5.7 <=
=> Minimum SDK version: 1.5.4 <=

* Web user interface to QIDO-RS, WADO-RS and STOW-RS client
* First implementation of WADO-RS "Retrieve Rendered Transaction"
* WADO-RS and STOW-RS client now create Orthanc jobs
* Support "Transfer-Encoding: chunked" to reduce memory consumption in STOW-RS
  (provided the SDK version is above 1.5.7)
* New URI: /dicom-web/servers/.../qido
* New URI: /dicom-web/servers/.../delete
* Handling of the HTTP header "Forwarded" for WADO-RS
* Full refactoring of multipart parsing


Version 0.6 (2019-02-27)
========================

+331 −164
Original line number Diff line number Diff line
@@ -21,15 +21,17 @@

#include "Configuration.h"

#include "DicomWebServers.h"

#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
#include <Core/Toolbox.h>

#include <fstream>
#include <json/reader.h>
#include <boost/regex.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/algorithm/string/predicate.hpp>

#include "DicomWebServers.h"

#include <Plugins/Samples/Common/OrthancPluginCppWrapper.h>
#include <Core/Toolbox.h>

namespace OrthancPlugins
{
@@ -85,220 +87,200 @@ namespace OrthancPlugins
  }


  static const boost::regex MULTIPART_HEADERS_ENDING("(.*?\r\n)\r\n(.*)");
  static const boost::regex MULTIPART_HEADERS_LINE(".*?\r\n");

  static void ParseMultipartHeaders(bool& hasLength /* out */,
                                    size_t& length /* out */,
                                    std::string& contentType /* out */,
                                    const char* startHeaders,
                                    const char* endHeaders)
  {
    hasLength = false;
    contentType = "application/octet-stream";

    // Loop over the HTTP headers of this multipart item
    boost::cregex_token_iterator it(startHeaders, endHeaders, MULTIPART_HEADERS_LINE, 0);
    boost::cregex_token_iterator iteratorEnd;

    for (; it != iteratorEnd; ++it)
  void ParseAssociativeArray(std::map<std::string, std::string>& target,
                             const Json::Value& value)
  {
      const std::string line(*it);
      size_t colon = line.find(':');
      size_t eol = line.find('\r');

      if (colon != std::string::npos &&
          eol != std::string::npos &&
          colon < eol &&
          eol + 2 == line.length())
    if (value.type() != Json::objectValue)
    {
        std::string key = Orthanc::Toolbox::StripSpaces(line.substr(0, colon));
        Orthanc::Toolbox::ToLowerCase(key);
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                      "The JSON object is not a JSON associative array as expected");
    }

        const std::string value = Orthanc::Toolbox::StripSpaces(line.substr(colon + 1, eol - colon - 1));
    Json::Value::Members names = value.getMemberNames();

        if (key == "content-length")
        {
          try
          {
            int tmp = boost::lexical_cast<int>(value);
            if (tmp >= 0)
    for (size_t i = 0; i < names.size(); i++)
    {
              hasLength = true;
              length = tmp;
            }
          }
          catch (boost::bad_lexical_cast&)
      if (value[names[i]].type() != Json::stringValue)
      {
            LogWarning("Unable to parse the Content-Length of a multipart item");
          }
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                        "Value \"" + names[i] + "\" in the associative array "
                                        "is not a string as expected");
      }
        else if (key == "content-type")
      else
      {
          contentType = value;
        }
        target[names[i]] = value[names[i]].asString();
      }
    }
  }
  

  static const char* ParseMultipartItem(std::vector<MultipartItem>& result,
                                        const char* start,
                                        const char* end,
                                        const boost::regex& nextSeparator)
  void ParseAssociativeArray(std::map<std::string, std::string>& target,
                             const Json::Value& value,
                             const std::string& key)
  {
    // Just before "start", it is guaranteed that "--[BOUNDARY]\r\n" is present

    boost::cmatch what;
    if (!boost::regex_match(start, end, what, MULTIPART_HEADERS_ENDING, boost::match_perl))
    if (value.type() != Json::objectValue)
    {
      // Cannot find the HTTP headers of this multipart item
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                      "This is not a JSON object");
    }

    // Some aliases for more clarity
    assert(what[1].first == start);
    const char* startHeaders = what[1].first;
    const char* endHeaders = what[1].second;
    const char* startBody = what[2].first;

    bool hasLength;
    size_t length;
    std::string contentType;
    ParseMultipartHeaders(hasLength, length, contentType, startHeaders, endHeaders);
    if (value.isMember(key))
    {
      ParseAssociativeArray(target, value[key]);
    }
    else
    {
      target.clear();
    }
  }

    boost::cmatch separator;

    if (hasLength)
  bool ParseTag(Orthanc::DicomTag& target,
                const std::string& name)
  {
      if (!boost::regex_match(startBody + length, end, separator, nextSeparator, boost::match_perl) ||
          startBody + length != separator[1].first)
    OrthancPluginDictionaryEntry entry;
    
    if (OrthancPluginLookupDictionary(OrthancPlugins::GetGlobalContext(), &entry, name.c_str()) == OrthancPluginErrorCode_Success)
    {
        // Cannot find the separator after skipping the "Content-Length" bytes
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
      }
      target = Orthanc::DicomTag(entry.group, entry.element);
      return true;
    }
    else
    {
      if (!boost::regex_match(startBody, end, separator, nextSeparator, boost::match_perl))
      {
        // No more occurrence of the boundary separator
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
      return false;
    }
  }

    MultipartItem item;
    item.data_ = startBody;
    item.size_ = separator[1].first - startBody;
    item.contentType_ = contentType;
    result.push_back(item);

    return separator[1].second;  // Return the end of the separator
  void ParseJsonBody(Json::Value& target,
                     const OrthancPluginHttpRequest* request)
  {
    Json::Reader reader;
    if (!reader.parse(reinterpret_cast<const char*>(request->body),
                      reinterpret_cast<const char*>(request->body) + request->bodySize, target))
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                      "A JSON file was expected");
    }
  }


  void ParseMultipartBody(std::vector<MultipartItem>& result,
                          const char* body,
                          const uint64_t bodySize,
                          const std::string& boundary)
  std::string RemoveMultipleSlashes(const std::string& source)
  {
    // Reference:
    // https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
    std::string target;
    target.reserve(source.size());

    result.clear();
    size_t prefix = 0;
  
    // Look for the first boundary separator in the body (note the "?"
    // to request non-greedy search)
    const boost::regex firstSeparator1("--" + boundary + "(--|\r\n).*");
    const boost::regex firstSeparator2(".*?\r\n--" + boundary + "(--|\r\n).*");
    if (boost::starts_with(source, "https://"))
    {
      prefix = 8;
    }
    else if (boost::starts_with(source, "http://"))
    {
      prefix = 7;
    }

    // Look for the next boundary separator in the body (note the "?"
    // to request non-greedy search)
    const boost::regex nextSeparator(".*?(\r\n--" + boundary + ").*");
    for (size_t i = 0; i < prefix; i++)
    {
      target.push_back(source[i]);
    }

    const char* end = body + bodySize;
    bool isLastSlash = false;

    boost::cmatch what;
    if (boost::regex_match(body, end, what, firstSeparator1, boost::match_perl | boost::match_single_line) ||
        boost::regex_match(body, end, what, firstSeparator2, boost::match_perl | boost::match_single_line))
    for (size_t i = prefix; i < source.size(); i++)
    {
      const char* current = what[1].first;

      while (current != NULL &&
             current + 2 < end)
      if (source[i] == '/')
      {
        if (current[0] != '\r' ||
            current[1] != '\n')
        if (!isLastSlash)
        {
          // We reached a separator with a trailing "--", which
          // means that reading the multipart body is done
          break;
          target.push_back('/');
          isLastSlash = true;
        }
      }
      else
      {
          current = ParseMultipartItem(result, current + 2, end, nextSeparator);
        }
        target.push_back(source[i]);
        isLastSlash = false;
      }
    }

    return target;
  }


  void ParseAssociativeArray(std::map<std::string, std::string>& target,
                             const Json::Value& value,
  bool LookupStringValue(std::string& target,
                         const Json::Value& json,
                         const std::string& key)
  {
    if (value.type() != Json::objectValue)
    if (json.type() != Json::objectValue)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                      "This is not a JSON object");
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
    }

    if (!value.isMember(key))
    else if (!json.isMember(key))
    {
      return;
      return false;
    }

    const Json::Value& tmp = value[key];

    if (tmp.type() != Json::objectValue)
    else if (json[key].type() != Json::stringValue)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                      "The field \"" + key + "\" of a JSON object is "
                                      "not a JSON associative array as expected");
      throw Orthanc::OrthancException(
        Orthanc::ErrorCode_BadFileFormat,
        "The field \"" + key + "\" in a JSON object should be a string");
    }
    else
    {
      target = json[key].asString();
      return true;
    }
  }

    Json::Value::Members names = tmp.getMemberNames();

    for (size_t i = 0; i < names.size(); i++)
  bool LookupIntegerValue(int& target,
                          const Json::Value& json,
                          const std::string& key)
  {
      if (tmp[names[i]].type() != Json::stringValue)
    if (json.type() != Json::objectValue)
    {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                        "Some value in the associative array \"" + key + 
                                        "\" is not a string as expected");
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
    }
      else
    else if (!json.isMember(key))
    {
        target[names[i]] = tmp[names[i]].asString();
      return false;
    }
    else if (json[key].type() != Json::intValue &&
             json[key].type() != Json::uintValue)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);      
    }
    else
    {
      target = json[key].asInt();
      return true;
    }
  }


  bool ParseTag(Orthanc::DicomTag& target,
                const std::string& name)
  bool LookupBooleanValue(bool& target,
                          const Json::Value& json,
                          const std::string& key)
  {
    OrthancPluginDictionaryEntry entry;
    
    if (OrthancPluginLookupDictionary(OrthancPlugins::GetGlobalContext(), &entry, name.c_str()) == OrthancPluginErrorCode_Success)
    if (json.type() != Json::objectValue)
    {
      target = Orthanc::DicomTag(entry.group, entry.element);
      return true;
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
    }
    else
    else if (!json.isMember(key))
    {
      return false;
    }
    else if (json[key].type() != Json::booleanValue)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);      
    }
    else
    {
      target = json[key].asBool();
      return true;
    }
  }


@@ -352,7 +334,7 @@ namespace OrthancPlugins
    }


    std::string GetRoot()
    std::string GetDicomWebRoot()
    {
      assert(configuration_.get() != NULL);
      std::string root = configuration_->GetStringValue("Root", "/dicom-web/");
@@ -373,6 +355,40 @@ namespace OrthancPlugins
    }

    
    std::string GetOrthancApiRoot()
    {
      std::string root = OrthancPlugins::Configuration::GetDicomWebRoot();
      std::vector<std::string> tokens;
      Orthanc::Toolbox::TokenizeString(tokens, root, '/');

      int depth = 0;
      for (size_t i = 0; i < tokens.size(); i++)
      {
        if (tokens[i].empty() ||
            tokens[i] == ".")
        {
          // Don't change the depth
        }
        else if (tokens[i] == "..")
        {
          depth--;
        }
        else
        {
          depth++;
        }
      }

      std::string orthancRoot = "./";
      for (int i = 0; i < depth; i++)
      {
        orthancRoot += "../";
      }

      return orthancRoot;
    }


    std::string GetWadoRoot()
    {
      assert(configuration_.get() != NULL);
@@ -395,21 +411,116 @@ namespace OrthancPlugins
    }


    std::string  GetBaseUrl(const OrthancPluginHttpRequest* request)
    static bool IsHttpsProto(const std::string& proto,
                             bool defaultValue)
    {
      if (proto == "http")
      {
        return false;
      }
      else if (proto == "https")
      {
        return true;
      }
      else
      {
        return defaultValue;
      }
    }


    static bool LookupHttpHeader2(std::string& value,
                                  const OrthancPlugins::HttpClient::HttpHeaders& headers,
                                  const std::string& name)
    {
      for (OrthancPlugins::HttpClient::HttpHeaders::const_iterator
             it = headers.begin(); it != headers.end(); ++it)
      {
        if (boost::iequals(it->first, name))
        {
          value = it->second;
          return false;
        }
      }

      return false;
    }


    std::string GetBaseUrl(const OrthancPlugins::HttpClient::HttpHeaders& headers)
    {
      assert(configuration_.get() != NULL);
      std::string host = configuration_->GetStringValue("Host", "");
      bool ssl = configuration_->GetBooleanValue("Ssl", false);
      bool https = configuration_->GetBooleanValue("Ssl", false);

      std::string forwarded;
      if (host.empty() &&
          !LookupHttpHeader(host, request, "host"))
          LookupHttpHeader2(forwarded, headers, "forwarded"))
      {
        // There is a "Forwarded" HTTP header in the query
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
        
        std::vector<std::string> forwarders;
        Orthanc::Toolbox::TokenizeString(forwarders, forwarded, ',');

        // Only consider the first forwarder, if any
        if (!forwarders.empty())
        {
          std::vector<std::string> tokens;
          Orthanc::Toolbox::TokenizeString(tokens, forwarders[0], ';');

          for (size_t j = 0; j < tokens.size(); j++)
          {
            std::vector<std::string> args;
            Orthanc::Toolbox::TokenizeString(args, tokens[j], '=');
            
            if (args.size() == 2)
            {
              std::string key = Orthanc::Toolbox::StripSpaces(args[0]);
              std::string value = Orthanc::Toolbox::StripSpaces(args[1]);

              Orthanc::Toolbox::ToLowerCase(key);
              if (key == "host")
              {
                host = value;
              }
              else if (key == "proto")
              {
                https = IsHttpsProto(value, https);
              }
            }
          }
        }
      }

      if (host.empty() &&
          !LookupHttpHeader2(host, headers, "host"))
      {
        // Should never happen: The "host" header should always be present
        // in HTTP requests. Provide a default value anyway.
        host = "localhost:8042";
      }

      return (ssl ? "https://" : "http://") + host + GetRoot();
      return (https ? "https://" : "http://") + host + GetDicomWebRoot();
    }


    std::string GetBaseUrl(const OrthancPluginHttpRequest* request)
    {
      OrthancPlugins::HttpClient::HttpHeaders headers;

      std::string value;
      if (LookupHttpHeader(value, request, "forwarded"))
      {
        headers["Forwarded"] = value;
      }

      if (LookupHttpHeader(value, request, "host"))
      {
        headers["Host"] = value;
      }

      return GetBaseUrl(headers);
    }


@@ -438,5 +549,61 @@ namespace OrthancPlugins
    {
      return defaultEncoding_;
    }


    static bool IsXmlExpected(const std::string& acceptHeader)
    {
      std::string accept;
      Orthanc::Toolbox::ToLowerCase(accept, acceptHeader);
  
      if (accept == "application/dicom+json" ||
          accept == "application/json" ||
          accept == "*/*")
      {
        return false;
      }
      else if (accept == "application/dicom+xml" ||
               accept == "application/xml" ||
               accept == "text/xml")
      {
        return true;
      }
      else
      {
        OrthancPlugins::LogError("Unsupported return MIME type: " + accept +
                                 ", will return DICOM+JSON");
        return false;
      }
    }


    bool IsXmlExpected(const std::map<std::string, std::string>& headers)
    {
      std::map<std::string, std::string>::const_iterator found = headers.find("accept");

      if (found == headers.end())
      {
        return false;   // By default, return DICOM+JSON
      }
      else
      {
        return IsXmlExpected(found->second);
      }
    }


    bool IsXmlExpected(const OrthancPluginHttpRequest* request)
    {
      std::string accept;

      if (OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
      {
        return IsXmlExpected(accept);
      }
      else
      {
        return false;   // By default, return DICOM+JSON
      }
    }
  }
}
+31 −13
Original line number Diff line number Diff line
@@ -45,13 +45,6 @@ namespace OrthancPlugins
  static const Orthanc::DicomTag DICOM_TAG_REFERENCED_SOP_CLASS_UID(0x0008, 0x1150);
  static const Orthanc::DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155);

  struct MultipartItem
  {
    const char*   data_;
    size_t        size_;
    std::string   contentType_;
  };

  bool LookupHttpHeader(std::string& value,
                        const OrthancPluginHttpRequest* request,
                        const std::string& header);
@@ -60,18 +53,33 @@ namespace OrthancPlugins
                        std::map<std::string, std::string>& attributes,
                        const std::string& header);

  void ParseMultipartBody(std::vector<MultipartItem>& result,
                          const char* body,
                          const uint64_t bodySize,
                          const std::string& boundary);

  void ParseAssociativeArray(std::map<std::string, std::string>& target,
                             const Json::Value& value,
                             const std::string& key);

  void ParseAssociativeArray(std::map<std::string, std::string>& target,
                             const Json::Value& value);

  bool ParseTag(Orthanc::DicomTag& target,
                const std::string& name);

  void ParseJsonBody(Json::Value& target,
                     const OrthancPluginHttpRequest* request);

  std::string RemoveMultipleSlashes(const std::string& source);

  bool LookupStringValue(std::string& target,
                         const Json::Value& json,
                         const std::string& key);

  bool LookupIntegerValue(int& target,
                          const Json::Value& json,
                          const std::string& key);

  bool LookupBooleanValue(bool& target,
                          const Json::Value& json,
                          const std::string& key);

  namespace Configuration
  {
    void Initialize();
@@ -85,10 +93,15 @@ namespace OrthancPlugins
    unsigned int GetUnsignedIntegerValue(const std::string& key,
                                         unsigned int defaultValue);

    std::string GetRoot();
    std::string GetDicomWebRoot();

    std::string GetOrthancApiRoot();

    std::string GetWadoRoot();
      
    std::string GetBaseUrl(const std::map<std::string, std::string>& headers);

    // TODO => REMOVE
    std::string GetBaseUrl(const OrthancPluginHttpRequest* request);

    std::string GetWadoUrl(const std::string& wadoBase,
@@ -97,5 +110,10 @@ namespace OrthancPlugins
                           const std::string& sopInstanceUid);

    Orthanc::Encoding GetDefaultEncoding();

    bool IsXmlExpected(const std::map<std::string, std::string>& headers);

    // TODO => REMOVE
    bool IsXmlExpected(const OrthancPluginHttpRequest* request);
  }
}
Loading