Newer
Older
#include "MantidKernel/InternetHelper.h"
#include "MantidKernel/ConfigService.h"
#include "MantidKernel/DateAndTime.h"
#include "MantidKernel/Exception.h"
#include "MantidKernel/Logger.h"
// Poco
#include <Poco/Net/AcceptCertificateHandler.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/HTTPRequest.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/Net/HTTPSClientSession.h>
#include <Poco/Net/PrivateKeyPassphraseHandler.h>
#include <Poco/Net/SecureStreamSocket.h>
#include <Poco/Net/SSLManager.h>
#include <Poco/StreamCopier.h>
#include <Poco/TemporaryFile.h>
#include <Poco/URI.h>
// Visual Studio complains with the inclusion of Poco/FileStream
// disabling this warning.
#if defined(_WIN32) || defined(_WIN64)
#pragma warning(push)
#pragma warning(disable : 4250)
#include <Poco/FileStream.h>
#include <Poco/NullStream.h>
#include <Winhttp.h>
#pragma warning(pop)
#include <Poco/FileStream.h>
#include <Poco/NullStream.h>
#include <stdlib.h>
#endif
// std
#include <fstream>
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");
/// Throw an exception occurs when the computer
/// is not connected to the internet
void throwNotConnected(const std::string &url,
const HostNotFoundException &ex) {
std::stringstream info;
info << "Failed to download " << 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());
}
/// @returns true if the return code is considered a relocation
bool isRelocated(const int response) {
return ((response == HTTPResponse::HTTP_FOUND) ||
(response == HTTPResponse::HTTP_MOVED_PERMANENTLY) ||
(response == HTTPResponse::HTTP_TEMPORARY_REDIRECT) ||
(response == HTTPResponse::HTTP_SEE_OTHER));
}
//----------------------------------------------------------------------------------------------
/** Constructor
*/
InternetHelper::InternetHelper()
: m_proxyInfo(), m_isProxySet(false), m_timeout(30),
m_method(HTTPRequest::HTTP_GET), m_contentType("application/json"),
m_request(NULL) {}
//----------------------------------------------------------------------------------------------
/** Constructor
*/
InternetHelper::InternetHelper(const Kernel::ProxyInfo &proxy)
: m_proxyInfo(proxy), m_isProxySet(true), m_timeout(30),
m_method(HTTPRequest::HTTP_GET), m_contentType("application/json"),
m_request(NULL) {}
//----------------------------------------------------------------------------------------------
/** Destructor
*/
InternetHelper::~InternetHelper() {
if (m_request != NULL) {
delete m_request;
}
}
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) {
m_request =
new HTTPRequest(m_method, uri.getPathAndQuery(), HTTPMessage::HTTP_1_1);
Federico Montesino Pouzols
committed
m_request->set("User-Agent", "MANTID");
// catch non-default Content-Type if given (example values: "text/plain",
// "multipart/mixed; boundary=bqJky99mlBWa-ZuqjC53mG6EzbmlxB", etc.)
const std::string CTName = "Content-Type";
auto cti = m_headers.find(CTName);
if (m_headers.end() != cti) {
m_request->setContentType(cti->second);
} else if (!m_contentType.empty()) {
m_request->setContentType(m_contentType);
}
Federico Montesino Pouzols
committed
for (auto itHeaders = m_headers.begin(); itHeaders != m_headers.end();
++itHeaders) {
Federico Montesino Pouzols
committed
if (itHeaders->first == CTName) // don't put Content-Type a 2nd time
continue;
m_request->set(itHeaders->first, itHeaders->second);
}
}
int InternetHelper::sendRequestAndProcess(HTTPClientSession &session,
Poco::URI &uri,
std::ostream &responseStream) {
// create a request
this->createRequest(uri);
m_request->setContentLength(m_body.length());
session.sendRequest(*m_request) << m_body;
HTTPResponse res;
std::istream &rs = session.receiveResponse(res);
int retStatus = res.getStatus();
g_log.debug() << "Answer from web: " << retStatus << " " << res.getReason()
<< std::endl;
if (retStatus == HTTPResponse::HTTP_OK ||
(retStatus == HTTPResponse::HTTP_CREATED &&
m_method == HTTPRequest::HTTP_POST)) {
Poco::StreamCopier::copyStream(rs, responseStream);
return retStatus;
} else if (isRelocated(retStatus)) {
return this->processRelocation(res, responseStream);
} else {
return processErrorStates(res, 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
* @param headers A optional key value pair map of any additional headers to
Federico Montesino Pouzols
committed
* include in the request. A default 'Content-Type: application/json' is used
* unless the 'Content-Type' key is included here with a different value.
* @param method Generally GET (default) or POST.
* @param body The request body to send.
int InternetHelper::sendRequest(const std::string &url,
std::ostream &responseStream,
const StringToStringMap &headers,
const std::string &method,
const std::string &body) {
// set instance variables from the input as appropriate
if (!method.empty()) {
m_method = method;
}
if (!headers.empty()) {
m_headers = headers;
}
if (!body.empty()) {
m_body = body;
}
// send the request
if ((uri.getScheme() == "https") || (uri.getPort() == 443)) {
return sendHTTPSRequest(url, responseStream);
return sendHTTPRequest(url, responseStream);
/**
* Helper to log (debug level) the request being sent (careful not to
* print blatant passwords, etc.).
*
* @param url url being sent (will be logged)
* @param schemeName Normally "http" or "https"
*/
void InternetHelper::logDebugRequestSending(const std::string &schemeName,
const std::string &url) const {
std::string methodStr = (HTTPRequest::HTTP_GET==m_method)?
"GET" : "POST";
const std::string insecString = "password=";
if (std::string::npos == url.find(insecString)) {
g_log.debug() << "Sending " << schemeName << " " << methodStr <<
" request to: " << url << "\n";
} {
g_log.debug() << "Sending " << schemeName << " " << methodStr <<
" 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) {
logDebugRequestSending("http", url);
// Configure Poco HTTP Client Session
try {
Poco::Net::HTTPClientSession session(uri.getHost(), uri.getPort());
session.setTimeout(Poco::Timespan(m_timeout, 0)); // m_timeout seconds
// configure proxy
setupProxyOnSession(session, url);
// low level sending the request
retStatus = this->sendRequestAndProcess(session, uri, responseStream);
} catch (HostNotFoundException &ex) {
throwNotConnected(url, 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) {
logDebugRequestSending("https", url);
Poco::URI uri(url);
try {
// 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(NULL, certificateHandler, context);
// Create the session
HTTPSClientSession session(uri.getHost(),
static_cast<Poco::UInt16>(uri.getPort()));
session.setTimeout(Poco::Timespan(m_timeout, 0)); // m_timeout seconds
// 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);
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 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() << std::endl;
// 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 == HTTPResponse::HTTP_OK) {
throw Exception::InternetError("Response was ok, processing should never "
"have entered processErrorStates",
retStatus);
} else if (retStatus == HTTPResponse::HTTP_FOUND) {
throw Exception::InternetError("Response was HTTP_FOUND, processing should "
"never have entered processErrorStates",
retStatus);
} else if (retStatus == HTTPResponse::HTTP_MOVED_PERMANENTLY) {
throw Exception::InternetError("Response was HTTP_MOVED_PERMANENTLY, "
"processing should never have entered "
"processErrorStates",
retStatus);
} else if (retStatus == HTTPResponse::HTTP_NOT_MODIFIED) {
throw Exception::InternetError("Not modified since provided date" +
rateLimitReset.toSimpleString(),
retStatus);
} else if ((retStatus == HTTPResponse::HTTP_FORBIDDEN) &&
(rateLimitRemaining == 0)) {
throw Exception::InternetError(
"The Github API rate limit has been reached, try again after " +
rateLimitReset.toSimpleString(),
retStatus);
} else {
std::stringstream info;
std::stringstream ss;
Poco::StreamCopier::copyStream(rs, ss);
if (retStatus == HTTPResponse::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();
}
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.
@param headers [optional] : A key value pair map of any additional headers to
Federico Montesino Pouzols
committed
include in the request. A default 'Content-Type: application/json' is used
unless the 'Content-Type' key is included here with a different value.
@exception Mantid::Kernel::Exception::InternetError : For any unexpected
behaviour.
int InternetHelper::downloadFile(const std::string &urlFile,
const std::string &localFilePath,
const StringToStringMap &headers) {
g_log.debug() << "DownloadFile : " << urlFile << " to file: " << localFilePath
<< std::endl;
Poco::TemporaryFile tempFile;
Poco::FileStream tempFileStream(tempFile.path());
retStatus = sendRequest(urlFile, tempFileStream, headers);
tempFileStream.close();
// if there have been no errors move it to the final location, and turn off
// automatic deletion.
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; }
} // namespace Kernel
} // namespace Mantid