-
LamarMoore authoredLamarMoore authored
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