Skip to content
Snippets Groups Projects
InternetHelper.cpp 12.7 KiB
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 )
#else
 #include <Poco/FileStream.h>
 #include <Poco/NullStream.h>
 #include <stdlib.h>
#endif

// std
#include <fstream>

namespace Mantid
{
namespace Kernel
{

using namespace Poco::Net;

namespace 
{ 
  // anonymous namespace for some utility functions

  /// static Logger object
  Logger g_log("InternetHelper");
}

//----------------------------------------------------------------------------------------------
/** Constructor
*/
InternetHelper::InternetHelper() : m_proxyInfo(), m_isProxySet(false),m_timeout(30)
{
}

//----------------------------------------------------------------------------------------------
/** Constructor
*/
InternetHelper::InternetHelper(const Kernel::ProxyInfo& proxy) : m_proxyInfo(proxy), m_isProxySet(true),m_timeout(30)
{
}

//----------------------------------------------------------------------------------------------
/** Destructor
*/
InternetHelper::~InternetHelper()
{
}

/** 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 [optional] : A key value pair map of any additional headers to include in the request.
**/
int InternetHelper::sendRequest(const std::string& url, 
                    std::ostream& responseStream,
                    const StringToStringMap& headers)
{
  Poco::URI uri(url);
  if ((uri.getScheme() == "https") || (uri.getPort() ==443))
  {
    return sendHTTPSRequest(url,responseStream,headers);
  }
  else
  {
    return sendHTTPRequest(url,responseStream,headers);
  }

}

/** Performs a request using http
* @param url the address to the network resource
* @param responseStream The stream to fill with the reply on success
* @param headers [optional] : A key value pair map of any additional headers to include in the request.
**/
int InternetHelper::sendHTTPRequest(const std::string& url, 
                            std::ostream& responseStream,
                            const StringToStringMap& headers)
{
  int retStatus = 0;
  g_log.debug() << "SendingRequest : " << url  << std::endl;

  Poco::URI uri(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
    if (!getProxy(url).emptyProxy())
    {
      session.setProxyHost(getProxy(url).host());
      session.setProxyPort(static_cast<Poco::UInt16>(getProxy(url).port()));
    }

    Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, uri.getPathAndQuery(),
        Poco::Net::HTTPMessage::HTTP_1_1);

    session.sendRequest(request);

    HTTPResponse res;
    std::istream & rs = session.receiveResponse(res);
    retStatus = res.getStatus();
    g_log.debug() << "Answer from web: " << res.getStatus() << " "
                  << res.getReason() << std::endl;
    
    if (res.getStatus() == HTTPResponse::HTTP_OK)
    {
      Poco::StreamCopier::copyStream(rs, responseStream);
      return res.getStatus();
    }  
    else if ((retStatus == HTTPResponse::HTTP_FOUND) ||  
      (retStatus == HTTPResponse::HTTP_MOVED_PERMANENTLY) ||
      (retStatus == HTTPResponse::HTTP_TEMPORARY_REDIRECT) ||
      (retStatus == HTTPResponse::HTTP_SEE_OTHER))
    {
      //extract the new location
      std::string newLocation = res.get("location","");
      if (newLocation != "")
      {
        return sendRequest(newLocation,responseStream,headers);
      }
    }
    else
    {
      retStatus = processErrorStates(res,rs,url);
    }

  } catch (HostNotFoundException & ex)
  {
    // this exception occurs when the pc is not connected to the internet
    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());

  } 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
* @param headers [optional] : A key value pair map of any additional headers to include in the request.
**/
int InternetHelper::sendHTTPSRequest(const std::string& url, 
                            std::ostream& responseStream,
                            const StringToStringMap& headers)
{
  int retStatus = 0;
  g_log.debug() << "SendingRequest : " << url  << std::endl;

  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

    if (!getProxy(uri.getHost()).emptyProxy())
    {
      session.setProxyHost(getProxy(url).host());
      session.setProxyPort(static_cast<Poco::UInt16>(getProxy(url).port()));
    }

    // create a request
    HTTPRequest req(HTTPRequest::HTTP_GET, uri.getPathAndQuery(),
                    HTTPMessage::HTTP_1_1);
    req.set("User-Agent","MANTID");
    for (auto itHeaders = headers.begin(); itHeaders != headers.end(); ++itHeaders)
    {
      req.set(itHeaders->first,itHeaders->second);
    }
    session.sendRequest(req);

    HTTPResponse res;
    std::istream & rs = session.receiveResponse(res);
    retStatus = res.getStatus();
    g_log.debug() << "Answer from web: " << res.getStatus() << " "
                  << res.getReason() << std::endl;
        
    if (res.getStatus() == HTTPResponse::HTTP_OK)
    {
      Poco::StreamCopier::copyStream(rs, responseStream);
      return res.getStatus();
    }  
    else if ((retStatus == HTTPResponse::HTTP_FOUND) ||  
      (retStatus == HTTPResponse::HTTP_MOVED_PERMANENTLY) ||
      (retStatus == HTTPResponse::HTTP_TEMPORARY_REDIRECT) ||
      (retStatus == HTTPResponse::HTTP_SEE_OTHER))
    {
      //extract the new location
      std::string newLocation = res.get("location","");
      if (newLocation != "")
      {
        return sendRequest(newLocation,responseStream,headers);
      }
    }
    else
    {
      retStatus = processErrorStates(res,rs,url);
    }
  } 
  catch (HostNotFoundException & ex)
  {
    // this exception occurs when the pc is not connected to the internet
    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());

  } 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 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 include in the request.

@exception Mantid::Kernel::Exception::InternetError : For any unexpected behaviour.
*/
int InternetHelper::downloadFile(const std::string & urlFile,
  const std::string & localFilePath,
  const StringToStringMap & headers)
{
  int retStatus = 0;
  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