Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
InternetHelper.cpp 19.87 KiB
#include "MantidKernel/InternetHelper.h"
#include "MantidKernel/ConfigService.h"
#include "MantidKernel/Exception.h"
#include "MantidKernel/Logger.h"
#include "MantidTypes/DateAndTime.h"

// Poco
#include <Poco/Net/AcceptCertificateHandler.h>
#include <Poco/Net/HTMLForm.h>
#include <Poco/Net/HTTPRequest.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/Net/HTTPSClientSession.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/PrivateKeyPassphraseHandler.h>
#include <Poco/Net/SSLManager.h>
#include <Poco/StreamCopier.h>
#include <Poco/TemporaryFile.h>
#include <Poco/URI.h>

#include <Poco/Exception.h>
#include <Poco/File.h>
#include <Poco/FileStream.h>
#include <Poco/Net/Context.h>
#include <Poco/Net/HTTPClientSession.h>
#include <Poco/Net/HTTPMessage.h>
#include <Poco/Net/InvalidCertificateHandler.h>
#include <Poco/SharedPtr.h>
#include <Poco/Timespan.h>
#include <Poco/Types.h>

#if defined(_WIN32) || defined(_WIN64)
#include <Winhttp.h>
#endif

#include <boost/lexical_cast.hpp>

// std
#include <fstream>
#include <mutex>
#include <utility>

using Mantid::Types::DateAndTime;

namespace Mantid {
namespace Kernel {

using namespace Poco::Net;
using std::map;
using std::string;

namespace {
// anonymous namespace for some utility functions
/// static Logger object
Logger g_log("InternetHelper");

/// Flag to protect SSL initialization
std::once_flag SSL_INIT_FLAG;

/**
 * Perform initialization of SSL context. Implementation
 * designed to be called by std::call_once
 */
void doSSLInit() {
  // initialize ssl
  Poco::SharedPtr<InvalidCertificateHandler> certificateHandler =
      new AcceptCertificateHandler(true);
  // Currently do not use any means of authentication. This should be updated
  // IDS has signed certificate.
  const Context::Ptr context =
      new Context(Context::CLIENT_USE, "", "", "", Context::VERIFY_NONE);
  // Create a singleton for holding the default context.
  // e.g. any future requests to publish are made to this certificate and
  // context.
  SSLManager::instance().initializeClient(nullptr, certificateHandler, context);
}

/**
 * Entry function to initialize SSL context for the process. It ensures the
 * initialization only happens once per process.
 */
void initializeSSL() { std::call_once(SSL_INIT_FLAG, doSSLInit); }
} // namespace

//----------------------------------------------------------------------------------------------
/** Constructor
 */
InternetHelper::InternetHelper()
    : m_proxyInfo(), m_isProxySet(false), m_timeout(30), m_isTimeoutSet(false),
      m_contentLength(0), m_method(HTTPRequest::HTTP_GET),
      m_contentType("application/json"), m_body(), m_headers(),
      m_request(nullptr), m_response(nullptr) {}

//----------------------------------------------------------------------------------------------
/** Constructor
 */
InternetHelper::InternetHelper(const Kernel::ProxyInfo &proxy)
    : m_proxyInfo(proxy), m_isProxySet(true), m_timeout(30),
      m_isTimeoutSet(false), m_contentLength(0),
      m_method(HTTPRequest::HTTP_GET), m_contentType("application/json"),
      m_body(), m_headers(), m_request(nullptr), m_response(nullptr) {}

//----------------------------------------------------------------------------------------------
/** Destructor
 */
InternetHelper::~InternetHelper() {
  if (m_request != nullptr) {
    delete m_request;
  }
  if (m_response != nullptr) {
    delete m_response;
  }
}

void InternetHelper::setupProxyOnSession(HTTPClientSession &session,
                                         const std::string &proxyUrl) {
  auto proxy = this->getProxy(proxyUrl);
  if (!proxy.emptyProxy()) {
    session.setProxyHost(proxy.host());
    session.setProxyPort(static_cast<Poco::UInt16>(proxy.port()));
  }
}

void InternetHelper::createRequest(Poco::URI &uri) {
  if (m_request != nullptr) {
    delete m_request;
  }
  if (m_response != nullptr) {
    delete m_response;
  }

  m_request =
      new HTTPRequest(m_method, uri.getPathAndQuery(), HTTPMessage::HTTP_1_1);

  m_response = new HTTPResponse();
  if (!m_contentType.empty()) {
    m_request->setContentType(m_contentType);
  }

  m_request->set("User-Agent", "MANTID");
  if (m_method == "POST") {
    // HTTP states that the 'Content-Length' header should not be included
    // if the 'Transfer-Encoding' header is set. UNKNOWN_CONTENT_LENGTH
    // indicates to Poco to remove the header field
    m_request->setContentLength(HTTPMessage::UNKNOWN_CONTENT_LENGTH);
    m_request->setChunkedTransferEncoding(true);
  } else if (m_contentLength > 0) {
    m_request->setContentLength(m_contentLength);
  }

  for (auto &header : m_headers) {
    m_request->set(header.first, header.second);
  }
}

int InternetHelper::sendRequestAndProcess(HTTPClientSession &session,
                                          Poco::URI &uri,
                                          std::ostream &responseStream) {
  // create a request
  this->createRequest(uri);
  session.sendRequest(*m_request) << m_body;

  std::istream &rs = session.receiveResponse(*m_response);
  int retStatus = m_response->getStatus();
  g_log.debug() << "Answer from web: " << retStatus << " "
                << m_response->getReason() << '\n';

  if (retStatus == HTTP_OK ||
      (retStatus == HTTP_CREATED && m_method == HTTPRequest::HTTP_POST)) {
    Poco::StreamCopier::copyStream(rs, responseStream);
    processResponseHeaders(*m_response);
    return retStatus;
  } else if (isRelocated(retStatus)) {
    return this->processRelocation(*m_response, responseStream);
  } else {
    Poco::StreamCopier::copyStream(rs, responseStream);
    return processErrorStates(*m_response, rs, uri.toString());
  }
}

int InternetHelper::processRelocation(const HTTPResponse &response,
                                      std::ostream &responseStream) {
  std::string newLocation = response.get("location", "");
  if (!newLocation.empty()) {
    g_log.information() << "url relocated to " << newLocation;
    return this->sendRequest(newLocation, responseStream);
  } else {
    g_log.warning("Apparent relocation did not give new location\n");
    return response.getStatus();
  }
}

/** Performs a request using http or https depending on the url
 * @param url the address to the network resource
 * @param responseStream The stream to fill with the reply on success
 **/
int InternetHelper::sendRequest(const std::string &url,
                                std::ostream &responseStream) {

  // send the request
  Poco::URI uri(url);
  if (uri.getPath().empty())
    uri = url + "/";
  int retval;
  if ((uri.getScheme() == "https") || (uri.getPort() == 443)) {
    retval = sendHTTPSRequest(uri.toString(), responseStream);
  } else {
    retval = sendHTTPRequest(uri.toString(), responseStream);
  }
  return retval;
}

/**
 * Helper to log (debug level) the request being sent (careful not to
 * print blatant passwords, etc.).
 *
 * @param schemeName Normally "http" or "https"
 * @param url url being sent (will be logged)
 */
void InternetHelper::logDebugRequestSending(const std::string &schemeName,
                                            const std::string &url) const {
  const std::string insecString = "password=";
  if (std::string::npos == url.find(insecString)) {
    g_log.debug() << "Sending " << schemeName << " " << m_method
                  << " request to: " << url << "\n";
  } else {
    g_log.debug()
        << "Sending " << schemeName << " " << m_method
        << " request to an url where the query string seems to contain a "
           "password! (not shown for security reasons)."
        << "\n";
  }
}

/** Performs a request using http
 * @param url the address to the network resource
 * @param responseStream The stream to fill with the reply on success
 **/
int InternetHelper::sendHTTPRequest(const std::string &url,
                                    std::ostream &responseStream) {
  int retStatus = 0;

  logDebugRequestSending("http", url);

  Poco::URI uri(url);
  // Configure Poco HTTP Client Session
  try {
    Poco::Net::HTTPClientSession session(uri.getHost(), uri.getPort());
    session.setTimeout(Poco::Timespan(getTimeout(), 0));

    // configure proxy
    setupProxyOnSession(session, url);

    // low level sending the request
    retStatus = this->sendRequestAndProcess(session, uri, responseStream);
  } catch (HostNotFoundException &ex) {
    throwNotConnected(url, ex);
  } catch (Poco::Exception &ex) {
    throw Exception::InternetError("Connection and request failed " +
                                   ex.displayText());
  }
  return retStatus;
}

/** Performs a request using https
 * @param url the address to the network resource
 * @param responseStream The stream to fill with the reply on success
 **/
int InternetHelper::sendHTTPSRequest(const std::string &url,
                                     std::ostream &responseStream) {
  int retStatus = 0;

  logDebugRequestSending("https", url);

  Poco::URI uri(url);
  try {
    initializeSSL();
    // Create the session
    HTTPSClientSession session(uri.getHost(),
                               static_cast<Poco::UInt16>(uri.getPort()));
    session.setTimeout(Poco::Timespan(getTimeout(), 0));

    // HACK:: Currently the automatic proxy detection only supports http proxy
    // detection
    // most locations use the same proxy for http and https, so force it to use
    // the http proxy
    std::string urlforProxy =
        ConfigService::Instance().getString("proxy.httpsTargetUrl");
    if (urlforProxy.empty()) {
      urlforProxy = "http://" + uri.getHost();
    }
    setupProxyOnSession(session, urlforProxy);

    // low level sending the request
    retStatus = this->sendRequestAndProcess(session, uri, responseStream);
  } catch (HostNotFoundException &ex) {
    throwNotConnected(url, ex);
  } catch (Poco::Exception &ex) {
    throw Exception::InternetError("Connection and request failed " +
                                   ex.displayText());
  }
  return retStatus;
}

/** Gets proxy details for a system and a url.
@param url : The url to be called
*/
Kernel::ProxyInfo &InternetHelper::getProxy(const std::string &url) {
  // set the proxy
  if (!m_isProxySet) {
    setProxy(ConfigService::Instance().getProxy(url));
  }
  return m_proxyInfo;
}

/** Clears cached proxy details.
 */
void InternetHelper::clearProxy() { m_isProxySet = false; }

/** sets the proxy details.
@param proxy the proxy information to use
*/
void InternetHelper::setProxy(const Kernel::ProxyInfo &proxy) {
  m_proxyInfo = proxy;
  m_isProxySet = true;
}

/** Process any headers from the response stream
Basic implementation does nothing.
*/
void InternetHelper::processResponseHeaders(const Poco::Net::HTTPResponse &) {}

/** Process any HTTP errors states.

@param res : The http response
@param rs : The iutput stream from the response
@param url : The url originally called

@exception Mantid::Kernel::Exception::InternetError : Coded for the failure
state.
*/
int InternetHelper::processErrorStates(const Poco::Net::HTTPResponse &res,
                                       std::istream &rs,
                                       const std::string &url) {
  int retStatus = res.getStatus();
  g_log.debug() << "Answer from web: " << res.getStatus() << " "
                << res.getReason() << '\n';

  // get github api rate limit information if available;
  int rateLimitRemaining;
  DateAndTime rateLimitReset;
  try {
    rateLimitRemaining =
        boost::lexical_cast<int>(res.get("X-RateLimit-Remaining", "-1"));
    rateLimitReset.set_from_time_t(
        boost::lexical_cast<int>(res.get("X-RateLimit-Reset", "0")));
  } catch (boost::bad_lexical_cast const &) {
    rateLimitRemaining = -1;
  }

  if (retStatus == HTTP_OK) {
    throw Exception::InternetError("Response was ok, processing should never "
                                   "have entered processErrorStates",
                                   retStatus);
  } else if (retStatus == HTTP_FOUND) {
    throw Exception::InternetError("Response was HTTP_FOUND, processing should "
                                   "never have entered processErrorStates",
                                   retStatus);
  } else if (retStatus == HTTP_MOVED_PERMANENTLY) {
    throw Exception::InternetError("Response was HTTP_MOVED_PERMANENTLY, "
                                   "processing should never have entered "
                                   "processErrorStates",
                                   retStatus);
  } else if (retStatus == HTTP_NOT_MODIFIED) {
    throw Exception::InternetError("Not modified since provided date" +
                                       rateLimitReset.toSimpleString(),
                                   retStatus);
  } else if ((retStatus == HTTP_FORBIDDEN) && (rateLimitRemaining == 0)) {
    throw Exception::InternetError(
        "The Github API rate limit has been reached, try again after " +
            rateLimitReset.toSimpleString() + " GMT",
        retStatus);
  } else {
    std::stringstream info;
    std::stringstream ss;
    Poco::StreamCopier::copyStream(rs, ss);
    if (retStatus == HTTP_NOT_FOUND)
      info << "Failed to download " << url << " with the link "
           << "<a href=\"" << url << "\">.\n"
           << "Hint. Check that link is correct</a>";
    else {
      // show the error
      info << res.getReason();
      info << ss.str();
      g_log.debug() << ss.str();
    }
    throw Exception::InternetError(info.str() + ss.str(), retStatus);
  }
}

/** Download a url and fetch it inside the local path given.

@param urlFile: Define a valid URL for the file to be downloaded. Eventually, it
may give
any valid https path. For example:

url_file = "http://www.google.com"

url_file = "https://mantidweb/repository/README.md"

The result is to connect to the http server, and request the path given.

The answer, will be inserted at the local_file_path.

@param localFilePath : Provide the destination of the file downloaded at the
url_file.

@exception Mantid::Kernel::Exception::InternetError : For any unexpected
behaviour.
*/
int InternetHelper::downloadFile(const std::string &urlFile,
                                 const std::string &localFilePath) {
  int retStatus = 0;
  g_log.debug() << "DownloadFile : " << urlFile << " to file: " << localFilePath
                << '\n';

  Poco::TemporaryFile tempFile;
  Poco::FileStream tempFileStream(tempFile.path());
  retStatus = sendRequest(urlFile, tempFileStream);
  tempFileStream.close();

  // if there have been no errors move it to the final location, and turn off
  // automatic deletion.
  // clear the way if the target file path is already in use
  Poco::File file(localFilePath);
  if (file.exists()) {
    file.remove();
  }

  tempFile.moveTo(localFilePath);
  tempFile.keep();

  return retStatus;
}

/** Sets the timeout in seconds
 * @param seconds The value in seconds for the timeout
 **/
void InternetHelper::setTimeout(int seconds) {
  m_timeout = seconds;
  m_isTimeoutSet = true;
}

/// Checks the HTTP status to decide if this is a relocation
/// @param response the HTTP status
/// @returns true if the return code is considered a relocation
bool InternetHelper::isRelocated(const int response) {
  return ((response == HTTP_FOUND) || (response == HTTP_MOVED_PERMANENTLY) ||
          (response == HTTP_TEMPORARY_REDIRECT) ||
          (response == HTTP_SEE_OTHER));
}

/// Throw an exception occurs when the computer
/// is not connected to the internet
/// @param url The url that was use
/// @param ex The exception generated by Poco
void InternetHelper::throwNotConnected(const std::string &url,
                                       const HostNotFoundException &ex) {
  std::stringstream info;
  info << "Failed to access " << url
       << " because there is no connection to the host " << ex.message()
       << ".\nHint: Check your connection following this link: <a href=\""
       << url << "\">" << url << "</a> ";
  throw Exception::InternetError(info.str() + ex.displayText());
}

/** Gets the timeout in seconds
 * @returns The value in seconds for the timeout
 **/
int InternetHelper::getTimeout() {
  if (!m_isTimeoutSet) {
    if (!ConfigService::Instance().getValue("network.default.timeout",
                                            m_timeout)) {
      m_timeout = 30; // the default value if the key is not found
    }
  }
  return m_timeout;
}

/** Sets the Method
 * @param method A string of GET or POST, anything other than POST is considered
 *GET
 **/
void InternetHelper::setMethod(const std::string &method) {
  if (method == "POST") {
    m_method = method;
  } else {
    m_method = "GET";
  }
}

/** Gets the method
 * @returns either "GET" or "POST"
 **/
const std::string &InternetHelper::getMethod() { return m_method; }

/** Sets the Content Type
 * @param contentType A string of the content type
 **/
void InternetHelper::setContentType(const std::string &contentType) {
  m_contentType = contentType;
}

/** Gets the Content Type
 * @returns A string of the content type
 **/
const std::string &InternetHelper::getContentType() { return m_contentType; }

/** Sets the content length
 * @param length The content length in bytes
 **/
void InternetHelper::setContentLength(std::streamsize length) {
  m_contentLength = length;
}

/** Gets the content length
 * @returns The content length in bytes
 **/
std::streamsize InternetHelper::getContentLength() { return m_contentLength; }

/** Sets the body & content length  for future requests, this will also
 *   set the method to POST is the body is not empty
 *   and GET if it is.
 * @param body A string of the body
 **/
void InternetHelper::setBody(const std::string &body) {
  m_body = body;
  if (m_body.empty()) {
    m_method = "GET";
  } else {
    m_method = "POST";
  }
  setContentLength(m_body.size());
}

/** Sets the body & content length  for future requests, this will also
 *   set the method to POST is the body is not empty
 *   and GET if it is.
 * @param body A stringstream of the body
 **/
void InternetHelper::setBody(const std::ostringstream &body) {
  setBody(body.str());
}

/** Sets the body & content length for future requests, this will also
 *   set the method to POST is the body is not empty
 *   and GET if it is.
 * @param form A HTMLform
 **/
void InternetHelper::setBody(Poco::Net::HTMLForm &form) {
  setMethod("POST");
  if (m_request == nullptr) {
    Poco::URI uri("http://www.mantidproject.org");
    createRequest(uri);
  }
  form.prepareSubmit(*m_request);
  setContentType(m_request->getContentType());

  std::ostringstream ss;
  form.write(ss);
  m_body = ss.str();
  setContentLength(m_body.size());
}

/** Gets the body set for future requests
 * @returns A string of the content type
 **/
const std::string &InternetHelper::getBody() { return m_body; }

/** Gets the body set for future requests
 * @returns A string of the content type
 **/
int InternetHelper::getResponseStatus() { return m_response->getStatus(); }

/** Gets the body set for future requests
 * @returns A string of the content type
 **/
const std::string &InternetHelper::getResponseReason() {
  return m_response->getReason();
}

/** Adds a header
 * @param key The key to refer to the value
 * @param value The value in seconds for the timeout
 **/
void InternetHelper::addHeader(const std::string &key,
                               const std::string &value) {
  m_headers.emplace(key, value);
}

/** Removes a header
 * @param key The key to refer to the value
 **/
void InternetHelper::removeHeader(const std::string &key) {
  m_headers.erase(key);
}

/** Gets the value of a header
 * @param key The key to refer to the value
 * @returns the value as a string
 **/
const std::string &InternetHelper::getHeader(const std::string &key) {
  return m_headers[key];
}

/** Clears all headers
 **/
void InternetHelper::clearHeaders() { m_headers.clear(); }

/** Returns a reference to the headers map
 **/
std::map<std::string, std::string> &InternetHelper::headers() {
  return m_headers;
}

/** Resets properties to defaults (except the proxy)
 **/
void InternetHelper::reset() {
  m_headers.clear();
  m_timeout = 30;
  m_isTimeoutSet = false;
  m_body = "";
  m_method = HTTPRequest::HTTP_GET;
  m_contentType = "application/json";
  m_request = nullptr;
}

} // namespace Kernel
} // namespace Mantid