diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index 52938194d..e449c0eda 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -1,818 +1,837 @@ -#ifndef EE_NETWORKCHTTP_HPP -#define EE_NETWORKCHTTP_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace EE { namespace System { -class IOStream; -}} // namespace EE::System - -using namespace EE::System; - -namespace EE { namespace Network { - -/** @brief A HTTP client */ -class EE_API Http : NonCopyable { - public: - /** @brief Define a HTTP response */ - class EE_API Response { - public: - // Types - typedef std::map FieldTable; - - /** @brief Enumerate all the valid status codes for a response */ - enum Status { - // 2xx: success - Ok = 200, ///< Most common code returned when operation was successful - Created = 201, ///< The resource has successfully been created - Accepted = - 202, ///< The request has been accepted, but will be processed later by the server - NoContent = 204, ///< The server didn't send any data in return - ResetContent = 205, ///< The server informs the client that it should clear the view - ///< (form) that caused the request to be sent - PartialContent = 206, ///< The server has sent a part of the resource, as a response to - ///< a partial GET request - - // 3xx: redirection - MultipleChoices = 300, ///< The requested page can be accessed from several locations - MovedPermanently = 301, ///< The requested page has permanently moved to a new location - MovedTemporarily = 302, ///< The requested page has temporarily moved to a new location - NotModified = 304, ///< For conditional requests, means the requested page hasn't - ///< changed and doesn't need to be refreshed - - // 4xx: client error - BadRequest = 400, ///< The server couldn't understand the request (syntax error) - Unauthorized = 401, ///< The requested page needs an authentication to be accessed - Forbidden = - 403, ///< The requested page cannot be accessed at all, even with authentication - NotFound = 404, ///< The requested page doesn't exist - RangeNotSatisfiable = 407, ///< The server can't satisfy the partial GET request (with a - ///< "Range" header field) - - // 5xx: server error - InternalServerError = 500, ///< The server encountered an unexpected error - NotImplemented = 501, ///< The server doesn't implement a requested feature - BadGateway = 502, ///< The gateway server has received an error from the source server - ServiceNotAvailable = - 503, ///< The server is temporarily unavailable (overloaded, in maintenance, ...) - GatewayTimeout = - 504, ///< The gateway server couldn't receive a response from the source server - VersionNotSupported = 505, ///< The server doesn't support the requested HTTP version - - // 10xx: Custom codes - InvalidResponse = 1000, ///< Response is not a valid HTTP one - ConnectionFailed = 1001 ///< Connection with server failed - }; - - /** @return The status string */ - static const char* statusToString( const Status& status ); - - /** @return True if the value is a valid Status */ - static Status intAsStatus( const int& value ); - - /** Creates a faked response. Useful for testing. */ - static Response createFakeResponse( const FieldTable& fields, Status& status, - const std::string& body, unsigned int majorVersion = 1, - unsigned int minorVersion = 1 ); - - /** @brief Default constructor - ** Constructs an empty response. */ - Response(); - - FieldTable getHeaders(); - - /** @brief Get the value of a field - ** If the field @a field is not found in the response header, - ** the empty string is returned. This function uses - ** case-insensitive comparisons. - ** @param field Name of the field to get - ** @return Value of the field, or empty string if not found */ - const std::string& getField( const std::string& field ) const; - - /** @return If the field is found in the response headers. */ - bool hasField( const std::string& field ) const; - - /** @brief Get the response status code - ** The status code should be the first thing to be checked - ** after receiving a response, it defines whether it is a - ** success, a failure or anything else (see the Status - ** enumeration). - ** @return Status code of the response */ - Status getStatus() const; - - /** @brief Get the response status description */ - const char* getStatusDescription() const; - - /** @brief Get the major HTTP version number of the response - ** @return Major HTTP version number - ** @see GetMinorHttpVersion */ - unsigned int getMajorHttpVersion() const; - - /** @brief Get the minor HTTP version number of the response - ** @return Minor HTTP version number - ** @see GetMajorHttpVersion */ - unsigned int getMinorHttpVersion() const; - - /** @brief Get the body of the response - ** The body of a response may contain: - ** @li the requested page (for GET requests) - ** @li a response from the server (for POST requests) - ** @li nothing (for HEAD requests) - ** @li an error message (in case of an error) - ** @return The response body */ - const std::string& getBody() const; - - private: - friend class Http; - - /** @brief Construct the header from a response string - ** This function is used by Http to build the response - ** of a request. - ** @param data Content of the response to parse */ - void parse( const std::string& data ); - - /** @brief Read values passed in the answer header - ** This function is used by Http to extract values passed - ** in the response. - ** @param in String stream containing the header values */ - void parseFields( std::istream& in ); - - // Member data - FieldTable mFields; ///< Fields of the header - Status mStatus; ///< Status code - unsigned int mMajorVersion; ///< Major HTTP version - unsigned int mMinorVersion; ///< Minor HTTP version - std::string mBody; ///< Body of the response - }; - - /** @brief Define a HTTP request */ - class EE_API Request { - public: - /** @brief Enumerate the available HTTP methods for a request */ - enum Method { - Get, ///< The GET method requests a representation of the specified resource. Requests - ///< using GET should only retrieve data. - Head, ///< Request a page's header only - Post, ///< The POST method is used to submit an entity to the specified resource, often - ///< causing a change in state or side effects on the server. - Put, ///< The PUT method replaces all current representations of the target resource - ///< with the request payload. - Delete, ///< The DELETE method deletes the specified resource. - Options, ///< The OPTIONS method is used to describe the communication options for the - ///< target resource. - Patch, ///< The PATCH method is used to apply partial modifications to a resource. - Connect ///< The CONNECT method starts two-way communications with the requested - ///< resource. It can be used to open a tunnel. - }; - - /** @brief Enumerate the available states for a request */ - enum Status { - Connected, ///< Connected to server. - Sent, ///< Request sent to the server. - HeaderReceived, ///< Header received. - ContentReceived ///< Content received. - }; - - /** @return Method from a method name string. */ - static Method methodFromString( std::string methodString ); - - /** @return The method string from a method */ - static std::string methodToString( const Method& method ); - - /** @brief Default constructor - ** This constructor creates a GET request, with the root - ** URI ("/") and an empty body. - ** @param uri Target URI - ** @param method Method to use for the request - ** @param body Content of the request's body - ** @param validateCertificate Enables certificate validation for https request - ** @param validateHostname Enables hostname validation for https request - ** @param followRedirect Allow follor redirects to the request. - ** @param compressedResponse Set if the requested response should be compressed ( if - *available ) - */ - Request( const std::string& uri = "/", Method method = Get, const std::string& body = "", - bool validateCertificate = true, bool validateHostname = true, - bool followRedirect = true, bool compressedResponse = false ); - - /** @brief Set the value of a field - ** The field is created if it doesn't exist. The name of - ** the field is case insensitive. - ** By default, a request doesn't contain any field (but the - ** mandatory fields are added later by the HTTP client when - ** sending the request). - ** @param field Name of the field to set - ** @param value Value of the field */ - void setField( const std::string& field, const std::string& value ); - - /** @see setField */ - void setHeader( const std::string& field, const std::string& value ); - - /** @brief Check if the request defines a field - ** This function uses case-insensitive comparisons. - ** @param field Name of the field to test - ** @return True if the field exists, false otherwise */ - bool hasField( const std::string& field ) const; - - /** @brief Get the value of a field - ** If the field @a field is not found in the response header, - ** the empty string is returned. This function uses - ** case-insensitive comparisons. - ** @param field Name of the field to get - ** @return Value of the field, or empty string if not found */ - const std::string& getField( const std::string& field ) const; - - /** @brief Set the request method - ** See the Method enumeration for a complete list of all - ** the available methods. - ** The method is Http::Request::Get by default. - ** @param method Method to use for the request */ - void setMethod( Method method ); - - /** @brief Set the requested URI - ** The URI is the resource (usually a web page or a file) - ** that you want to get or post. - ** The URI is "/" (the root page) by default. - ** @param uri URI to request, relative to the host */ - void setUri( const std::string& uri ); - - /** @brief Set the HTTP version for the request - ** The HTTP version is 1.0 by default. - ** @param major Major HTTP version number - ** @param minor Minor HTTP version number */ - void setHttpVersion( unsigned int major, unsigned int minor ); - - /** @brief Set the body of the request - ** The body of a request is optional and only makes sense - ** for POST requests. It is ignored for all other methods. - ** The body is empty by default. - ** @param body Content of the body */ - void setBody( const std::string& body ); - - /** @see setBody */ - void setBody( std::string&& body ); - - /** @see setBody */ - void appendBody( const std::string& body ); - void appendBody( std::string&& body ); - - /** @return The request Uri */ - const std::string& getUri() const; - - /** @return The request Method */ - const Method& getMethod() const; - - /** @return If SSL certificate validation is enabled */ - const bool& getValidateCertificate() const; - - /** Enable/disable SSL certificate validation */ - void setValidateCertificate( bool enable ); - - /** @return If SSL hostname validation is enabled */ - const bool& getValidateHostname() const; - - /** Enable/disable SSL hostname validation */ - void setValidateHostname( bool enable ); - - /** @return If requests follow redirects */ - const bool& getFollowRedirect() const; - - /** Enables/Disables follow redirects */ - void setFollowRedirect( bool follow ); - - /** @return The maximum number of redirects allowed if follow redirect is enabled. */ - const unsigned int& getMaxRedirects() const; - - /** Set the maximum number of redirects allowed if follow redirect is enabled. */ - void setMaxRedirects( unsigned int maxRedirects ); - - /** Definition of the current progress callback - * @param http The http client - * @param request The http request - * @param status The status of the progress event - * @param totalBytes The total bytes of the document / files ( only available if - * Content-Length is returned, otherwise is 0 ) - * @param currentBytes Current received total bytes - * @return True if continue the request, false will cancel the current request. - */ - typedef std::function - ProgressCallback; - - /** Sets a progress callback */ - void setProgressCallback( const ProgressCallback& progressCallback ); - - /** Get the progress callback */ - const ProgressCallback& getProgressCallback() const; - - /** Cancels the current request if being processed */ - void cancel(); - - /** @return True if the current request was cancelled */ - const bool& isCancelled() const; - - /** @return If requests a compressed response */ - const bool& isCompressedResponse() const; - - /** Set to request a compressed response from the server - ** The returned response will be automatically decompressed - ** by the client. - */ - void setCompressedResponse( const bool& compressedResponse ); - - /** Resumes download if a file is already present */ - void setContinue( const bool& resume ); - - /** @return If must continue a download previously started. */ - const bool& isContinue() const; - - // Types - typedef std::map FieldTable; - - /** @return True if request is verbose logging */ - bool isVerbose() const; - - /** Set verbose logging */ - void setVerbose( bool verbose ); - - private: - friend class Http; - - /** @brief Prepare the final request to send to the server - ** This is used internally by Http before sending the - ** request to the web server. - ** @return String containing the request, ready to be sent */ - std::string prepare( const Http& http ) const; - - /** Prepares a http tunnel request */ - std::string prepareTunnel( const Http& http ); - - // Member data - FieldTable mFields; ///< Fields of the header associated to their value - Method mMethod; ///< Method to use for the request - std::string mUri; ///< Target URI of the request - unsigned int mMajorVersion; ///< Major HTTP version - unsigned int mMinorVersion; ///< Minor HTTP version - std::string mBody; ///< Body of the request - bool mValidateCertificate; ///< Validates the SSL certificate in case of an HTTPS request - bool mValidateHostname; ///< Validates the hostname in case of an HTTPS request - bool mFollowRedirect; ///< Follows redirect response codes - bool mCompressedResponse; ///< Request compressed response - bool mContinue; ///< Resume download - mutable bool mCancel; ///< Cancel state of current request - bool mVerbose{ false }; ///< Enable/Disable verbosity - ProgressCallback mProgressCallback; ///< Progress callback - unsigned int mMaxRedirections; ///< Maximum number of redirections allowed - mutable unsigned int mRedirectionCount; ///< Number of redirections followed by the request - URI mProxy; ///< Proxy information - }; - - /** @brief Default constructor */ - Http(); - - /** @brief Construct the HTTP client with the target host - ** This is equivalent to calling setHost(host, port). - ** The port has a default value of 0, which means that the - ** HTTP client will use the right port according to the - ** protocol used (80 for HTTP, 443 for HTTPS). You should - ** leave it like this unless you really need a port other - ** than the standard one, or use an unknown protocol. - ** @param host Web server to connect to - ** @param port Port to use for connection - ** @param useSSL force the SSL usage ( if compiled with the support of it ). If the host starts - *with https:// it will use it by default. - ** @param proxy Set an http proxy for the host connection - */ - Http( const std::string& host, unsigned short port = 0, bool useSSL = false, - URI proxy = URI() ); - - ~Http(); - - /** @brief Set the target host - ** This function just stores the host address and port, it - ** doesn't actually connect to it until you send a request. - ** The port has a default value of 0, which means that the - ** HTTP client will use the right port according to the - ** protocol used (80 for HTTP, 443 for HTTPS). You should - ** leave it like this unless you really need a port other - ** than the standard one, or use an unknown protocol. - ** @param host Web server to connect to - ** @param port Port to use for connection - ** @param useSSL force the SSL usage ( if compiled with the support of it ). If the host starts - *with https:// it will use it by default. * @param proxy Set an http proxy for the host - *connection - */ - void setHost( const std::string& host, unsigned short port = 0, bool useSSL = false, - URI proxy = URI() ); - - /** @brief Sets the host from an URI (this is the equivalent of calling setHost( uri.getHost(), uri.getPort(), uri.getScheme() == "https" ) ) */ - void setHost( const URI& uri, URI proxy = URI() ); - - /** @brief Send a HTTP request and return the server's response. - ** You must have a valid host before sending a request (see setHost). - ** Any missing mandatory header field in the request will be added - ** with an appropriate value. - ** Warning: this function waits for the server's response and may - ** not return instantly; use a thread if you don't want to block your - ** application, or use a timeout to limit the time to wait. A value - ** of Time::Zero means that the client will use the system default timeout - ** (which is usually pretty long). - ** @param request Request to send - ** @param timeout Maximum time to wait - ** @return Server's response */ - Response sendRequest( const Request& request, Time timeout = Time::Zero ); - - /** @brief Send a HTTP request and writes the server's response to a IOStream file. - ** You must have a valid host before sending a request (see setHost). - ** Any missing mandatory header field in the request will be added - ** with an appropriate value. - ** Warning: this function waits for the server's response and may - ** not return instantly; use a thread if you don't want to block your - ** application, or use a timeout to limit the time to wait. A value - ** of Time::Zero means that the client will use the system default timeout - ** (which is usually pretty long). - ** @param request Request to send - ** @param writeTo The IO stream to write the downloaded content - ** @param timeout Maximum time to wait - ** @return Server's response */ - Response downloadRequest( const Request& request, IOStream& writeTo, - Time timeout = Time::Zero ); - - /** @brief Send a HTTP request and writes the server's response to a file system path. - ** You must have a valid host before sending a request (see setHost). - ** Any missing mandatory header field in the request will be added - ** with an appropriate value. - ** Warning: this function waits for the server's response and may - ** not return instantly; use a thread if you don't want to block your - ** application, or use a timeout to limit the time to wait. A value - ** of Time::Zero means that the client will use the system default timeout - ** (which is usually pretty long). - ** @param request Request to send - ** @param writePath The path of the file to write the downloaded content - ** @param timeout Maximum time to wait - ** @return Server's response */ - Response downloadRequest( const Request& request, std::string writePath, - Time timeout = Time::Zero ); - - /** Definition of the async callback response */ - typedef std::function - AsyncResponseCallback; - - /** @brief Sends the request and creates a new thread, when got the response informs the result - *to the callback. * This function does not lock the caller thread. - ** @see sendRequest */ - void sendAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, - Time timeout = Time::Zero ); - - /** @brief Sends the request and creates a new thread, when got the response informs the result - *to the callback. * This function does not lock the caller thread. - ** @see downloadRequest */ - void downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, - IOStream& writeTo, Time timeout = Time::Zero ); - - /** @brief Sends the request and creates a new thread, when got the response informs the result - *to the callback. * This function does not lock the caller thread. - ** @see downloadRequest */ - void downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, - std::string writePath, Time timeout = Time::Zero ); - - /** @return The host address */ - const IpAddress& getHost() const; - - /** @return The host name */ - const std::string& getHostName() const; - - /** @return The host port */ - const unsigned short& getPort() const; - - /** @return If the HTTP client uses SSL/TLS */ - const bool& isSSL() const; - - /** @return The URI from the schema + hostname + port */ - URI getURI() const; - - /** Sets the request proxy */ - void setProxy( const URI& uri ); - - /** @return The request proxy */ - const URI& getProxy() const; - - /** @return Is a proxy is need to be used */ - bool isProxied() const; - - /** Helper class to build the body of a multipart/form-data request. */ - class EE_API MultipartEntitiesBuilder { - public: - MultipartEntitiesBuilder(); - - /** @param boundary The boundary to use in the multipart data. */ - MultipartEntitiesBuilder( const std::string& boundary ); - - /** @returns The corresponding request Content-Type needed. - * This Content-Type header must be set to the request in order to work correctly. - * - * For example: - * @code - * Http::Request request; - * Http::MultipartEntitiesBuilder builder; - * ... - * request.setField( "Content-Type", builder.getContentType() ); - * @endcode - */ - std::string getContentType(); - - /** @return The boundary used to build the multipart data. */ - const std::string& getBoundary() const; - - /** Adds a text multipart form field. */ - void addParameter( const std::string& name, const std::string& value ); - - /** Adds a file to the multipart data. - * @param parameterName The field name. - * @param fileName The file name of the stream. - * @param stream The stream were the file is located and is going to be read. - */ - void addFile( const std::string& parameterName, const std::string& fileName, - IOStream* stream ); - - /** Adds a file to the multipart data. - * @param parameterName The field name. - * @param filePath The local file path. - */ - void addFile( const std::string& parameterName, const std::string& filePath ); - - std::string build(); - - protected: - void buildFilePart( std::ostream& ostream, IOStream* stream, const std::string& fieldName, - const std::string& fileName, const std::string& contentType ); - - void buildTextPart( std::ostream& ostream, const std::string& parameterName, - const std::string& parameterValue ); - - std::string mBoundary; - std::map> mStreamParams; - std::map mFileParams; - std::map mParams; - }; - - /** HTTP Client Pool. - * Will keep the instances of the HTTP clients until the Pool is destroyed. - * Acts as a host client cache. - */ - class EE_API Pool { - public: - /** @returns The reference to the global HTTP Pool - * A global HTTP Pool is created at the program start - */ - static Pool& getGlobal(); - - Pool(); - - ~Pool(); - - /** Clear all the HTTP Clients */ - void clear(); - - /** @return True if the client already exists in the pool - * @param host The scheme + hostname + port represented as an URI. - * @param proxy The client proxy if any, scheme + hostname + post as URI. - */ - bool exists( const URI& host, const URI& proxy = URI() ); - - /** @return An HTTP Client to the host and proxy ( creates one if no one is found ) - * @param host The scheme + hostname + port represented as an URI. - * @param proxy The client proxy if any, scheme + hostname + post as URI. - */ - std::shared_ptr get( const URI& host, const URI& proxy = URI() ); - - protected: - Mutex mMutex; - UnorderedMap> mHttps; - - static std::string getHostKey( const URI& host, const URI& proxy ); - - static String::HashType getHostHash( const URI& host, const URI& proxy ); - }; - - /** Creates an HTTP Request using the global HTTP Client Pool */ - static Response - request( const URI& uri, Request::Method method = Request::Method::Get, - const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), - const std::string& body = "", const bool& validateCertificate = true, - const URI& proxy = URI() ); - - /** Creates an HTTP GET Request using the global HTTP Client Pool */ - static Response - get( const URI& uri, const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", - const bool& validateCertificate = true, const URI& proxy = URI() ); - - /** Creates an HTTP POST Request using the global HTTP Client Pool */ - static Response - post( const URI& uri, const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", - const bool& validateCertificate = true, const URI& proxy = URI() ); - - /** Creates an async HTTP Request using the global HTTP Client Pool */ - static void - requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri, - const Time& timeout = Time::Zero, Request::Method method = Request::Method::Get, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), - const std::string& body = "", const bool& validateCertificate = true, - const URI& proxy = URI() ); - - /** Creates an async HTTP GET Request using the global HTTP Client Pool */ - static void getAsync( - const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", - const bool& validateCertificate = true, const URI& proxy = URI() ); - - /** Creates an async HTTP POST Request using the global HTTP Client Pool */ - static void postAsync( - const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero, - const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), - const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", - const bool& validateCertificate = true, const URI& proxy = URI() ); - - /** It will try to get the proxy from the environment variables. */ - static URI getEnvProxyURI(); - - /** Set the thread pool to consume for async requests, otherwise it will use its own */ - static void setThreadPool( std::shared_ptr pool ); - - private: - class AsyncRequest : public Thread { - public: - AsyncRequest( Http* http, const AsyncResponseCallback& cb, Http::Request request, - Time timeout ); - - AsyncRequest( Http* http, const AsyncResponseCallback& cb, Http::Request request, - IOStream& writeTo, Time timeout ); - - AsyncRequest( Http* http, const AsyncResponseCallback& cb, Http::Request request, - std::string writePath, Time timeout ); - - ~AsyncRequest(); - - void run(); - - protected: - friend class Http; - Http* mHttp; - AsyncResponseCallback mCb; - Http::Request mRequest; - Time mTimeout; - bool mRunning; - bool mStreamed; - bool mStreamOwned; - IOStream* mStream; - }; - - class HttpConnection { - public: - HttpConnection(); - - HttpConnection( TcpSocket* socket ); - - ~HttpConnection(); - - void setSocket( TcpSocket* socket ); - - TcpSocket* getSocket() const; - - void disconnect(); - - const bool& isConnected() const; - - void setConnected( const bool& connected ); - - const bool& isTunneled() const; - - void setTunneled( const bool& tunneled ); - - const bool& isSSL() const; - - void setSSL( const bool& ssl ); - - const bool& isKeepAlive() const; - - void setKeepAlive( const bool& isKeepAlive ); - - protected: - TcpSocket* mSocket; - bool mIsConnected; - bool mIsTunneled; - bool mIsSSL; - bool mIsKeepAlive; - }; - - friend class AsyncRequest; - ThreadLocalPtr mConnection; ///< Connection to the host - IpAddress mHost; ///< Web host address - std::string mHostName; ///< Web host name - unsigned short mPort; ///< Port used for connection with host - std::vector mThreads; - Mutex mThreadsMutex; - bool mIsSSL; - bool mHostSolved; - URI mProxy; - - void removeOldThreads(); - - Request prepareFields( const Http::Request& request ); -}; - -}} // namespace EE::Network - -#endif // EE_NETWORKCHTTP_HPP - -/** -@class EE::Network::Http - -Http is a very simple HTTP client that allows you -to communicate with a web server. You can retrieve -web pages, send data to an interactive resource, -download a remote file, etc. -The HTTP client is split into 3 classes: -@li EE::Network::Http::Request -@li EE::Network::Http::Response -@li EE::Network::Http -EE::Network::Http::Request builds the request that will be -sent to the server. A request is made of: -@li a method (what you want to do) -@li a target URI (usually the name of the web page or file) -@li one or more header fields (options that you can pass to the server) -@li an optional body (for POST requests) -EE::Network::Http::Response parse the response from the web server -and provides getters to read them. The response contains: -@li a status code -@li header fields (that may be answers to the ones that you requested) -@li a body, which contains the contents of the requested resource -Http provides a simple function, sendRequest, to send a -EE::Network::Http::Request and return the corresponding EE::Network::Http::Response -from the server. -Usage example: -@code -// Create a new HTTP client -Http http; - -// We'll work on http://www.google.com -http.setHost( "http://www.google.com" ); - -// Prepare a request to get the 'features.php' page -Http::Request request( "features.php" ); - -// Send the request -Http::Response response = http.sendRequest(request); - -// Check the status code and display the result -Http::Response::Status status = response.getStatus(); -if ( status == Http::Response::Ok ) { - std::cout << response.getBody() << std::endl; -} else { - std::cout << "Error " << status << std::endl; -} -@endcode - -Shorthand methods are also provided: -@code -Http::Response response = Http::get( "http://www.google.com" ); -if ( response.getStatus() == Http::Response::Ok ) { - std::cout << response.getBody() << std::endl; -} else { - std::cout << "Error " << response.getStatus() << std::endl; -} -@endcode - -You can also use the shorthand async alternative method: -@code -Http::getAsync( - [=]( const Http&, Http::Request&, Http::Response& response ) { - if ( response.getStatus() == Http::Response::Ok) { - std::cout << response.getBody() << std::endl; - } else { - std::cout << "Error " << response.getStatus() << std::endl; - } - }, "http://www.google.com" ); -@endcode -*/ +#ifndef EE_NETWORKCHTTP_HPP +#define EE_NETWORKCHTTP_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace System { +class IOStream; +}} // namespace EE::System + +using namespace EE::System; + +namespace EE { namespace Network { + +/** @brief A HTTP client */ +class EE_API Http : NonCopyable { + public: + /** @brief Define a HTTP response */ + class EE_API Response { + public: + // Types + typedef std::map FieldTable; + + /** @brief Enumerate all the valid status codes for a response */ + enum Status { + // 2xx: success + Ok = 200, ///< Most common code returned when operation was successful + Created = 201, ///< The resource has successfully been created + Accepted = + 202, ///< The request has been accepted, but will be processed later by the server + NoContent = 204, ///< The server didn't send any data in return + ResetContent = 205, ///< The server informs the client that it should clear the view + ///< (form) that caused the request to be sent + PartialContent = 206, ///< The server has sent a part of the resource, as a response to + ///< a partial GET request + + // 3xx: redirection + MultipleChoices = 300, ///< The requested page can be accessed from several locations + MovedPermanently = 301, ///< The requested page has permanently moved to a new location + MovedTemporarily = 302, ///< The requested page has temporarily moved to a new location + NotModified = 304, ///< For conditional requests, means the requested page hasn't + ///< changed and doesn't need to be refreshed + + // 4xx: client error + BadRequest = 400, ///< The server couldn't understand the request (syntax error) + Unauthorized = 401, ///< The requested page needs an authentication to be accessed + Forbidden = + 403, ///< The requested page cannot be accessed at all, even with authentication + NotFound = 404, ///< The requested page doesn't exist + RangeNotSatisfiable = 407, ///< The server can't satisfy the partial GET request (with a + ///< "Range" header field) + + // 5xx: server error + InternalServerError = 500, ///< The server encountered an unexpected error + NotImplemented = 501, ///< The server doesn't implement a requested feature + BadGateway = 502, ///< The gateway server has received an error from the source server + ServiceNotAvailable = + 503, ///< The server is temporarily unavailable (overloaded, in maintenance, ...) + GatewayTimeout = + 504, ///< The gateway server couldn't receive a response from the source server + VersionNotSupported = 505, ///< The server doesn't support the requested HTTP version + + // 10xx: Custom codes + InvalidResponse = 1000, ///< Response is not a valid HTTP one + ConnectionFailed = 1001 ///< Connection with server failed + }; + + /** @return The status string */ + static const char* statusToString( const Status& status ); + + /** @return True if the value is a valid Status */ + static Status intAsStatus( const int& value ); + + /** Creates a faked response. Useful for testing. */ + static Response createFakeResponse( const FieldTable& fields, Status& status, + const std::string& body, unsigned int majorVersion = 1, + unsigned int minorVersion = 1 ); + + /** @brief Default constructor + ** Constructs an empty response. */ + Response(); + + FieldTable getHeaders(); + + /** @brief Get the value of a field + ** If the field @a field is not found in the response header, + ** the empty string is returned. This function uses + ** case-insensitive comparisons. + ** @param field Name of the field to get + ** @return Value of the field, or empty string if not found */ + const std::string& getField( const std::string& field ) const; + + /** @return If the field is found in the response headers. */ + bool hasField( const std::string& field ) const; + + /** @brief Get the response status code + ** The status code should be the first thing to be checked + ** after receiving a response, it defines whether it is a + ** success, a failure or anything else (see the Status + ** enumeration). + ** @return Status code of the response */ + Status getStatus() const; + + /** @brief Get the response status description */ + const char* getStatusDescription() const; + + /** @brief Get the major HTTP version number of the response + ** @return Major HTTP version number + ** @see GetMinorHttpVersion */ + unsigned int getMajorHttpVersion() const; + + /** @brief Get the minor HTTP version number of the response + ** @return Minor HTTP version number + ** @see GetMajorHttpVersion */ + unsigned int getMinorHttpVersion() const; + + /** @brief Get the body of the response + ** The body of a response may contain: + ** @li the requested page (for GET requests) + ** @li a response from the server (for POST requests) + ** @li nothing (for HEAD requests) + ** @li an error message (in case of an error) + ** @return The response body */ + const std::string& getBody() const; + + private: + friend class Http; + + /** @brief Construct the header from a response string + ** This function is used by Http to build the response + ** of a request. + ** @param data Content of the response to parse */ + void parse( const std::string& data ); + + /** @brief Read values passed in the answer header + ** This function is used by Http to extract values passed + ** in the response. + ** @param in String stream containing the header values */ + void parseFields( std::istream& in ); + + // Member data + FieldTable mFields; ///< Fields of the header + Status mStatus; ///< Status code + unsigned int mMajorVersion; ///< Major HTTP version + unsigned int mMinorVersion; ///< Minor HTTP version + std::string mBody; ///< Body of the response + }; + + /** @brief Define a HTTP request */ + class EE_API Request { + public: + /** @brief Enumerate the available HTTP methods for a request */ + enum Method { + Get, ///< The GET method requests a representation of the specified resource. Requests + ///< using GET should only retrieve data. + Head, ///< Request a page's header only + Post, ///< The POST method is used to submit an entity to the specified resource, often + ///< causing a change in state or side effects on the server. + Put, ///< The PUT method replaces all current representations of the target resource + ///< with the request payload. + Delete, ///< The DELETE method deletes the specified resource. + Options, ///< The OPTIONS method is used to describe the communication options for the + ///< target resource. + Patch, ///< The PATCH method is used to apply partial modifications to a resource. + Connect ///< The CONNECT method starts two-way communications with the requested + ///< resource. It can be used to open a tunnel. + }; + + /** @brief Enumerate the available states for a request */ + enum Status { + Connected, ///< Connected to server. + Sent, ///< Request sent to the server. + HeaderReceived, ///< Header received. + ContentReceived ///< Content received. + }; + + /** @return Method from a method name string. */ + static Method methodFromString( std::string methodString ); + + /** @return The method string from a method */ + static std::string methodToString( const Method& method ); + + /** @brief Default constructor + ** This constructor creates a GET request, with the root + ** URI ("/") and an empty body. + ** @param uri Target URI + ** @param method Method to use for the request + ** @param body Content of the request's body + ** @param validateCertificate Enables certificate validation for https request + ** @param validateHostname Enables hostname validation for https request + ** @param followRedirect Allow follor redirects to the request. + ** @param compressedResponse Set if the requested response should be compressed ( if + *available ) + */ + Request( const std::string& uri = "/", Method method = Get, const std::string& body = "", + bool validateCertificate = true, bool validateHostname = true, + bool followRedirect = true, bool compressedResponse = false ); + + /** @brief Set the value of a field + ** The field is created if it doesn't exist. The name of + ** the field is case insensitive. + ** By default, a request doesn't contain any field (but the + ** mandatory fields are added later by the HTTP client when + ** sending the request). + ** @param field Name of the field to set + ** @param value Value of the field */ + void setField( const std::string& field, const std::string& value ); + + /** @see setField */ + void setHeader( const std::string& field, const std::string& value ); + + /** @brief Check if the request defines a field + ** This function uses case-insensitive comparisons. + ** @param field Name of the field to test + ** @return True if the field exists, false otherwise */ + bool hasField( const std::string& field ) const; + + /** @brief Get the value of a field + ** If the field @a field is not found in the response header, + ** the empty string is returned. This function uses + ** case-insensitive comparisons. + ** @param field Name of the field to get + ** @return Value of the field, or empty string if not found */ + const std::string& getField( const std::string& field ) const; + + /** @brief Set the request method + ** See the Method enumeration for a complete list of all + ** the available methods. + ** The method is Http::Request::Get by default. + ** @param method Method to use for the request */ + void setMethod( Method method ); + + /** @brief Set the requested URI + ** The URI is the resource (usually a web page or a file) + ** that you want to get or post. + ** The URI is "/" (the root page) by default. + ** @param uri URI to request, relative to the host */ + void setUri( const std::string& uri ); + + /** @brief Set the HTTP version for the request + ** The HTTP version is 1.0 by default. + ** @param major Major HTTP version number + ** @param minor Minor HTTP version number */ + void setHttpVersion( unsigned int major, unsigned int minor ); + + /** @brief Set the body of the request + ** The body of a request is optional and only makes sense + ** for POST requests. It is ignored for all other methods. + ** The body is empty by default. + ** @param body Content of the body */ + void setBody( const std::string& body ); + + /** @see setBody */ + void setBody( std::string&& body ); + + /** @see setBody */ + void appendBody( const std::string& body ); + void appendBody( std::string&& body ); + + /** @return The request Uri */ + const std::string& getUri() const; + + /** @return The request Method */ + const Method& getMethod() const; + + /** @return If SSL certificate validation is enabled */ + const bool& getValidateCertificate() const; + + /** Enable/disable SSL certificate validation */ + void setValidateCertificate( bool enable ); + + /** @return If SSL hostname validation is enabled */ + const bool& getValidateHostname() const; + + /** Enable/disable SSL hostname validation */ + void setValidateHostname( bool enable ); + + /** @return If requests follow redirects */ + const bool& getFollowRedirect() const; + + /** Enables/Disables follow redirects */ + void setFollowRedirect( bool follow ); + + /** @return The maximum number of redirects allowed if follow redirect is enabled. */ + const unsigned int& getMaxRedirects() const; + + /** Set the maximum number of redirects allowed if follow redirect is enabled. */ + void setMaxRedirects( unsigned int maxRedirects ); + + /** Definition of the current progress callback + * @param http The http client + * @param request The http request + * @param status The status of the progress event + * @param totalBytes The total bytes of the document / files ( only available if + * Content-Length is returned, otherwise is 0 ) + * @param currentBytes Current received total bytes + * @return True if continue the request, false will cancel the current request. + */ + typedef std::function + ProgressCallback; + + /** Sets a progress callback */ + void setProgressCallback( const ProgressCallback& progressCallback ); + + /** Get the progress callback */ + const ProgressCallback& getProgressCallback() const; + + /** Cancels the current request if being processed */ + void cancel(); + + /** @return True if the current request was cancelled */ + const bool& isCancelled() const; + + /** @return If requests a compressed response */ + const bool& isCompressedResponse() const; + + /** Set to request a compressed response from the server + ** The returned response will be automatically decompressed + ** by the client. + */ + void setCompressedResponse( const bool& compressedResponse ); + + /** Resumes download if a file is already present */ + void setContinue( const bool& resume ); + + /** @return If must continue a download previously started. */ + const bool& isContinue() const; + + // Types + typedef std::map FieldTable; + + /** @return True if request is verbose logging */ + bool isVerbose() const; + + /** Set verbose logging */ + void setVerbose( bool verbose ); + + private: + friend class Http; + + /** @brief Prepare the final request to send to the server + ** This is used internally by Http before sending the + ** request to the web server. + ** @return String containing the request, ready to be sent */ + std::string prepare( const Http& http ) const; + + /** Prepares a http tunnel request */ + std::string prepareTunnel( const Http& http ); + + // Member data + FieldTable mFields; ///< Fields of the header associated to their value + Method mMethod; ///< Method to use for the request + std::string mUri; ///< Target URI of the request + unsigned int mMajorVersion; ///< Major HTTP version + unsigned int mMinorVersion; ///< Minor HTTP version + std::string mBody; ///< Body of the request + bool mValidateCertificate; ///< Validates the SSL certificate in case of an HTTPS request + bool mValidateHostname; ///< Validates the hostname in case of an HTTPS request + bool mFollowRedirect; ///< Follows redirect response codes + bool mCompressedResponse; ///< Request compressed response + bool mContinue; ///< Resume download + mutable bool mCancel; ///< Cancel state of current request + bool mVerbose{ false }; ///< Enable/Disable verbosity + ProgressCallback mProgressCallback; ///< Progress callback + unsigned int mMaxRedirections; ///< Maximum number of redirections allowed + mutable unsigned int mRedirectionCount; ///< Number of redirections followed by the request + URI mProxy; ///< Proxy information + }; + + /** @brief Default constructor */ + Http(); + + /** @brief Construct the HTTP client with the target host + ** This is equivalent to calling setHost(host, port). + ** The port has a default value of 0, which means that the + ** HTTP client will use the right port according to the + ** protocol used (80 for HTTP, 443 for HTTPS). You should + ** leave it like this unless you really need a port other + ** than the standard one, or use an unknown protocol. + ** @param host Web server to connect to + ** @param port Port to use for connection + ** @param useSSL force the SSL usage ( if compiled with the support of it ). If the host starts + *with https:// it will use it by default. + ** @param proxy Set an http proxy for the host connection + */ + Http( const std::string& host, unsigned short port = 0, bool useSSL = false, + URI proxy = URI() ); + + ~Http(); + + /** @brief Set the target host + ** This function just stores the host address and port, it + ** doesn't actually connect to it until you send a request. + ** The port has a default value of 0, which means that the + ** HTTP client will use the right port according to the + ** protocol used (80 for HTTP, 443 for HTTPS). You should + ** leave it like this unless you really need a port other + ** than the standard one, or use an unknown protocol. + ** @param host Web server to connect to + ** @param port Port to use for connection + ** @param useSSL force the SSL usage ( if compiled with the support of it ). If the host starts + *with https:// it will use it by default. * @param proxy Set an http proxy for the host + *connection + */ + void setHost( const std::string& host, unsigned short port = 0, bool useSSL = false, + URI proxy = URI() ); + + /** @brief Sets the host from an URI (this is the equivalent of calling setHost( + * uri.getHost(), + + * * uri.getPort(), uri.getScheme() == "https" ) ) */ + void setHost( const URI& uri, URI proxy = URI() ); + + /** @brief Send a HTTP request and return the server's response. + ** You must have a valid host before sending a request (see setHost). + ** Any missing mandatory header field in the request will be added + ** with an appropriate value. + ** Warning: this function waits for the server's response and may + ** not return instantly; use a thread if you don't want to block your + ** application, or use a timeout to limit the time to wait. A value + ** of Time::Zero means that the client will use the system default timeout + ** (which is usually pretty long). + ** @param request Request to send + ** @param timeout Maximum time to wait + ** @return Server's response */ + Response sendRequest( const Request& request, Time timeout = Time::Zero ); + + /** @brief Send a HTTP request and writes the server's response to a IOStream file. + ** You must have a valid host before sending a request (see setHost). + ** Any missing mandatory header field in the request will be added + ** with an appropriate value. + ** Warning: this function waits for the server's response and may + ** not return instantly; use a thread if you don't want to block your + ** application, or use a timeout to limit the time to wait. A value + ** of Time::Zero means that the client will use the system default timeout + ** (which is usually pretty long). + ** @param request Request to send + ** @param writeTo The IO stream to write the downloaded content + ** @param timeout Maximum time to wait + ** @return Server's response */ + Response downloadRequest( const Request& request, IOStream& writeTo, + Time timeout = Time::Zero ); + + /** @brief Send a HTTP request and writes the server's response to a file system path. + ** You must have a valid host before sending a request (see setHost). + ** Any missing mandatory header field in the request will be added + ** with an appropriate value. + ** Warning: this function waits for the server's response and may + ** not return instantly; use a thread if you don't want to block your + ** application, or use a timeout to limit the time to wait. A value + ** of Time::Zero means that the client will use the system default timeout + ** (which is usually pretty long). + ** @param request Request to send + ** @param writePath The path of the file to write the downloaded content + ** @param timeout Maximum time to wait + ** @return Server's response */ + Response downloadRequest( const Request& request, std::string writePath, + Time timeout = Time::Zero ); + + /** Definition of the async callback response */ + typedef std::function + AsyncResponseCallback; + + /** @brief Sends the request and creates a new thread, when got the response informs the result + ** to the callback. * This function does not lock the caller thread. + ** @see sendRequest + ** @return Unique Id of the request added */ + Uint64 sendAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, + Time timeout = Time::Zero ); + + /** @brief Sends the request and creates a new thread, when got the response informs the result + *to the callback. * This function does not lock the caller thread. + ** @see downloadRequest + ** @return Unique Id of the request added */ + Uint64 downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, + IOStream& writeTo, Time timeout = Time::Zero ); + + /** @brief Sends the request and creates a new thread, when got the response informs the result + *to the callback. * This function does not lock the caller thread. + ** @see downloadRequest + ** @return Unique Id of the request added */ + Uint64 downloadAsyncRequest( const AsyncResponseCallback& cb, const Http::Request& request, + std::string writePath, Time timeout = Time::Zero ); + + /** @return The host address */ + const IpAddress& getHost() const; + + /** @return The host name */ + const std::string& getHostName() const; + + /** @return The host port */ + const unsigned short& getPort() const; + + /** @return If the HTTP client uses SSL/TLS */ + const bool& isSSL() const; + + /** @return The URI from the schema + hostname + port */ + URI getURI() const; + + /** Sets the request proxy */ + void setProxy( const URI& uri ); + + /** @return The request proxy */ + const URI& getProxy() const; + + /** @return Is a proxy is need to be used */ + bool isProxied() const; + + /** @return If request has been found and canceled */ + bool setCancelRequest( Uint64 reqId ); + + /** Helper class to build the body of a multipart/form-data request. */ + class EE_API MultipartEntitiesBuilder { + public: + MultipartEntitiesBuilder(); + + /** @param boundary The boundary to use in the multipart data. */ + MultipartEntitiesBuilder( const std::string& boundary ); + + /** @returns The corresponding request Content-Type needed. + * This Content-Type header must be set to the request in order to work correctly. + * + * For example: + * @code + * Http::Request request; + * Http::MultipartEntitiesBuilder builder; + * ... + * request.setField( "Content-Type", builder.getContentType() ); + * @endcode + */ + std::string getContentType(); + + /** @return The boundary used to build the multipart data. */ + const std::string& getBoundary() const; + + /** Adds a text multipart form field. */ + void addParameter( const std::string& name, const std::string& value ); + + /** Adds a file to the multipart data. + * @param parameterName The field name. + * @param fileName The file name of the stream. + * @param stream The stream were the file is located and is going to be read. + */ + void addFile( const std::string& parameterName, const std::string& fileName, + IOStream* stream ); + + /** Adds a file to the multipart data. + * @param parameterName The field name. + * @param filePath The local file path. + */ + void addFile( const std::string& parameterName, const std::string& filePath ); + + std::string build(); + + protected: + void buildFilePart( std::ostream& ostream, IOStream* stream, const std::string& fieldName, + const std::string& fileName, const std::string& contentType ); + + void buildTextPart( std::ostream& ostream, const std::string& parameterName, + const std::string& parameterValue ); + + std::string mBoundary; + std::map> mStreamParams; + std::map mFileParams; + std::map mParams; + }; + + /** HTTP Client Pool. + * Will keep the instances of the HTTP clients until the Pool is destroyed. + * Acts as a host client cache. + */ + class EE_API Pool { + public: + /** @returns The reference to the global HTTP Pool + * A global HTTP Pool is created at the program start + */ + static Pool& getGlobal(); + + Pool(); + + ~Pool(); + + /** Clear all the HTTP Clients */ + void clear(); + + /** @return True if the client already exists in the pool + * @param host The scheme + hostname + port represented as an URI. + * @param proxy The client proxy if any, scheme + hostname + post as URI. + */ + bool exists( const URI& host, const URI& proxy = URI() ); + + /** @return An HTTP Client to the host and proxy ( creates one if no one is found ) + * @param host The scheme + hostname + port represented as an URI. + * @param proxy The client proxy if any, scheme + hostname + post as URI. + */ + std::shared_ptr get( const URI& host, const URI& proxy = URI() ); + + protected: + Mutex mMutex; + UnorderedMap> mHttps; + + static std::string getHostKey( const URI& host, const URI& proxy ); + + static String::HashType getHostHash( const URI& host, const URI& proxy ); + }; + + /** Creates an HTTP Request using the global HTTP Client Pool */ + static Response + request( const URI& uri, Request::Method method = Request::Method::Get, + const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), + const std::string& body = "", const bool& validateCertificate = true, + const URI& proxy = URI() ); + + /** Creates an HTTP GET Request using the global HTTP Client Pool */ + static Response + get( const URI& uri, const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", + const bool& validateCertificate = true, const URI& proxy = URI() ); + + /** Creates an HTTP POST Request using the global HTTP Client Pool */ + static Response + post( const URI& uri, const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", + const bool& validateCertificate = true, const URI& proxy = URI() ); + + /** Creates an async HTTP Request using the global HTTP Client Pool */ + static void + requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri, + const Time& timeout = Time::Zero, Request::Method method = Request::Method::Get, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), + const std::string& body = "", const bool& validateCertificate = true, + const URI& proxy = URI() ); + + /** Creates an async HTTP GET Request using the global HTTP Client Pool */ + static void getAsync( + const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", + const bool& validateCertificate = true, const URI& proxy = URI() ); + + /** Creates an async HTTP POST Request using the global HTTP Client Pool */ + static void postAsync( + const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout = Time::Zero, + const Request::ProgressCallback& progressCallback = Request::ProgressCallback(), + const Request::FieldTable& headers = Request::FieldTable(), const std::string& body = "", + const bool& validateCertificate = true, const URI& proxy = URI() ); + + /** It will try to get the proxy from the environment variables. */ + static URI getEnvProxyURI(); + + /** Set the thread pool to consume for async requests, otherwise it will use its own */ + static void setThreadPool( std::shared_ptr pool ); + + private: + class AsyncRequest : public Thread { + public: + static std::atomic IdCounter; + + AsyncRequest( Uint64 id, Http* http, const AsyncResponseCallback& cb, Http::Request request, + Time timeout, bool fromLocalPool ); + + AsyncRequest( Uint64 id, Http* http, const AsyncResponseCallback& cb, Http::Request request, + IOStream& writeTo, Time timeout, bool fromLocalPool ); + + AsyncRequest( Uint64 id, Http* http, const AsyncResponseCallback& cb, Http::Request request, + std::string writePath, Time timeout, bool fromLocalPool ); + + ~AsyncRequest(); + + void run(); + + Uint64 id() const { return mId; } + + void cancel(); + + protected: + friend class Http; + Uint64 mId{ 0 }; + Http* mHttp; + AsyncResponseCallback mCb; + Http::Request mRequest; + Time mTimeout; + bool mRunning; + bool mStreamed; + bool mStreamOwned; + bool mFromLocalPool; + IOStream* mStream; + }; + + class HttpConnection { + public: + HttpConnection(); + + HttpConnection( TcpSocket* socket ); + + ~HttpConnection(); + + void setSocket( TcpSocket* socket ); + + TcpSocket* getSocket() const; + + void disconnect(); + + const bool& isConnected() const; + + void setConnected( const bool& connected ); + + const bool& isTunneled() const; + + void setTunneled( const bool& tunneled ); + + const bool& isSSL() const; + + void setSSL( const bool& ssl ); + + const bool& isKeepAlive() const; + + void setKeepAlive( const bool& isKeepAlive ); + + protected: + TcpSocket* mSocket; + bool mIsConnected; + bool mIsTunneled; + bool mIsSSL; + bool mIsKeepAlive; + }; + + friend class AsyncRequest; + ThreadLocalPtr mConnection; ///< Connection to the host + IpAddress mHost; ///< Web host address + std::string mHostName; ///< Web host name + unsigned short mPort; ///< Port used for connection with host + std::vector mThreads; + Mutex mThreadsMutex; + bool mIsSSL; + bool mHostSolved; + URI mProxy; + Mutex mCurRequestsMutex; + std::unordered_map mCurRequests; + + void removeAsyncRequest( AsyncRequest* req ); + + Request prepareFields( const Http::Request& request ); +}; + +}} // namespace EE::Network + +#endif // EE_NETWORKCHTTP_HPP + +/** +@class EE::Network::Http + +Http is a very simple HTTP client that allows you +to communicate with a web server. You can retrieve +web pages, send data to an interactive resource, +download a remote file, etc. +The HTTP client is split into 3 classes: +@li EE::Network::Http::Request +@li EE::Network::Http::Response +@li EE::Network::Http +EE::Network::Http::Request builds the request that will be +sent to the server. A request is made of: +@li a method (what you want to do) +@li a target URI (usually the name of the web page or file) +@li one or more header fields (options that you can pass to the server) +@li an optional body (for POST requests) +EE::Network::Http::Response parse the response from the web server +and provides getters to read them. The response contains: +@li a status code +@li header fields (that may be answers to the ones that you requested) +@li a body, which contains the contents of the requested resource +Http provides a simple function, sendRequest, to send a +EE::Network::Http::Request and return the corresponding EE::Network::Http::Response +from the server. +Usage example: +@code +// Create a new HTTP client +Http http; + +// We'll work on http://www.google.com +http.setHost( "http://www.google.com" ); + +// Prepare a request to get the 'features.php' page +Http::Request request( "features.php" ); + +// Send the request +Http::Response response = http.sendRequest(request); + +// Check the status code and display the result +Http::Response::Status status = response.getStatus(); +if ( status == Http::Response::Ok ) { + std::cout << response.getBody() << std::endl; +} else { + std::cout << "Error " << status << std::endl; +} +@endcode + +Shorthand methods are also provided: +@code +Http::Response response = Http::get( "http://www.google.com" ); +if ( response.getStatus() == Http::Response::Ok ) { + std::cout << response.getBody() << std::endl; +} else { + std::cout << "Error " << response.getStatus() << std::endl; +} +@endcode + +You can also use the shorthand async alternative method: +@code +Http::getAsync( + [=]( const Http&, Http::Request&, Http::Response& response ) { + if ( response.getStatus() == Http::Response::Ok) { + std::cout << response.getBody() << std::endl; + } else { + std::cout << "Error " << response.getStatus() << std::endl; + } + }, "http://www.google.com" ); +@endcode +*/ diff --git a/include/eepp/system/thread.hpp b/include/eepp/system/thread.hpp index e878070d4..128762691 100644 --- a/include/eepp/system/thread.hpp +++ b/include/eepp/system/thread.hpp @@ -19,7 +19,7 @@ class EE_API Thread : NonCopyable { typedef void ( *FuncType )( void* ); /** @return The current thread id */ - static Uint32 getCurrentThreadId(); + static UintPtr getCurrentThreadId(); /** @brief Construct the thread from a functor with no argument ** This constructor works for function objects, as well diff --git a/include/eepp/window/engine.hpp b/include/eepp/window/engine.hpp index 34b8f2bc3..075c7a49e 100644 --- a/include/eepp/window/engine.hpp +++ b/include/eepp/window/engine.hpp @@ -26,7 +26,7 @@ class EE_API Engine { static bool isEngineRunning(); /** @return The id of the thread that was used to initialize the OpenGL Context. */ - static Uint64 getMainThreadId(); + static UintPtr getMainThreadId(); /** @returns True if the current thread is the main thread. */ static bool isMainThread(); diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index a6145dac5..31a0515d7 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -1,1638 +1,1731 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN -#include -#endif - -using namespace EE::Network::SSL; -using namespace EE::Network::Private; - -namespace EE { namespace Network { - -#define PACKET_BUFFER_SIZE ( 16384 ) - -Http::Request::Method Http::Request::methodFromString( std::string methodString ) { - String::toLowerInPlace( methodString ); - if ( "get" == methodString ) - return Method::Get; - else if ( "head" == methodString ) - return Method::Head; - else if ( "post" == methodString ) - return Method::Post; - else if ( "put" == methodString ) - return Method::Put; - else if ( "delete" == methodString ) - return Method::Delete; - else if ( "options" == methodString ) - return Method::Options; - else if ( "patch" == methodString ) - return Method::Patch; - else if ( "connect" == methodString ) - return Method::Connect; - else - return Method::Get; -} - -std::string Http::Request::methodToString( const Http::Request::Method& method ) { - switch ( method ) { - default: - case Get: - return "GET"; - case Head: - return "HEAD"; - case Post: - return "POST"; - case Put: - return "PUT"; - case Delete: - return "DELETE"; - case Options: - return "OPTIONS"; - case Patch: - return "PATCH"; - case Connect: - return "CONNECT"; - } -} - -Http::Request::Request( const std::string& uri, Method method, const std::string& body, - bool validateCertificate, bool validateHostname, bool followRedirect, - bool compressedResponse ) : - mValidateCertificate( validateCertificate ), - mValidateHostname( validateHostname ), - mFollowRedirect( followRedirect ), - mCompressedResponse( compressedResponse ), - mContinue( false ), - mCancel( false ), - mMaxRedirections( 10 ), - mRedirectionCount( 0 ) { - setMethod( method ); - setUri( uri ); - setHttpVersion( 1, 1 ); - setBody( body ); -} - -void Http::Request::setField( const std::string& field, const std::string& value ) { - mFields[String::toLower( field )] = value; -} - -void Http::Request::setHeader( const std::string& field, const std::string& value ) { - setField( field, value ); -} - -void Http::Request::setMethod( Http::Request::Method method ) { - mMethod = method; -} - -void Http::Request::setUri( const std::string& uri ) { - mUri = uri; - - // Make sure it starts with a '/' - if ( mUri.empty() || ( mUri[0] != '/' ) ) - mUri.insert( 0, "/" ); -} - -void Http::Request::setHttpVersion( unsigned int major, unsigned int minor ) { - mMajorVersion = major; - mMinorVersion = minor; -} - -void Http::Request::setBody( const std::string& body ) { - mBody = body; -} - -void Http::Request::setBody( std::string&& body ) { - mBody = std::move( body ); -} - -void Http::Request::appendBody( const std::string& body ) { - mBody.append( body ); -} - -void Http::Request::appendBody( std::string&& body ) { - mBody.append( std::move( body ) ); -} - -const std::string& Http::Request::getUri() const { - return mUri; -} - -const Http::Request::Method& Http::Request::getMethod() const { - return mMethod; -} - -const bool& Http::Request::getValidateCertificate() const { - return mValidateCertificate; -} - -void Http::Request::setValidateCertificate( bool enable ) { - mValidateCertificate = enable; -} - -const bool& Http::Request::getValidateHostname() const { - return mValidateHostname; -} - -void Http::Request::setValidateHostname( bool enable ) { - mValidateHostname = enable; -} - -const bool& Http::Request::getFollowRedirect() const { - return mFollowRedirect; -} - -void Http::Request::setFollowRedirect( bool follow ) { - mFollowRedirect = follow; -} - -const unsigned int& Http::Request::getMaxRedirects() const { - return mMaxRedirections; -} - -void Http::Request::setMaxRedirects( unsigned int maxRedirects ) { - mMaxRedirections = maxRedirects; -} - -void Http::Request::setProgressCallback( const Http::Request::ProgressCallback& progressCallback ) { - mProgressCallback = progressCallback; -} - -const Http::Request::ProgressCallback& Http::Request::getProgressCallback() const { - return mProgressCallback; -} - -void Http::Request::cancel() { - mCancel = true; -} - -const bool& Http::Request::isCancelled() const { - return mCancel; -} - -std::string Http::Request::prepareTunnel( const Http& http ) { - std::ostringstream out; - - setMethod( Connect ); - - std::string method = methodToString( mMethod ); - - out << method << " " << http.getHostName() << ":" << http.getPort() << " "; - out << "HTTP/" << mMajorVersion << "." << mMinorVersion << "\r\n"; - - setField( "Host", String::format( "%s:%d", http.getHostName().c_str(), http.getPort() ) ); - setField( "Proxy-Connection", "Keep-Alive" ); - setField( "User-Agent", "eepp-network" ); - - for ( FieldTable::const_iterator i = mFields.begin(); i != mFields.end(); ++i ) - out << i->first << ": " << i->second << "\r\n"; - - out << "\r\n"; - - return out.str(); -} - -bool Http::Request::isVerbose() const { - return mVerbose; -} - -void Http::Request::setVerbose( bool verbose ) { - mVerbose = verbose; -} - -void Http::Request::setContinue( const bool& resume ) { - mContinue = resume; -} - -const bool& Http::Request::isContinue() const { - return mContinue; -} - -const bool& Http::Request::isCompressedResponse() const { - return mCompressedResponse; -} - -void Http::Request::setCompressedResponse( const bool& compressedResponse ) { - mCompressedResponse = compressedResponse; -} - -std::string Http::Request::prepare( const Http& http ) const { - std::ostringstream out; - - // Convert the method to its string representation - std::string method = methodToString( mMethod ); - - // Write the first line containing the request type - if ( http.getProxy().empty() ) { - out << method << " " << mUri << " "; - } else { - URI uri = http.getURI(); - uri.setPathEtc( mUri ); - out << method << " " << uri.toString() << " "; - } - - out << "HTTP/" << mMajorVersion << "." << mMinorVersion << "\r\n"; - - // Write fields - for ( FieldTable::const_iterator i = mFields.begin(); i != mFields.end(); ++i ) { - out << i->first << ": " << i->second << "\r\n"; - } - - // Use an extra \r\n to separate the header from the body - out << "\r\n"; - - // Add the body - out << mBody; - - return out.str(); -} - -bool Http::Request::hasField( const std::string& field ) const { - return mFields.find( String::toLower( field ) ) != mFields.end(); -} - -const std::string& Http::Request::getField( const std::string& field ) const { - FieldTable::const_iterator it = mFields.find( String::toLower( field ) ); - if ( it != mFields.end() ) { - return it->second; - } else { - static const std::string empty = ""; - return empty; - } -} - -URI Http::getEnvProxyURI() { - const char* http_proxy = getenv( "http_proxy" ); - URI proxy; - - if ( NULL == http_proxy ) - http_proxy = getenv( "HTTP_PROXY" ); - - if ( NULL != http_proxy ) { - std::string httpProxy; - httpProxy = std::string( http_proxy ); - if ( !httpProxy.empty() && httpProxy.find( "://" ) == std::string::npos ) - httpProxy = "http://" + httpProxy; - proxy = URI( httpProxy ); - } - return proxy; -} - -const char* Http::Response::statusToString( const Http::Response::Status& status ) { - switch ( status ) { - // 2xx: success - case Ok: - return "OK"; - case Created: - return "Created"; - case Accepted: - return "Accepted"; - case NoContent: - return "No Content"; - case ResetContent: - return "Reset Content"; - case PartialContent: - return "Partial Content"; - - // 3xx: redirection - case MultipleChoices: - return "Multiple Choices"; - case MovedPermanently: - return "Moved Permanently"; - case MovedTemporarily: - return "Moved Temporarily"; - case NotModified: - return "Not Modified"; - - // 4xx: client error - case BadRequest: - return "BadRequest"; - case Unauthorized: - return "Unauthorized"; - case Forbidden: - return "Forbidden"; - case NotFound: - return "Not Found"; - case RangeNotSatisfiable: - return "Range Not Satisfiable"; - - // 5xx: server error - case InternalServerError: - return "Internal Server Error"; - case NotImplemented: - return "Not Implemented"; - case BadGateway: - return "Bad Gateway"; - case ServiceNotAvailable: - return "Service Not Available"; - case GatewayTimeout: - return "Gateway Timeout"; - case VersionNotSupported: - return "Version Not Supported"; - - // 10xx: Custom codes - case InvalidResponse: - return "Invalid Response"; - case ConnectionFailed: - return "Connection Failed"; - default: - return ""; - } -} - -Http::Response::Status Http::Response::intAsStatus( const int& value ) { - switch ( value ) { - case Ok: - case Created: - case Accepted: - case NoContent: - case ResetContent: - case PartialContent: - case MultipleChoices: - case MovedPermanently: - case MovedTemporarily: - case NotModified: - case BadRequest: - case Unauthorized: - case Forbidden: - case NotFound: - case RangeNotSatisfiable: - case InternalServerError: - case NotImplemented: - case BadGateway: - case ServiceNotAvailable: - case GatewayTimeout: - case VersionNotSupported: - case InvalidResponse: - case ConnectionFailed: - return (Status)value; - default: - return InternalServerError; - } -} - -Http::Response Http::Response::createFakeResponse( const Http::Response::FieldTable& fields, - Http::Response::Status& status, - const std::string& body, - unsigned int majorVersion, - unsigned int minorVersion ) { - Response response; - response.mStatus = status; - response.mBody = body; - response.mFields = fields; - response.mMajorVersion = majorVersion; - response.mMinorVersion = minorVersion; - return response; -} - -Http::Response::Response() : mStatus( ConnectionFailed ), mMajorVersion( 0 ), mMinorVersion( 0 ) {} - -Http::Response::FieldTable Http::Response::getHeaders() { - return mFields; -} - -const std::string& Http::Response::getField( const std::string& field ) const { - FieldTable::const_iterator it = mFields.find( String::toLower( field ) ); - if ( it != mFields.end() ) { - return it->second; - } else { - static const std::string empty = ""; - return empty; - } -} - -bool Http::Response::hasField( const std::string& field ) const { - return mFields.find( String::toLower( field ) ) != mFields.end(); -} - -Http::Response::Status Http::Response::getStatus() const { - return mStatus; -} - -const char* Http::Response::getStatusDescription() const { - switch ( mStatus ) { - // 2xx: success - case Ok: - return "Successful"; - case Created: - return "The resource has successfully been created"; - case Accepted: - return "The request has been accepted, but will be processed later by the server"; - case NoContent: - return "The server didn't send any data in return"; - case ResetContent: - return "The server informs the client that it should clear the view (form) that caused " - "the request to be sent"; - case PartialContent: - return "The server has sent a part of the resource, as a response to a partial GET " - "request"; - - // 3xx: redirection - case MultipleChoices: - return "The requested page can be accessed from several locations"; - case MovedPermanently: - return "The requested page has permanently moved to a new location"; - case MovedTemporarily: - return "The requested page has temporarily moved to a new location"; - case NotModified: - return "For conditional requests, means the requested page hasn't changed and doesn't " - "need to be refreshed"; - - // 4xx: client error - case BadRequest: - return "The server couldn't understand the request (syntax error)"; - case Unauthorized: - return "The requested page needs an authentication to be accessed"; - case Forbidden: - return "The requested page cannot be accessed at all, even with authentication"; - case NotFound: - return "The requested page doesn't exist"; - case RangeNotSatisfiable: - return "The server can't satisfy the partial GET request (with a \"Range\" header " - "field)"; - - // 5xx: server error - case InternalServerError: - return "The server encountered an unexpected error"; - case NotImplemented: - return "The server doesn't implement a requested feature"; - case BadGateway: - return "The gateway server has received an error from the source server"; - case ServiceNotAvailable: - return "The server is temporarily unavailable (overloaded, in maintenance, ...)"; - case GatewayTimeout: - return "The gateway server couldn't receive a response from the source server"; - case VersionNotSupported: - return "The server doesn't support the requested HTTP version"; - - // 10xx: Custom codes - case InvalidResponse: - return "Response is not a valid HTTP one"; - case ConnectionFailed: - return "Connection with server failed"; - default: - return "Unknown response status"; - } -} - -unsigned int Http::Response::getMajorHttpVersion() const { - return mMajorVersion; -} - -unsigned int Http::Response::getMinorHttpVersion() const { - return mMinorVersion; -} - -const std::string& Http::Response::getBody() const { - return mBody; -} - -void Http::Response::parse( const std::string& data ) { - std::istringstream in( data ); - - // Extract the HTTP version from the first line - std::string version; - - if ( in >> version ) { - if ( ( version.size() >= 8 ) && ( version[6] == '.' ) && - ( String::toLower( version.substr( 0, 5 ) ) == "http/" ) && - std::isdigit( version[5] ) && std::isdigit( version[7] ) ) { - mMajorVersion = version[5] - '0'; - mMinorVersion = version[7] - '0'; - } else { - // Invalid HTTP version - mStatus = InvalidResponse; - return; - } - } - - // Extract the status code from the first line - int status = InvalidResponse; - - if ( in >> status ) { - mStatus = static_cast( status ); - } else { - // Invalid status code - mStatus = InvalidResponse; - return; - } - - // Ignore the end of the first line - in.ignore( std::numeric_limits::max(), '\n' ); - - // Parse the other lines, which contain fields, one by one - parseFields( in ); - - mBody.clear(); -} - -void Http::Response::parseFields( std::istream& in ) { - std::string line; - while ( std::getline( in, line ) && ( line.size() > 2 ) ) { - std::string::size_type pos = line.find( ": " ); - - if ( pos != std::string::npos ) { - // Extract the field name and its value - std::string field = line.substr( 0, pos ); - std::string value = line.substr( pos + 2 ); - - // Remove any trailing \r - if ( !value.empty() && ( *value.rbegin() == '\r' ) ) - value.erase( value.size() - 1 ); - - // Add the field - mFields[String::toLower( field )] = value; - } - } -} - -static Http::Pool sGlobalHttpPool = Http::Pool(); - -static std::shared_ptr sGlobalThreadPool = nullptr; - -Http::Response Http::request( const URI& uri, Request::Method method, const Time& timeout, - const Http::Request::ProgressCallback& progressCallback, - const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { - auto http = sGlobalHttpPool.get( uri, proxy ); - Request request( uri.getPathAndQuery(), method, body, validateCertificate, validateCertificate, - true, true ); - request.setProgressCallback( progressCallback ); - - for ( const auto& field : headers ) - request.setField( field.first, field.second ); - - return http->sendRequest( request, timeout ); -} - -Http::Response Http::get( const URI& uri, const Time& timeout, - const Http::Request::ProgressCallback& progressCallback, - const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { - return request( uri, Request::Method::Get, timeout, progressCallback, headers, body, - validateCertificate, proxy ); -} - -Http::Response Http::post( const URI& uri, const Time& timeout, - const Http::Request::ProgressCallback& progressCallback, - const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { - return request( uri, Request::Method::Post, timeout, progressCallback, headers, body, - validateCertificate, proxy ); -} - -void Http::requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, - Request::Method method, - const Http::Request::ProgressCallback& progressCallback, - const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { - auto http = sGlobalHttpPool.get( uri, proxy ); - Request request( uri.getPathAndQuery(), method, body, validateCertificate, validateCertificate, - true, true ); - request.setProgressCallback( progressCallback ); - - for ( const auto& field : headers ) - request.setField( field.first, field.second ); - - http->sendAsyncRequest( cb, request, timeout ); -} - -void Http::getAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, - const Http::Request::ProgressCallback& progressCallback, - const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { - requestAsync( cb, uri, timeout, Request::Method::Get, progressCallback, headers, body, - validateCertificate, proxy ); -} - -void Http::postAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, - const Http::Request::ProgressCallback& progressCallback, - const Http::Request::FieldTable& headers, const std::string& body, - const bool& validateCertificate, const URI& proxy ) { - requestAsync( cb, uri, timeout, Request::Method::Post, progressCallback, headers, body, - validateCertificate, proxy ); -} - -Http::Http() : mConnection( NULL ), mHost(), mPort( 0 ), mIsSSL( false ), mHostSolved( false ) {} - -Http::Http( const std::string& host, unsigned short port, bool useSSL, URI proxy ) : - mConnection( NULL ), - mHostName( host ), - mPort( port ), - mIsSSL( useSSL ), - mHostSolved( false ), - mProxy( proxy ) { - setHost( host, port, useSSL, proxy ); -} - -Http::~Http() { - // First we wait to finish any request pending - for ( auto&& itt : mThreads ) { - itt->wait(); - } - - for ( auto&& itt : mThreads ) { - eeDelete( itt ); - } - - // Then we destroy the last open connection - HttpConnection* connection = mConnection; - - eeSAFE_DELETE( connection ); -} - -void Http::setHost( const std::string& host, unsigned short port, bool useSSL, URI proxy ) { - mProxy = proxy; - - bool sameHost( host == mHostName && port == mPort && useSSL == mIsSSL ); - - // Check the protocol - if ( String::toLower( host.substr( 0, 7 ) ) == "http://" ) { - // HTTP protocol - mHostName = host.substr( 7 ); - mPort = ( port != 0 ? port : 80 ); - } else if ( String::toLower( host.substr( 0, 8 ) ) == "https://" ) { -// HTTPS protocol -#if defined( EE_SSL_SUPPORT ) || EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - mIsSSL = true; - mHostName = host.substr( 8 ); - mPort = ( port != 0 ? port : 443 ); -#else - mHostName = ""; - mPort = 0; -#endif - } else { - // Undefined protocol - use HTTP, unless SSL is specified - mHostName = host; - mPort = ( port != 0 ? port : 80 ); - -#if defined( EE_SSL_SUPPORT ) || EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - mPort = useSSL ? ( port != 0 ? port : 443 ) : mPort; - mIsSSL = useSSL || mPort == 443; -#endif - } - - // Remove any trailing '/' from the host name - if ( !mHostName.empty() && ( *mHostName.rbegin() == '/' ) ) - mHostName.erase( mHostName.size() - 1 ); - - if ( !mProxy.empty() ) { - sameHost = false; - } - - // If the new host is different to the last set host - // and there's an open connection to the host, we close - // the old connection to prepare a new one. - if ( !sameHost && NULL != mConnection ) { - HttpConnection* connection = mConnection; - eeSAFE_DELETE( connection ); - mConnection = NULL; - } -} - -void Http::setHost( const URI& uri, URI proxy ) { - setHost( uri.getHost(), uri.getPort(), uri.getScheme() == "https" ); -} - -Http::Response Http::sendRequest( const Http::Request& request, Time timeout ) { - IOStreamString stream; - Response response = downloadRequest( request, stream, timeout ); - response.mBody = std::move( stream.getStream() ); - return response; -} - -static bool sendProgress( const Http& http, const Http::Request& request, - const Http::Response& response, const Http::Request::Status& status, - const std::size_t& totalBytes, const std::size_t& currentBytes ) { - if ( request.getProgressCallback() ) - return request.getProgressCallback()( http, request, response, status, totalBytes, - currentBytes ); - return true; -} - -Http::Response Http::downloadRequest( const Http::Request& request, IOStream& writeTo, - Time timeout ) { - // Solve the host IP only when the request starts. - if ( !mHostSolved ) { - if ( !mProxy.empty() ) { - mHost = IpAddress( mProxy.getHost() ); - } else { - mHost = IpAddress( mHostName ); - } - mHostSolved = true; - } - - if ( 0 == mHost.toInteger() ) { - return Response(); - } - - if ( NULL == mConnection ) { - HttpConnection* connection = eeNew( HttpConnection, () ); - TcpSocket* socket = NULL; - - // If the http client is proxied and the end host use SSL - // We need to create an HTTP Tunnel against the proxy server - if ( isProxied() && mIsSSL && SSLSocket::isSupported() ) { - socket = SSLSocket::New( mHostName, request.getValidateCertificate(), - request.getValidateHostname() ); - - connection->setSSL( true ); - } else { - bool isSSL = !isProxied() - ? mIsSSL - : ( SSLSocket::isSupported() && mProxy.getScheme() == "https" ); - - socket = isSSL ? SSLSocket::New( mHostName, request.getValidateCertificate(), - request.getValidateHostname() ) - : TcpSocket::New(); - - connection->setSSL( isSSL ); - } - - if ( timeout != Time::Zero ) { - socket->setReceiveTimeout( timeout ); - socket->setSendTimeout( timeout ); - } - - connection->setSocket( socket ); - - mConnection = connection; - } - - // First make sure that the request is valid -- add missing mandatory fields - Request toSend( prepareFields( request ) ); - - // Prepare the response - Response received; - - // If not connected, try to connect to the server - if ( !mConnection->isConnected() ) { - // We need to create an HTTP Tunnel? - if ( isProxied() && mIsSSL && SSLSocket::isSupported() ) { - SSLSocket* sslSocket = reinterpret_cast( mConnection->getSocket() ); - - // For an HTTP Tunnel first we need to connect to the proxy server ( without TLS ) - if ( sslSocket->tcpConnect( mHost, mProxy.getPort(), timeout ) != Socket::Done ) { - return received; - } else { - mConnection->setConnected( true ); - } - } else { - if ( mConnection->getSocket()->connect( - mHost, mProxy.empty() ? mPort : mProxy.getPort(), timeout ) != Socket::Done ) { - return received; - } else { - mConnection->setConnected( true ); - } - } - - if ( mConnection->isConnected() && - !sendProgress( *this, request, received, Request::Connected, 0, 0 ) ) { - mConnection->disconnect(); - return received; - } - } - - // Connect the socket to the host - if ( mConnection->isConnected() ) { - // Create a HTTP Tunnel for SSL connections if not ready - if ( isProxied() && mIsSSL && !mConnection->isTunneled() ) { - // Create the HTTP Tunnel request - Request tunnelRequest; - std::string tunnelStr = tunnelRequest.prepareTunnel( *this ); - - SSLSocket* sslSocket = reinterpret_cast( mConnection->getSocket() ); - std::size_t sent; - - // Send the request - if ( sslSocket->tcpSend( tunnelStr.c_str(), tunnelStr.size(), sent ) == Socket::Done ) { - char buffer[PACKET_BUFFER_SIZE + 1]; - std::size_t read = 0; - - // Get the proxy server response - if ( sslSocket->tcpReceive( buffer, PACKET_BUFFER_SIZE, read ) == Socket::Done ) { - // Parse the HTTP Tunnel request response - Response tunnelResponse; - std::string header; - header.append( buffer, read ); - tunnelResponse.parse( header ); - - if ( tunnelResponse.getStatus() == Response::Ok ) { - // Stablish the SSL connection if the response is positive - if ( sslSocket->sslConnect( mHost, mProxy.getPort(), timeout ) != - Socket::Done ) { - return received; - } - } else { - return tunnelResponse; - } - } else { - return received; - } - - mConnection->setTunneled( true ); - mConnection->setKeepAlive( true ); - } - } - - if ( request.isContinue() ) { - std::size_t continueLength = writeTo.getSize(); - - if ( continueLength > 0 ) { - IOStreamString responseHeadBody; - Request requestHead = request; - requestHead.setContinue( false ); - requestHead.setMethod( Request::Head ); - Response responseHead = downloadRequest( requestHead, responseHeadBody ); - Uint64 contentLength = 0; - - if ( responseHead.hasField( "Accept-Ranges" ) && - responseHead.hasField( "Content-Length" ) && - String::fromString( contentLength, - responseHead.getField( "Content-Length" ) ) && - contentLength > 0 && continueLength < contentLength ) { - writeTo.seek( continueLength ); - Request newRequest( request ); - newRequest.setContinue( false ); - newRequest.setField( "Range", String::format( "bytes=%lu-%lu", - (unsigned long)continueLength, - (unsigned long)contentLength ) ); - return downloadRequest( newRequest, writeTo, timeout ); - } - } - } - - // Convert the request to string and send it through the connected socket - std::string requestStr = toSend.prepare( *this ); - - if ( request.isVerbose() ) { - std::cout << "Request:" << std::endl; - std::cout << requestStr << std::endl; - } - - if ( !requestStr.empty() ) { - Socket::Status status; - - // Send it through the socket - if ( mConnection->getSocket()->send( requestStr.c_str(), requestStr.size() ) == - Socket::Done ) { - if ( !sendProgress( *this, request, received, Request::Sent, 0, 0 ) ) { - request.mCancel = true; - } - - // Wait for the server's response - std::size_t currentTotalBytes = 0; - std::size_t len = 0; - std::size_t read = 0; - char* eol = NULL; // end of line - char* bol = NULL; // beginning of line - char buffer[PACKET_BUFFER_SIZE + 1]; - bool isnheader = false; - bool chunked = false; - bool compressed = false; - Uint64 contentLength = 0; - std::string headerBuffer; - HttpStreamChunked* chunkedStream = NULL; - IOStreamInflate* inflateStream = NULL; - IOStream* bufferStream = NULL; - - while ( !request.isCancelled() && - ( status = mConnection->getSocket()->receive( buffer, PACKET_BUFFER_SIZE, - read ) ) == Socket::Done ) { - char* readBuffer = buffer; - - // If we didn't receive the header yet, we will try to find the end of the - // header - if ( !isnheader ) { - // calculate combined length of unprocessed data and new data - len += read; - - // NULL terminate buffer for string functions - readBuffer[len] = '\0'; - - // process each line in buffer looking for header break - bol = readBuffer; - - while ( !isnheader && ( eol = strchr( bol, '\n' ) ) != NULL ) { - // update bol based upon the value of eol - bol = eol + 1; - - // test if end of headers has been reached - if ( 0 == strncmp( bol, "\r\n", 2 ) || 0 == strncmp( bol, "\n", 1 ) ) { - // note that end of headers has been reached - isnheader = true; - - // update the value of bol to reflect the beginning of the line - // immediately after the headers - if ( bol[0] != '\n' ) - bol += 1; - - bol += 1; - - // calculate the amount of data remaining in the buffer - len = read - ( bol - readBuffer ); - - // Fill the header buffer - headerBuffer.append( readBuffer, ( bol - readBuffer ) ); - - if ( !headerBuffer.empty() ) { - // Build the Response object from the received data - received.parse( headerBuffer ); - - // Check if the response is chunked - chunked = received.getField( "transfer-encoding" ) == "chunked"; - - // Check if the content is compressed - std::string encoding( received.getField( "content-encoding" ) ); - compressed = encoding == "gzip" || encoding == "deflate"; - - if ( compressed ) { - Compression::Mode compressionMode = - "gzip" == encoding ? Compression::MODE_GZIP - : Compression::MODE_DEFLATE; - - inflateStream = - IOStreamInflate::New( writeTo, compressionMode ); - } - - if ( chunked ) { - IOStream& writeToStream = - compressed ? *inflateStream : writeTo; - chunkedStream = - eeNew( HttpStreamChunked, ( writeToStream ) ); - } - - bufferStream = chunked - ? chunkedStream - : ( compressed ? inflateStream : &writeTo ); - - // Get the content length - if ( !received.getField( "content-length" ).empty() ) { - if ( !String::fromString( - contentLength, - received.getField( "content-length" ) ) ) - contentLength = 0; - } - - if ( received.getField( "connection" ) == "closed" ) { - mConnection->setConnected( false ); - mConnection->setTunneled( false ); - } - - // If a redirection is requested, and requests follows - // redirections, send a new request to the redirection location. - if ( ( received.getStatus() == Response::MovedPermanently || - received.getStatus() == Response::MovedTemporarily ) && - request.getFollowRedirect() ) { - - // Only continue redirecting if less than 10 redirections - // were done - if ( request.mRedirectionCount < - request.getMaxRedirects() ) { - std::string location( received.getField( "location" ) ); - URI uri( location ); - - // Close the connection - if ( !mConnection->isKeepAlive() ) - mConnection->disconnect(); - - eeSAFE_DELETE( chunkedStream ); - eeSAFE_DELETE( inflateStream ); - - Http::Request newRequest( request ); - newRequest.setUri( uri.getPathAndQuery() ); - - request.mRedirectionCount++; - newRequest.mRedirectionCount = - request.mRedirectionCount; - - // Same host, expects a path in the same domain - if ( uri.getHost().empty() || - uri.getHost() == getHost() ) { - return downloadRequest( newRequest, writeTo, - timeout ); - } else { - // New host, we need to solve the host - Http http( uri.getHost(), uri.getPort(), - uri.getScheme() == "https" ? true - : false ); - return http.downloadRequest( newRequest, writeTo, - timeout ); - } - } - } - - if ( !sendProgress( *this, request, received, - Request::HeaderReceived, contentLength, - 0 ) ) { - request.mCancel = true; - } - - // Move the readBuffer to the starting point - // of the file buffer - if ( len > 0 ) { - readBuffer = bol; - read = len; - } else { - read = 0; - } - - headerBuffer.clear(); - } - } - } - - if ( !isnheader ) { - headerBuffer.append( readBuffer, ( bol - readBuffer ) ); - } - } - - if ( isnheader ) { - currentTotalBytes += read; - - if ( read > 0 ) - bufferStream->write( readBuffer, read ); - - if ( !sendProgress( *this, request, received, Request::ContentReceived, - contentLength, currentTotalBytes ) ) { - request.mCancel = true; - break; - } - - // If the response is compressed and the stream ended means that we received - // the message. So we can skip the socket receive call. - if ( ( compressed && NULL != inflateStream && !inflateStream->isOpen() ) || - ( contentLength > 0 && contentLength == currentTotalBytes ) ) { - break; - } - } - } - - if ( chunked && NULL != chunkedStream && - !chunkedStream->getHeaderBuffer().empty() ) { - headerBuffer.append( chunkedStream->getHeaderBuffer() ); - } - - if ( !headerBuffer.empty() ) { - std::istringstream in( headerBuffer ); - received.parseFields( in ); - } - - if ( status == Socket::Status::Disconnected ) { - mConnection->setConnected( false ); - mConnection->setTunneled( false ); - } - - eeSAFE_DELETE( chunkedStream ); - eeSAFE_DELETE( inflateStream ); - } else { - mConnection->setConnected( false ); - mConnection->setTunneled( false ); - } - } - - // Close the connection - if ( !mConnection->isKeepAlive() ) { - mConnection->disconnect(); - - if ( mConnection ) { - HttpConnection* connection = mConnection; - eeSAFE_DELETE( connection ); - mConnection = NULL; - } - } - } - - return received; -} - -Http::Response Http::downloadRequest( const Http::Request& request, std::string writePath, - Time timeout ) { - IOStreamFile file( writePath, request.isContinue() ? "ab+" : "wb+" ); - return downloadRequest( request, file, timeout ); -} - -void Http::setThreadPool( std::shared_ptr pool ) { - sGlobalThreadPool = pool; -} - -Http::AsyncRequest::AsyncRequest( Http* http, const Http::AsyncResponseCallback& cb, - Http::Request request, Time timeout ) : - mHttp( http ), - mCb( cb ), - mRequest( request ), - mTimeout( timeout ), - mRunning( true ), - mStreamed( false ), - mStreamOwned( false ), - mStream( NULL ) {} - -Http::AsyncRequest::AsyncRequest( Http* http, const Http::AsyncResponseCallback& cb, - Http::Request request, IOStream& writeTo, Time timeout ) : - mHttp( http ), - mCb( cb ), - mRequest( request ), - mTimeout( timeout ), - mRunning( true ), - mStreamed( true ), - mStreamOwned( false ), - mStream( &writeTo ) {} - -Http::AsyncRequest::AsyncRequest( Http* http, const Http::AsyncResponseCallback& cb, - Http::Request request, std::string writePath, Time timeout ) : - mHttp( http ), - mCb( cb ), - mRequest( request ), - mTimeout( timeout ), - mRunning( true ), - mStreamed( true ), - mStreamOwned( true ), - mStream( IOStreamFile::New( writePath, "wb" ) ) {} - -Http::AsyncRequest::~AsyncRequest() { - if ( mStreamOwned ) - eeSAFE_DELETE( mStream ); -} - -void Http::AsyncRequest::run() { - Http::Response response = mStreamed ? mHttp->downloadRequest( mRequest, *mStream, mTimeout ) - : mHttp->sendRequest( mRequest, mTimeout ); - - mCb( *mHttp, mRequest, response ); - - if ( mStreamed && mStreamOwned ) { - eeSAFE_DELETE( mStream ); - } - - mRunning = false; -} - -void Http::removeOldThreads() { - std::vector remove; - - for ( AsyncRequest* ar : mThreads ) { - if ( ar->mRunning ) - continue; - // We need to be sure, since the state is set in the thread, this will not block the - // thread anyway - ar->wait(); - - eeDelete( ar ); - - remove.push_back( ar ); - } - - for ( auto rem : remove ) { - auto found = std::find( mThreads.begin(), mThreads.end(), rem ); - if ( found != mThreads.end() ) - mThreads.erase( found ); - } -} - -Http::Request Http::prepareFields( const Http::Request& request ) { - Request toSend( request ); - - if ( !toSend.hasField( "User-Agent" ) ) - toSend.setField( "User-Agent", "eepp-network" ); - - if ( !toSend.hasField( "Accept" ) ) - toSend.setField( "Accept", "*/*" ); - - if ( !toSend.hasField( "Host" ) ) { - toSend.setField( - "Host", - mHostName + ( mPort != 80 && mPort != 443 ? ":" + String::toString( mPort ) : "" ) ); - } - - if ( !toSend.hasField( "Content-Length" ) && toSend.mBody.size() > 0 ) { - std::ostringstream out; - out << toSend.mBody.size(); - toSend.setField( "Content-Length", out.str() ); - } - - if ( ( toSend.mMethod == Request::Post ) && !toSend.hasField( "Content-Type" ) ) - toSend.setField( "Content-Type", "application/x-www-form-urlencoded" ); - - if ( ( toSend.mMajorVersion * 10 + toSend.mMinorVersion >= 11 ) && - !toSend.hasField( "Connection" ) && ( !mConnection || !mConnection->isKeepAlive() ) ) { - toSend.setField( "Connection", "close" ); - } - - if ( !mProxy.empty() ) { - toSend.setField( "Accept", "*/*" ); - - if ( mIsSSL ) { - toSend.setField( "Proxy-connection", "keep-alive" ); - } else { - toSend.setField( "Proxy-connection", "close" ); - } - } - - if ( request.isCompressedResponse() ) - toSend.setField( "Accept-Encoding", "gzip, deflate" ); - - return toSend; -} - -void Http::setProxy( const URI& uri ) { - setHost( mHostName, mPort, mIsSSL, uri ); -} - -const URI& Http::getProxy() const { - return mProxy; -} - -bool Http::isProxied() const { - return !mProxy.empty(); -} - -#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN -struct WGetAsyncRequest { - Http* http; - Http::Request request; - Http::AsyncResponseCallback cb; - IOStream* writeTo{ nullptr }; -}; - -void emscripten_async_wget2_got_data( unsigned, void* vwget, void* buffer, unsigned bufferSize ) { - WGetAsyncRequest* wget = reinterpret_cast( vwget ); - Http::Response::Status status = Http::Response::Status::Ok; - if ( wget->writeTo ) { - wget->writeTo->write( (const char*)buffer, bufferSize ); - Http::Response response = - Http::Response::createFakeResponse( Http::Response::FieldTable(), status, "" ); - wget->cb( *wget->http, wget->request, response ); - } else { - std::string responseBody; - responseBody.insert( 0, (const char*)buffer, bufferSize ); - Http::Response response = Http::Response::createFakeResponse( Http::Response::FieldTable(), - status, responseBody ); - wget->cb( *wget->http, wget->request, response ); - } - delete wget; -} - -void emscripten_async_wget2_got_file( unsigned int, void* vwget, const char* ) { - WGetAsyncRequest* wget = reinterpret_cast( vwget ); - Http::Response::Status status = Http::Response::Status::Ok; - Http::Response response = - Http::Response::createFakeResponse( Http::Response::FieldTable(), status, "" ); - wget->cb( *wget->http, wget->request, response ); - delete wget; -} - -void emscripten_async_wget2_got_error_data( unsigned, void* vwget, int errorCode, - const char* errorDescription ) { - WGetAsyncRequest* wget = reinterpret_cast( vwget ); - std::string responseBody; - Http::Response::Status status = Http::Response::intAsStatus( errorCode ); - Http::Response response = - Http::Response::createFakeResponse( Http::Response::FieldTable(), status, responseBody ); - wget->cb( *wget->http, wget->request, response ); - delete wget; -} - -void emscripten_async_wget2_got_error_file( unsigned int, void* vwget, int errorCode ) { - WGetAsyncRequest* wget = reinterpret_cast( vwget ); - std::string responseBody; - Http::Response::Status status = Http::Response::intAsStatus( errorCode ); - Http::Response response = - Http::Response::createFakeResponse( Http::Response::FieldTable(), status, responseBody ); - wget->cb( *wget->http, wget->request, response ); - delete wget; -} -#endif - -void Http::sendAsyncRequest( const Http::AsyncResponseCallback& cb, const Http::Request& request, - Time timeout ) { -#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - WGetAsyncRequest* wget = new WGetAsyncRequest(); - wget->http = this; - wget->cb = cb; - wget->request = Http::Request( request ); - emscripten_async_wget2_data( ( getURI().toString() + request.getUri() ).c_str(), - Request::methodToString( request.getMethod() ).c_str(), - URI( request.getUri() ).getQuery().c_str(), wget, 1, - emscripten_async_wget2_got_data, - emscripten_async_wget2_got_error_data, NULL ); -#else - if ( sGlobalThreadPool ) { - sGlobalThreadPool->run( [this, cb, request, timeout] { - AsyncRequest asyncRequest( this, cb, request, timeout ); - asyncRequest.run(); - } ); - return; - } - AsyncRequest* thread = eeNew( AsyncRequest, ( this, cb, request, timeout ) ); - thread->launch(); - Lock l( mThreadsMutex ); - removeOldThreads(); - mThreads.push_back( thread ); -#endif -} - -void Http::downloadAsyncRequest( const Http::AsyncResponseCallback& cb, - const Http::Request& request, IOStream& writeTo, Time timeout ) { -#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - WGetAsyncRequest* wget = new WGetAsyncRequest(); - wget->http = this; - wget->cb = cb; - wget->writeTo = &writeTo; - wget->request = Http::Request( request ); - emscripten_async_wget2_data( ( getURI().toString() + request.getUri() ).c_str(), - Request::methodToString( request.getMethod() ).c_str(), - URI( request.getUri() ).getQuery().c_str(), wget, 1, - emscripten_async_wget2_got_data, - emscripten_async_wget2_got_error_data, NULL ); -#else - if ( sGlobalThreadPool ) { - sGlobalThreadPool->run( [this, cb, request, &writeTo, timeout] { - AsyncRequest asyncRequest( this, cb, request, writeTo, timeout ); - asyncRequest.run(); - } ); - return; - } - AsyncRequest* thread = eeNew( AsyncRequest, ( this, cb, request, writeTo, timeout ) ); - thread->launch(); - Lock l( mThreadsMutex ); - removeOldThreads(); - mThreads.push_back( thread ); -#endif -} - -void Http::downloadAsyncRequest( const Http::AsyncResponseCallback& cb, - const Http::Request& request, std::string writePath, - Time timeout ) { -#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - WGetAsyncRequest* wget = new WGetAsyncRequest(); - wget->http = this; - wget->cb = cb; - wget->request = Http::Request( request ); - emscripten_async_wget2( ( getURI().toString() + request.getUri() ).c_str(), writePath.c_str(), - Request::methodToString( request.getMethod() ).c_str(), - URI( request.getUri() ).getQuery().c_str(), wget, - emscripten_async_wget2_got_file, emscripten_async_wget2_got_error_file, - NULL ); -#else - if ( sGlobalThreadPool ) { - sGlobalThreadPool->run( [this, cb, request, writePath, timeout] { - AsyncRequest asyncRequest( this, cb, request, writePath, timeout ); - asyncRequest.run(); - } ); - return; - } - AsyncRequest* thread = eeNew( AsyncRequest, ( this, cb, request, writePath, timeout ) ); - thread->launch(); - Lock l( mThreadsMutex ); - removeOldThreads(); - mThreads.push_back( thread ); -#endif -} - -const IpAddress& Http::getHost() const { - return mHost; -} - -const std::string& Http::getHostName() const { - return mHostName; -} - -const unsigned short& Http::getPort() const { - return mPort; -} - -const bool& Http::isSSL() const { - return mIsSSL; -} - -URI Http::getURI() const { - return URI( - String::format( "%s://%s:%d", mIsSSL ? "https" : "http", mHostName.c_str(), mPort ) ); -} - -Http::HttpConnection::HttpConnection() : - mSocket( NULL ), - mIsConnected( false ), - mIsTunneled( false ), - mIsSSL( false ), - mIsKeepAlive( false ) {} - -Http::HttpConnection::HttpConnection( TcpSocket* socket ) : - mSocket( socket ), mIsConnected( false ), mIsTunneled( false ), mIsSSL( false ) {} - -Http::HttpConnection::~HttpConnection() { - eeSAFE_DELETE( mSocket ); -} - -void Http::HttpConnection::setSocket( TcpSocket* socket ) { - mSocket = socket; -} - -TcpSocket* Http::HttpConnection::getSocket() const { - return mSocket; -} - -void Http::HttpConnection::disconnect() { - if ( NULL != mSocket ) - mSocket->disconnect(); - - mIsConnected = false; -} - -const bool& Http::HttpConnection::isConnected() const { - return mIsConnected; -} - -void Http::HttpConnection::setConnected( const bool& connected ) { - mIsConnected = connected; -} - -const bool& Http::HttpConnection::isTunneled() const { - return mIsTunneled; -} - -void Http::HttpConnection::setTunneled( const bool& tunneled ) { - mIsTunneled = tunneled; -} - -const bool& Http::HttpConnection::isSSL() const { - return mIsSSL; -} - -void Http::HttpConnection::setSSL( const bool& ssl ) { - mIsSSL = ssl; -} - -const bool& Http::HttpConnection::isKeepAlive() const { - return mIsKeepAlive; -} - -void Http::HttpConnection::setKeepAlive( const bool& isKeepAlive ) { - mIsKeepAlive = isKeepAlive; -} - -Http::Pool& Http::Pool::getGlobal() { - return sGlobalHttpPool; -} - -Http::Pool::Pool() {} - -Http::Pool::~Pool() { - clear(); -} - -void Http::Pool::clear() { - Lock l( mMutex ); - mHttps.clear(); -} - -std::string Http::Pool::getHostKey( const URI& host, const URI& proxy ) { - return proxy.empty() ? host.getSchemeAndAuthority() - : String::format( "%s-%s", host.getSchemeAndAuthority().c_str(), - proxy.getSchemeAndAuthority().c_str() ); -} - -String::HashType Http::Pool::getHostHash( const URI& host, const URI& proxy ) { - return String::hash( Http::Pool::getHostKey( host, proxy ) ); -} - -bool Http::Pool::exists( const URI& host, const URI& proxy ) { - Lock l( mMutex ); - return mHttps.find( getHostHash( host, proxy ) ) != mHttps.end(); -} - -std::shared_ptr Http::Pool::get( const URI& host, const URI& proxy ) { - { - Lock l( mMutex ); - auto hostInstance = mHttps.find( Http::Pool::getHostHash( host, proxy ) ); - - if ( hostInstance != mHttps.end() ) { - return hostInstance->second; - } - } - - auto http = - std::make_shared( host.getHost(), host.getPort(), host.getScheme() == "https" ); - Lock l( mMutex ); - mHttps[getHostHash( host, proxy )] = http; - return http; -} - -static constexpr const char* TWO_HYPHENS = "--"; -static constexpr const char* LINE_END = "\r\n"; - -Http::MultipartEntitiesBuilder::MultipartEntitiesBuilder() : - MultipartEntitiesBuilder( "eepp-client-boundary-" + - String::toString( (Uint64)Sys::getSystemTime() ) ) {} - -Http::MultipartEntitiesBuilder::MultipartEntitiesBuilder( const std::string& boundary ) : - mBoundary( boundary ) {} - -std::string Http::MultipartEntitiesBuilder::getContentType() { - return "multipart/form-data;boundary=" + getBoundary(); -} - -const std::string& Http::MultipartEntitiesBuilder::getBoundary() const { - return mBoundary; -} - -void Http::MultipartEntitiesBuilder::addParameter( const std::string& name, - const std::string& value ) { - mParams[name] = value; -} - -void Http::MultipartEntitiesBuilder::addFile( const std::string& parameterName, - const std::string& fileName, IOStream* stream ) { - auto pair = std::make_pair( fileName, stream ); - - mStreamParams[parameterName] = pair; -} - -void Http::MultipartEntitiesBuilder::addFile( const std::string& parameterName, - const std::string& filePath ) { - mFileParams[parameterName] = filePath; -} - -std::string Http::MultipartEntitiesBuilder::build() { - std::ostringstream ostream; - - for ( auto& file : mStreamParams ) { - buildFilePart( ostream, file.second.second, file.first, file.second.first, "" ); - } - - for ( auto& file : mFileParams ) { - IOStreamFile f( file.second ); - buildFilePart( ostream, &f, file.first, FileSystem::fileNameFromPath( file.second ), "" ); - } - - for ( auto& text : mParams ) { - buildTextPart( ostream, text.first, text.second ); - } - - ostream << TWO_HYPHENS << getBoundary() << TWO_HYPHENS << LINE_END; - - return ostream.str(); -} - -void Http::MultipartEntitiesBuilder::buildFilePart( std::ostream& ostream, IOStream* stream, - const std::string& fieldName, - const std::string& fileName, - const std::string& contentType ) { - size_t initialPos = stream->tell(); - stream->seek( 0 ); - int bytesAvailable = stream->getSize(); - int maxBufferSize = 1024 * 1024; - int bufferSize = eemin( bytesAvailable, maxBufferSize ); - TScopedBuffer buffer( bufferSize ); - - ostream << TWO_HYPHENS << getBoundary() << LINE_END; - ostream << "Content-Disposition: form-data; name=\"" << fieldName << "\"; filename=\"" - << fileName << "\"" << LINE_END; - ostream << "Content-Transfer-Encoding: binary" << LINE_END; - ostream << "Content-Length: " << bytesAvailable << LINE_END; - if ( !contentType.empty() ) { - ostream << "Content-Type: " << contentType << LINE_END; - } - ostream << LINE_END; - - // read file and write it into form... - int bytesRead = stream->read( buffer.get(), bufferSize ); - - while ( bytesRead > 0 ) { - ostream.write( buffer.get(), bufferSize ); - bytesAvailable -= bytesRead; - bufferSize = eemin( bytesAvailable, maxBufferSize ); - bytesRead = stream->read( buffer.get(), bufferSize ); - } - - ostream << LINE_END; - stream->seek( initialPos ); -} - -void Http::MultipartEntitiesBuilder::buildTextPart( std::ostream& ostream, - const std::string& parameterName, - const std::string& parameterValue ) { - ostream << TWO_HYPHENS << getBoundary() << LINE_END; - ostream << "Content-Disposition: form-data; name=\"" << parameterName << "\"" << LINE_END; - ostream << "Content-Type: text/plain; charset=UTF-8" << LINE_END; - ostream << LINE_END; - ostream << parameterValue; - ostream << LINE_END; -} - -}} // namespace EE::Network +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN +#include +#endif + +using namespace EE::Network::SSL; +using namespace EE::Network::Private; + +namespace EE { namespace Network { + +#define PACKET_BUFFER_SIZE ( 16384 ) + +Http::Request::Method Http::Request::methodFromString( std::string methodString ) { + String::toLowerInPlace( methodString ); + if ( "get" == methodString ) + return Method::Get; + else if ( "head" == methodString ) + return Method::Head; + else if ( "post" == methodString ) + return Method::Post; + else if ( "put" == methodString ) + return Method::Put; + else if ( "delete" == methodString ) + return Method::Delete; + else if ( "options" == methodString ) + return Method::Options; + else if ( "patch" == methodString ) + return Method::Patch; + else if ( "connect" == methodString ) + return Method::Connect; + else + return Method::Get; +} + +std::string Http::Request::methodToString( const Http::Request::Method& method ) { + switch ( method ) { + default: + case Get: + return "GET"; + case Head: + return "HEAD"; + case Post: + return "POST"; + case Put: + return "PUT"; + case Delete: + return "DELETE"; + case Options: + return "OPTIONS"; + case Patch: + return "PATCH"; + case Connect: + return "CONNECT"; + } +} + +Http::Request::Request( const std::string& uri, Method method, const std::string& body, + bool validateCertificate, bool validateHostname, bool followRedirect, + bool compressedResponse ) : + mValidateCertificate( validateCertificate ), + mValidateHostname( validateHostname ), + mFollowRedirect( followRedirect ), + mCompressedResponse( compressedResponse ), + mContinue( false ), + mCancel( false ), + mMaxRedirections( 10 ), + mRedirectionCount( 0 ) { + setMethod( method ); + setUri( uri ); + setHttpVersion( 1, 1 ); + setBody( body ); +} + +void Http::Request::setField( const std::string& field, const std::string& value ) { + mFields[String::toLower( field )] = value; +} + +void Http::Request::setHeader( const std::string& field, const std::string& value ) { + setField( field, value ); +} + +void Http::Request::setMethod( Http::Request::Method method ) { + mMethod = method; +} + +void Http::Request::setUri( const std::string& uri ) { + mUri = uri; + + // Make sure it starts with a '/' + if ( mUri.empty() || ( mUri[0] != '/' ) ) + mUri.insert( 0, "/" ); +} + +void Http::Request::setHttpVersion( unsigned int major, unsigned int minor ) { + mMajorVersion = major; + mMinorVersion = minor; +} + +void Http::Request::setBody( const std::string& body ) { + mBody = body; +} + +void Http::Request::setBody( std::string&& body ) { + mBody = std::move( body ); +} + +void Http::Request::appendBody( const std::string& body ) { + mBody.append( body ); +} + +void Http::Request::appendBody( std::string&& body ) { + mBody.append( std::move( body ) ); +} + +const std::string& Http::Request::getUri() const { + return mUri; +} + +const Http::Request::Method& Http::Request::getMethod() const { + return mMethod; +} + +const bool& Http::Request::getValidateCertificate() const { + return mValidateCertificate; +} + +void Http::Request::setValidateCertificate( bool enable ) { + mValidateCertificate = enable; +} + +const bool& Http::Request::getValidateHostname() const { + return mValidateHostname; +} + +void Http::Request::setValidateHostname( bool enable ) { + mValidateHostname = enable; +} + +const bool& Http::Request::getFollowRedirect() const { + return mFollowRedirect; +} + +void Http::Request::setFollowRedirect( bool follow ) { + mFollowRedirect = follow; +} + +const unsigned int& Http::Request::getMaxRedirects() const { + return mMaxRedirections; +} + +void Http::Request::setMaxRedirects( unsigned int maxRedirects ) { + mMaxRedirections = maxRedirects; +} + +void Http::Request::setProgressCallback( const Http::Request::ProgressCallback& progressCallback ) { + mProgressCallback = progressCallback; +} + +const Http::Request::ProgressCallback& Http::Request::getProgressCallback() const { + return mProgressCallback; +} + +void Http::Request::cancel() { + mCancel = true; + setProgressCallback( {} ); +} + +const bool& Http::Request::isCancelled() const { + return mCancel; +} + +std::string Http::Request::prepareTunnel( const Http& http ) { + std::ostringstream out; + + setMethod( Connect ); + + std::string method = methodToString( mMethod ); + + out << method << " " << http.getHostName() << ":" << http.getPort() << " "; + out << "HTTP/" << mMajorVersion << "." << mMinorVersion << "\r\n"; + + setField( "Host", String::format( "%s:%d", http.getHostName().c_str(), http.getPort() ) ); + setField( "Proxy-Connection", "Keep-Alive" ); + setField( "User-Agent", "eepp-network" ); + + for ( FieldTable::const_iterator i = mFields.begin(); i != mFields.end(); ++i ) + out << i->first << ": " << i->second << "\r\n"; + + out << "\r\n"; + + return out.str(); +} + +bool Http::Request::isVerbose() const { + return mVerbose; +} + +void Http::Request::setVerbose( bool verbose ) { + mVerbose = verbose; +} + +void Http::Request::setContinue( const bool& resume ) { + mContinue = resume; +} + +const bool& Http::Request::isContinue() const { + return mContinue; +} + +const bool& Http::Request::isCompressedResponse() const { + return mCompressedResponse; +} + +void Http::Request::setCompressedResponse( const bool& compressedResponse ) { + mCompressedResponse = compressedResponse; +} + +std::string Http::Request::prepare( const Http& http ) const { + std::ostringstream out; + + // Convert the method to its string representation + std::string method = methodToString( mMethod ); + + // Write the first line containing the request type + if ( http.getProxy().empty() ) { + out << method << " " << mUri << " "; + } else { + URI uri = http.getURI(); + uri.setPathEtc( mUri ); + out << method << " " << uri.toString() << " "; + } + + out << "HTTP/" << mMajorVersion << "." << mMinorVersion << "\r\n"; + + // Write fields + for ( FieldTable::const_iterator i = mFields.begin(); i != mFields.end(); ++i ) { + out << i->first << ": " << i->second << "\r\n"; + } + + // Use an extra \r\n to separate the header from the body + out << "\r\n"; + + // Add the body + out << mBody; + + return out.str(); +} + +bool Http::Request::hasField( const std::string& field ) const { + return mFields.find( String::toLower( field ) ) != mFields.end(); +} + +const std::string& Http::Request::getField( const std::string& field ) const { + FieldTable::const_iterator it = mFields.find( String::toLower( field ) ); + if ( it != mFields.end() ) { + return it->second; + } else { + static const std::string empty = ""; + return empty; + } +} + +URI Http::getEnvProxyURI() { + const char* http_proxy = getenv( "http_proxy" ); + URI proxy; + + if ( NULL == http_proxy ) + http_proxy = getenv( "HTTP_PROXY" ); + + if ( NULL != http_proxy ) { + std::string httpProxy; + httpProxy = std::string( http_proxy ); + if ( !httpProxy.empty() && httpProxy.find( "://" ) == std::string::npos ) + httpProxy = "http://" + httpProxy; + proxy = URI( httpProxy ); + } + return proxy; +} + +const char* Http::Response::statusToString( const Http::Response::Status& status ) { + switch ( status ) { + // 2xx: success + case Ok: + return "OK"; + case Created: + return "Created"; + case Accepted: + return "Accepted"; + case NoContent: + return "No Content"; + case ResetContent: + return "Reset Content"; + case PartialContent: + return "Partial Content"; + + // 3xx: redirection + case MultipleChoices: + return "Multiple Choices"; + case MovedPermanently: + return "Moved Permanently"; + case MovedTemporarily: + return "Moved Temporarily"; + case NotModified: + return "Not Modified"; + + // 4xx: client error + case BadRequest: + return "BadRequest"; + case Unauthorized: + return "Unauthorized"; + case Forbidden: + return "Forbidden"; + case NotFound: + return "Not Found"; + case RangeNotSatisfiable: + return "Range Not Satisfiable"; + + // 5xx: server error + case InternalServerError: + return "Internal Server Error"; + case NotImplemented: + return "Not Implemented"; + case BadGateway: + return "Bad Gateway"; + case ServiceNotAvailable: + return "Service Not Available"; + case GatewayTimeout: + return "Gateway Timeout"; + case VersionNotSupported: + return "Version Not Supported"; + + // 10xx: Custom codes + case InvalidResponse: + return "Invalid Response"; + case ConnectionFailed: + return "Connection Failed"; + default: + return ""; + } +} + +Http::Response::Status Http::Response::intAsStatus( const int& value ) { + switch ( value ) { + case Ok: + case Created: + case Accepted: + case NoContent: + case ResetContent: + case PartialContent: + case MultipleChoices: + case MovedPermanently: + case MovedTemporarily: + case NotModified: + case BadRequest: + case Unauthorized: + case Forbidden: + case NotFound: + case RangeNotSatisfiable: + case InternalServerError: + case NotImplemented: + case BadGateway: + case ServiceNotAvailable: + case GatewayTimeout: + case VersionNotSupported: + case InvalidResponse: + case ConnectionFailed: + return (Status)value; + default: + return InternalServerError; + } +} + +Http::Response Http::Response::createFakeResponse( const Http::Response::FieldTable& fields, + Http::Response::Status& status, + const std::string& body, + unsigned int majorVersion, + unsigned int minorVersion ) { + Response response; + response.mStatus = status; + response.mBody = body; + response.mFields = fields; + response.mMajorVersion = majorVersion; + response.mMinorVersion = minorVersion; + return response; +} + +Http::Response::Response() : mStatus( ConnectionFailed ), mMajorVersion( 0 ), mMinorVersion( 0 ) {} + +Http::Response::FieldTable Http::Response::getHeaders() { + return mFields; +} + +const std::string& Http::Response::getField( const std::string& field ) const { + FieldTable::const_iterator it = mFields.find( String::toLower( field ) ); + if ( it != mFields.end() ) { + return it->second; + } else { + static const std::string empty = ""; + return empty; + } +} + +bool Http::Response::hasField( const std::string& field ) const { + return mFields.find( String::toLower( field ) ) != mFields.end(); +} + +Http::Response::Status Http::Response::getStatus() const { + return mStatus; +} + +const char* Http::Response::getStatusDescription() const { + switch ( mStatus ) { + // 2xx: success + case Ok: + return "Successful"; + case Created: + return "The resource has successfully been created"; + case Accepted: + return "The request has been accepted, but will be processed later by the server"; + case NoContent: + return "The server didn't send any data in return"; + case ResetContent: + return "The server informs the client that it should clear the view (form) that caused " + "the request to be sent"; + case PartialContent: + return "The server has sent a part of the resource, as a response to a partial GET " + "request"; + + // 3xx: redirection + case MultipleChoices: + return "The requested page can be accessed from several locations"; + case MovedPermanently: + return "The requested page has permanently moved to a new location"; + case MovedTemporarily: + return "The requested page has temporarily moved to a new location"; + case NotModified: + return "For conditional requests, means the requested page hasn't changed and doesn't " + "need to be refreshed"; + + // 4xx: client error + case BadRequest: + return "The server couldn't understand the request (syntax error)"; + case Unauthorized: + return "The requested page needs an authentication to be accessed"; + case Forbidden: + return "The requested page cannot be accessed at all, even with authentication"; + case NotFound: + return "The requested page doesn't exist"; + case RangeNotSatisfiable: + return "The server can't satisfy the partial GET request (with a \"Range\" header " + "field)"; + + // 5xx: server error + case InternalServerError: + return "The server encountered an unexpected error"; + case NotImplemented: + return "The server doesn't implement a requested feature"; + case BadGateway: + return "The gateway server has received an error from the source server"; + case ServiceNotAvailable: + return "The server is temporarily unavailable (overloaded, in maintenance, ...)"; + case GatewayTimeout: + return "The gateway server couldn't receive a response from the source server"; + case VersionNotSupported: + return "The server doesn't support the requested HTTP version"; + + // 10xx: Custom codes + case InvalidResponse: + return "Response is not a valid HTTP one"; + case ConnectionFailed: + return "Connection with server failed"; + default: + return "Unknown response status"; + } +} + +unsigned int Http::Response::getMajorHttpVersion() const { + return mMajorVersion; +} + +unsigned int Http::Response::getMinorHttpVersion() const { + return mMinorVersion; +} + +const std::string& Http::Response::getBody() const { + return mBody; +} + +void Http::Response::parse( const std::string& data ) { + std::istringstream in( data ); + + // Extract the HTTP version from the first line + std::string version; + + if ( in >> version ) { + if ( ( version.size() >= 8 ) && ( version[6] == '.' ) && + ( String::toLower( version.substr( 0, 5 ) ) == "http/" ) && + std::isdigit( version[5] ) && std::isdigit( version[7] ) ) { + mMajorVersion = version[5] - '0'; + mMinorVersion = version[7] - '0'; + } else { + // Invalid HTTP version + mStatus = InvalidResponse; + return; + } + } + + // Extract the status code from the first line + int status = InvalidResponse; + + if ( in >> status ) { + mStatus = static_cast( status ); + } else { + // Invalid status code + mStatus = InvalidResponse; + return; + } + + // Ignore the end of the first line + in.ignore( std::numeric_limits::max(), '\n' ); + + // Parse the other lines, which contain fields, one by one + parseFields( in ); + + mBody.clear(); +} + +void Http::Response::parseFields( std::istream& in ) { + std::string line; + while ( std::getline( in, line ) && ( line.size() > 2 ) ) { + std::string::size_type pos = line.find( ": " ); + + if ( pos != std::string::npos ) { + // Extract the field name and its value + std::string field = line.substr( 0, pos ); + std::string value = line.substr( pos + 2 ); + + // Remove any trailing \r + if ( !value.empty() && ( *value.rbegin() == '\r' ) ) + value.erase( value.size() - 1 ); + + // Add the field + mFields[String::toLower( field )] = value; + } + } +} + +static Http::Pool sGlobalHttpPool = Http::Pool(); + +static std::shared_ptr sGlobalThreadPool = nullptr; + +Http::Response Http::request( const URI& uri, Request::Method method, const Time& timeout, + const Http::Request::ProgressCallback& progressCallback, + const Http::Request::FieldTable& headers, const std::string& body, + const bool& validateCertificate, const URI& proxy ) { + auto http = sGlobalHttpPool.get( uri, proxy ); + Request request( uri.getPathAndQuery(), method, body, validateCertificate, validateCertificate, + true, true ); + request.setProgressCallback( progressCallback ); + + for ( const auto& field : headers ) + request.setField( field.first, field.second ); + + return http->sendRequest( request, timeout ); +} + +Http::Response Http::get( const URI& uri, const Time& timeout, + const Http::Request::ProgressCallback& progressCallback, + const Http::Request::FieldTable& headers, const std::string& body, + const bool& validateCertificate, const URI& proxy ) { + return request( uri, Request::Method::Get, timeout, progressCallback, headers, body, + validateCertificate, proxy ); +} + +Http::Response Http::post( const URI& uri, const Time& timeout, + const Http::Request::ProgressCallback& progressCallback, + const Http::Request::FieldTable& headers, const std::string& body, + const bool& validateCertificate, const URI& proxy ) { + return request( uri, Request::Method::Post, timeout, progressCallback, headers, body, + validateCertificate, proxy ); +} + +void Http::requestAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, + Request::Method method, + const Http::Request::ProgressCallback& progressCallback, + const Http::Request::FieldTable& headers, const std::string& body, + const bool& validateCertificate, const URI& proxy ) { + auto http = sGlobalHttpPool.get( uri, proxy ); + Request request( uri.getPathAndQuery(), method, body, validateCertificate, validateCertificate, + true, true ); + request.setProgressCallback( progressCallback ); + + for ( const auto& field : headers ) + request.setField( field.first, field.second ); + + http->sendAsyncRequest( cb, request, timeout ); +} + +void Http::getAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, + const Http::Request::ProgressCallback& progressCallback, + const Http::Request::FieldTable& headers, const std::string& body, + const bool& validateCertificate, const URI& proxy ) { + requestAsync( cb, uri, timeout, Request::Method::Get, progressCallback, headers, body, + validateCertificate, proxy ); +} + +void Http::postAsync( const Http::AsyncResponseCallback& cb, const URI& uri, const Time& timeout, + const Http::Request::ProgressCallback& progressCallback, + const Http::Request::FieldTable& headers, const std::string& body, + const bool& validateCertificate, const URI& proxy ) { + requestAsync( cb, uri, timeout, Request::Method::Post, progressCallback, headers, body, + validateCertificate, proxy ); +} + +Http::Http() : mConnection( NULL ), mHost(), mPort( 0 ), mIsSSL( false ), mHostSolved( false ) {} + +Http::Http( const std::string& host, unsigned short port, bool useSSL, URI proxy ) : + mConnection( NULL ), + mHostName( host ), + mPort( port ), + mIsSSL( useSSL ), + mHostSolved( false ), + mProxy( proxy ) { + setHost( host, port, useSSL, proxy ); +} + +Http::~Http() { + + { + Lock l( mThreadsMutex ); + // First we wait to finish any request pending + for ( auto&& itt : mThreads ) { + itt->cancel(); + itt->wait(); + } + } + + { + Lock l( mThreadsMutex ); + for ( auto&& itt : mThreads ) { + eeDelete( itt ); + } + } + + { + Lock l( mCurRequestsMutex ); + for ( auto [_, req] : mCurRequests ) + req->cancel(); + } + + // Then we destroy the last open connection + HttpConnection* connection = mConnection; + + eeSAFE_DELETE( connection ); +} + +void Http::setHost( const std::string& host, unsigned short port, bool useSSL, URI proxy ) { + mProxy = proxy; + + bool sameHost( host == mHostName && port == mPort && useSSL == mIsSSL ); + + // Check the protocol + if ( String::toLower( host.substr( 0, 7 ) ) == "http://" ) { + // HTTP protocol + mHostName = host.substr( 7 ); + mPort = ( port != 0 ? port : 80 ); + } else if ( String::toLower( host.substr( 0, 8 ) ) == "https://" ) { +// HTTPS protocol +#if defined( EE_SSL_SUPPORT ) || EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + mIsSSL = true; + mHostName = host.substr( 8 ); + mPort = ( port != 0 ? port : 443 ); +#else + mHostName = ""; + mPort = 0; +#endif + } else { + // Undefined protocol - use HTTP, unless SSL is specified + mHostName = host; + mPort = ( port != 0 ? port : 80 ); + +#if defined( EE_SSL_SUPPORT ) || EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + mPort = useSSL ? ( port != 0 ? port : 443 ) : mPort; + mIsSSL = useSSL || mPort == 443; +#endif + } + + // Remove any trailing '/' from the host name + if ( !mHostName.empty() && ( *mHostName.rbegin() == '/' ) ) + mHostName.erase( mHostName.size() - 1 ); + + if ( !mProxy.empty() ) { + sameHost = false; + } + + // If the new host is different to the last set host + // and there's an open connection to the host, we close + // the old connection to prepare a new one. + if ( !sameHost && NULL != mConnection ) { + HttpConnection* connection = mConnection; + eeSAFE_DELETE( connection ); + mConnection = NULL; + } +} + +void Http::setHost( const URI& uri, URI proxy ) { + setHost( uri.getHost(), uri.getPort(), uri.getScheme() == "https" ); +} + +Http::Response Http::sendRequest( const Http::Request& request, Time timeout ) { + IOStreamString stream; + Response response = downloadRequest( request, stream, timeout ); + response.mBody = std::move( stream.getStream() ); + return response; +} + +static bool sendProgress( const Http& http, const Http::Request& request, + const Http::Response& response, const Http::Request::Status& status, + const std::size_t& totalBytes, const std::size_t& currentBytes ) { + if ( request.getProgressCallback() ) + return request.getProgressCallback()( http, request, response, status, totalBytes, + currentBytes ); + return true; +} + +Http::Response Http::downloadRequest( const Http::Request& request, IOStream& writeTo, + Time timeout ) { + // Solve the host IP only when the request starts. + if ( !mHostSolved ) { + if ( !mProxy.empty() ) { + mHost = IpAddress( mProxy.getHost() ); + } else { + mHost = IpAddress( mHostName ); + } + mHostSolved = true; + } + + if ( 0 == mHost.toInteger() ) { + return Response(); + } + + if ( NULL == mConnection ) { + HttpConnection* connection = eeNew( HttpConnection, () ); + TcpSocket* socket = NULL; + + // If the http client is proxied and the end host use SSL + // We need to create an HTTP Tunnel against the proxy server + if ( isProxied() && mIsSSL && SSLSocket::isSupported() ) { + socket = SSLSocket::New( mHostName, request.getValidateCertificate(), + request.getValidateHostname() ); + + connection->setSSL( true ); + } else { + bool isSSL = !isProxied() + ? mIsSSL + : ( SSLSocket::isSupported() && mProxy.getScheme() == "https" ); + + socket = isSSL ? SSLSocket::New( mHostName, request.getValidateCertificate(), + request.getValidateHostname() ) + : TcpSocket::New(); + + connection->setSSL( isSSL ); + } + + if ( timeout != Time::Zero ) { + socket->setReceiveTimeout( timeout ); + socket->setSendTimeout( timeout ); + } + + connection->setSocket( socket ); + + mConnection = connection; + } + + // First make sure that the request is valid -- add missing mandatory fields + Request toSend( prepareFields( request ) ); + + // Prepare the response + Response received; + + if ( request.isCancelled() ) + return Response(); + + // If not connected, try to connect to the server + if ( mConnection && !mConnection->isConnected() ) { + // We need to create an HTTP Tunnel? + if ( isProxied() && mIsSSL && SSLSocket::isSupported() ) { + SSLSocket* sslSocket = reinterpret_cast( mConnection->getSocket() ); + + // For an HTTP Tunnel first we need to connect to the proxy server ( without TLS ) + if ( sslSocket->tcpConnect( mHost, mProxy.getPort(), timeout ) != Socket::Done ) { + return received; + } else { + mConnection->setConnected( true ); + } + } else { + if ( mConnection->getSocket()->connect( + mHost, mProxy.empty() ? mPort : mProxy.getPort(), timeout ) != Socket::Done ) { + return received; + } else { + mConnection->setConnected( true ); + } + } + + if ( mConnection->isConnected() && + !sendProgress( *this, request, received, Request::Connected, 0, 0 ) ) { + mConnection->disconnect(); + return received; + } + } + + // Connect the socket to the host + if ( mConnection && mConnection->isConnected() ) { + // Create a HTTP Tunnel for SSL connections if not ready + if ( isProxied() && mIsSSL && !mConnection->isTunneled() ) { + // Create the HTTP Tunnel request + Request tunnelRequest; + std::string tunnelStr = tunnelRequest.prepareTunnel( *this ); + + SSLSocket* sslSocket = reinterpret_cast( mConnection->getSocket() ); + std::size_t sent; + + // Send the request + if ( sslSocket->tcpSend( tunnelStr.c_str(), tunnelStr.size(), sent ) == Socket::Done ) { + char buffer[PACKET_BUFFER_SIZE + 1]; + std::size_t read = 0; + + // Get the proxy server response + if ( sslSocket->tcpReceive( buffer, PACKET_BUFFER_SIZE, read ) == Socket::Done ) { + // Parse the HTTP Tunnel request response + Response tunnelResponse; + std::string header; + header.append( buffer, read ); + tunnelResponse.parse( header ); + + if ( tunnelResponse.getStatus() == Response::Ok ) { + // Stablish the SSL connection if the response is positive + if ( sslSocket->sslConnect( mHost, mProxy.getPort(), timeout ) != + Socket::Done ) { + return received; + } + } else { + return tunnelResponse; + } + } else { + return received; + } + + mConnection->setTunneled( true ); + mConnection->setKeepAlive( true ); + } + } + + if ( request.isContinue() ) { + std::size_t continueLength = writeTo.getSize(); + + if ( continueLength > 0 ) { + IOStreamString responseHeadBody; + Request requestHead = request; + requestHead.setContinue( false ); + requestHead.setMethod( Request::Head ); + Response responseHead = downloadRequest( requestHead, responseHeadBody ); + Uint64 contentLength = 0; + + if ( responseHead.hasField( "Accept-Ranges" ) && + responseHead.hasField( "Content-Length" ) && + String::fromString( contentLength, + responseHead.getField( "Content-Length" ) ) && + contentLength > 0 && continueLength < contentLength ) { + writeTo.seek( continueLength ); + Request newRequest( request ); + newRequest.setContinue( false ); + newRequest.setField( "Range", String::format( "bytes=%lu-%lu", + (unsigned long)continueLength, + (unsigned long)contentLength ) ); + return downloadRequest( newRequest, writeTo, timeout ); + } + } + } + + // Convert the request to string and send it through the connected socket + std::string requestStr = toSend.prepare( *this ); + + if ( request.isVerbose() ) { + std::cout << "Request:" << std::endl; + std::cout << requestStr << std::endl; + } + + if ( !requestStr.empty() ) { + Socket::Status status; + + // Send it through the socket + if ( mConnection && mConnection->getSocket()->send( + requestStr.c_str(), requestStr.size() ) == Socket::Done ) { + if ( !request.isCancelled() && + !sendProgress( *this, request, received, Request::Sent, 0, 0 ) ) { + request.mCancel = true; + } + + // Wait for the server's response + std::size_t currentTotalBytes = 0; + std::size_t len = 0; + std::size_t read = 0; + char* eol = NULL; // end of line + char* bol = NULL; // beginning of line + char buffer[PACKET_BUFFER_SIZE + 1]; + bool isnheader = false; + bool chunked = false; + bool compressed = false; + Uint64 contentLength = 0; + std::string headerBuffer; + HttpStreamChunked* chunkedStream = NULL; + IOStreamInflate* inflateStream = NULL; + IOStream* bufferStream = NULL; + + while ( !request.isCancelled() && mConnection && + ( status = mConnection->getSocket()->receive( buffer, PACKET_BUFFER_SIZE, + read ) ) == Socket::Done ) { + char* readBuffer = buffer; + + // If we didn't receive the header yet, we will try to find the end of the + // header + if ( !isnheader ) { + // calculate combined length of unprocessed data and new data + len += read; + + // NULL terminate buffer for string functions + readBuffer[len] = '\0'; + + // process each line in buffer looking for header break + bol = readBuffer; + + while ( !isnheader && ( eol = strchr( bol, '\n' ) ) != NULL ) { + // update bol based upon the value of eol + bol = eol + 1; + + // test if end of headers has been reached + if ( 0 == strncmp( bol, "\r\n", 2 ) || 0 == strncmp( bol, "\n", 1 ) ) { + // note that end of headers has been reached + isnheader = true; + + // update the value of bol to reflect the beginning of the line + // immediately after the headers + if ( bol[0] != '\n' ) + bol += 1; + + bol += 1; + + // calculate the amount of data remaining in the buffer + len = read - ( bol - readBuffer ); + + // Fill the header buffer + headerBuffer.append( readBuffer, ( bol - readBuffer ) ); + + if ( !headerBuffer.empty() ) { + // Build the Response object from the received data + received.parse( headerBuffer ); + + // Check if the response is chunked + chunked = received.getField( "transfer-encoding" ) == "chunked"; + + // Check if the content is compressed + std::string encoding( received.getField( "content-encoding" ) ); + compressed = encoding == "gzip" || encoding == "deflate"; + + if ( compressed ) { + Compression::Mode compressionMode = + "gzip" == encoding ? Compression::MODE_GZIP + : Compression::MODE_DEFLATE; + + inflateStream = + IOStreamInflate::New( writeTo, compressionMode ); + } + + if ( chunked ) { + IOStream& writeToStream = + compressed ? *inflateStream : writeTo; + chunkedStream = + eeNew( HttpStreamChunked, ( writeToStream ) ); + } + + bufferStream = chunked + ? chunkedStream + : ( compressed ? inflateStream : &writeTo ); + + // Get the content length + if ( !received.getField( "content-length" ).empty() ) { + if ( !String::fromString( + contentLength, + received.getField( "content-length" ) ) ) + contentLength = 0; + } + + if ( mConnection && + received.getField( "connection" ) == "closed" ) { + mConnection->setConnected( false ); + mConnection->setTunneled( false ); + } + + // If a redirection is requested, and requests follows + // redirections, send a new request to the redirection location. + if ( ( received.getStatus() == Response::MovedPermanently || + received.getStatus() == Response::MovedTemporarily ) && + request.getFollowRedirect() ) { + + // Only continue redirecting if less than 10 redirections + // were done + if ( request.mRedirectionCount < + request.getMaxRedirects() ) { + std::string location( received.getField( "location" ) ); + URI uri( location ); + + // Close the connection + if ( mConnection && !mConnection->isKeepAlive() ) + mConnection->disconnect(); + + eeSAFE_DELETE( chunkedStream ); + eeSAFE_DELETE( inflateStream ); + + Http::Request newRequest( request ); + newRequest.setUri( uri.getPathAndQuery() ); + + request.mRedirectionCount++; + newRequest.mRedirectionCount = + request.mRedirectionCount; + + // Same host, expects a path in the same domain + if ( uri.getHost().empty() || + uri.getHost() == getHost() ) { + return downloadRequest( newRequest, writeTo, + timeout ); + } else { + // New host, we need to solve the host + Http http( uri.getHost(), uri.getPort(), + uri.getScheme() == "https" ? true + : false ); + return http.downloadRequest( newRequest, writeTo, + timeout ); + } + } + } + + if ( !request.isCancelled() && + !sendProgress( *this, request, received, + Request::HeaderReceived, contentLength, + 0 ) ) { + request.mCancel = true; + } + + // Move the readBuffer to the starting point + // of the file buffer + if ( len > 0 ) { + readBuffer = bol; + read = len; + } else { + read = 0; + } + + headerBuffer.clear(); + } + } + } + + if ( !isnheader ) { + headerBuffer.append( readBuffer, ( bol - readBuffer ) ); + } + } + + if ( isnheader ) { + currentTotalBytes += read; + + if ( read > 0 && !request.isCancelled() ) + bufferStream->write( readBuffer, read ); + + if ( !request.isCancelled() && + !sendProgress( *this, request, received, Request::ContentReceived, + contentLength, currentTotalBytes ) ) { + request.mCancel = true; + break; + } + + // If the response is compressed and the stream ended means that we received + // the message. So we can skip the socket receive call. + if ( ( compressed && NULL != inflateStream && !inflateStream->isOpen() ) || + ( contentLength > 0 && contentLength == currentTotalBytes ) ) { + break; + } + } + } + + if ( chunked && NULL != chunkedStream && + !chunkedStream->getHeaderBuffer().empty() ) { + headerBuffer.append( chunkedStream->getHeaderBuffer() ); + } + + if ( !headerBuffer.empty() ) { + std::istringstream in( headerBuffer ); + received.parseFields( in ); + } + + if ( mConnection && status == Socket::Status::Disconnected ) { + mConnection->setConnected( false ); + mConnection->setTunneled( false ); + } + + eeSAFE_DELETE( chunkedStream ); + eeSAFE_DELETE( inflateStream ); + } else if ( mConnection ) { + mConnection->setConnected( false ); + mConnection->setTunneled( false ); + } + } + + // Close the connection + if ( mConnection && !mConnection->isKeepAlive() ) { + mConnection->disconnect(); + + if ( mConnection ) { + HttpConnection* connection = mConnection; + eeSAFE_DELETE( connection ); + mConnection = NULL; + } + } + } + + return received; +} + +Http::Response Http::downloadRequest( const Http::Request& request, std::string writePath, + Time timeout ) { + IOStreamFile file( writePath, request.isContinue() ? "ab+" : "wb+" ); + return downloadRequest( request, file, timeout ); +} + +void Http::setThreadPool( std::shared_ptr pool ) { + sGlobalThreadPool = pool; +} + +std::atomic Http::AsyncRequest::IdCounter = 1; + +Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncResponseCallback& cb, + Http::Request request, Time timeout, bool fromLocalPool ) : + mId( id ), + mHttp( http ), + mCb( cb ), + mRequest( request ), + mTimeout( timeout ), + mRunning( true ), + mStreamed( false ), + mStreamOwned( false ), + mFromLocalPool( fromLocalPool ), + mStream( NULL ) {} + +Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncResponseCallback& cb, + Http::Request request, IOStream& writeTo, Time timeout, + bool fromLocalPool ) : + mId( id ), + mHttp( http ), + mCb( cb ), + mRequest( request ), + mTimeout( timeout ), + mRunning( true ), + mStreamed( true ), + mStreamOwned( false ), + mFromLocalPool( fromLocalPool ), + mStream( &writeTo ) {} + +Http::AsyncRequest::AsyncRequest( Uint64 id, Http* http, const Http::AsyncResponseCallback& cb, + Http::Request request, std::string writePath, Time timeout, + bool fromLocalPool ) : + mId( id ), + mHttp( http ), + mCb( cb ), + mRequest( request ), + mTimeout( timeout ), + mRunning( true ), + mStreamed( true ), + mStreamOwned( true ), + mFromLocalPool( fromLocalPool ), + mStream( IOStreamFile::New( writePath, "wb" ) ) {} + +Http::AsyncRequest::~AsyncRequest() { + if ( mStreamOwned ) + eeSAFE_DELETE( mStream ); +} + +void Http::AsyncRequest::cancel() { + mRequest.cancel(); +} + +void Http::AsyncRequest::run() { + Http::Response response = mStreamed ? mHttp->downloadRequest( mRequest, *mStream, mTimeout ) + : mHttp->sendRequest( mRequest, mTimeout ); + + if ( !mRequest.isCancelled() ) + mCb( *mHttp, mRequest, response ); + + if ( mStreamed && mStreamOwned ) { + eeSAFE_DELETE( mStream ); + } + + mRunning = false; + + if ( mFromLocalPool ) { + mHttp->removeAsyncRequest( this ); + auto me = this; + eeSAFE_DELETE( me ); + } +} + +void Http::removeAsyncRequest( AsyncRequest* req ) { + { + Lock l( mCurRequestsMutex ); + mCurRequests.erase( req->id() ); + } + + { + Lock l( mThreadsMutex ); + auto found = std::find( mThreads.begin(), mThreads.end(), req ); + if ( found != mThreads.end() ) + mThreads.erase( found ); + } +} + +Http::Request Http::prepareFields( const Http::Request& request ) { + Request toSend( request ); + + if ( !toSend.hasField( "User-Agent" ) ) + toSend.setField( "User-Agent", "eepp-network" ); + + if ( !toSend.hasField( "Accept" ) ) + toSend.setField( "Accept", "*/*" ); + + if ( !toSend.hasField( "Host" ) ) { + toSend.setField( + "Host", + mHostName + ( mPort != 80 && mPort != 443 ? ":" + String::toString( mPort ) : "" ) ); + } + + if ( !toSend.hasField( "Content-Length" ) && toSend.mBody.size() > 0 ) { + std::ostringstream out; + out << toSend.mBody.size(); + toSend.setField( "Content-Length", out.str() ); + } + + if ( ( toSend.mMethod == Request::Post ) && !toSend.hasField( "Content-Type" ) ) + toSend.setField( "Content-Type", "application/x-www-form-urlencoded" ); + + if ( ( toSend.mMajorVersion * 10 + toSend.mMinorVersion >= 11 ) && + !toSend.hasField( "Connection" ) && ( !mConnection || !mConnection->isKeepAlive() ) ) { + toSend.setField( "Connection", "close" ); + } + + if ( !mProxy.empty() ) { + toSend.setField( "Accept", "*/*" ); + + if ( mIsSSL ) { + toSend.setField( "Proxy-connection", "keep-alive" ); + } else { + toSend.setField( "Proxy-connection", "close" ); + } + } + + if ( request.isCompressedResponse() ) + toSend.setField( "Accept-Encoding", "gzip, deflate" ); + + return toSend; +} + +void Http::setProxy( const URI& uri ) { + setHost( mHostName, mPort, mIsSSL, uri ); +} + +const URI& Http::getProxy() const { + return mProxy; +} + +bool Http::isProxied() const { + return !mProxy.empty(); +} + +bool Http::setCancelRequest( Uint64 reqId ) { + Lock l( mCurRequestsMutex ); + auto found = mCurRequests.find( reqId ); + if ( found != mCurRequests.end() ) { + found->second->cancel(); + return true; + } + return false; +} + +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN +struct WGetAsyncRequest { + Http* http; + Http::Request request; + Http::AsyncResponseCallback cb; + IOStream* writeTo{ nullptr }; +}; + +void emscripten_async_wget2_got_data( unsigned, void* vwget, void* buffer, unsigned bufferSize ) { + WGetAsyncRequest* wget = reinterpret_cast( vwget ); + Http::Response::Status status = Http::Response::Status::Ok; + if ( wget->writeTo ) { + wget->writeTo->write( (const char*)buffer, bufferSize ); + Http::Response response = + Http::Response::createFakeResponse( Http::Response::FieldTable(), status, "" ); + wget->cb( *wget->http, wget->request, response ); + } else { + std::string responseBody; + responseBody.insert( 0, (const char*)buffer, bufferSize ); + Http::Response response = Http::Response::createFakeResponse( Http::Response::FieldTable(), + status, responseBody ); + wget->cb( *wget->http, wget->request, response ); + } + delete wget; +} + +void emscripten_async_wget2_got_file( unsigned int, void* vwget, const char* ) { + WGetAsyncRequest* wget = reinterpret_cast( vwget ); + Http::Response::Status status = Http::Response::Status::Ok; + Http::Response response = + Http::Response::createFakeResponse( Http::Response::FieldTable(), status, "" ); + wget->cb( *wget->http, wget->request, response ); + delete wget; +} + +void emscripten_async_wget2_got_error_data( unsigned, void* vwget, int errorCode, + const char* errorDescription ) { + WGetAsyncRequest* wget = reinterpret_cast( vwget ); + std::string responseBody; + Http::Response::Status status = Http::Response::intAsStatus( errorCode ); + Http::Response response = + Http::Response::createFakeResponse( Http::Response::FieldTable(), status, responseBody ); + wget->cb( *wget->http, wget->request, response ); + delete wget; +} + +void emscripten_async_wget2_got_error_file( unsigned int, void* vwget, int errorCode ) { + WGetAsyncRequest* wget = reinterpret_cast( vwget ); + std::string responseBody; + Http::Response::Status status = Http::Response::intAsStatus( errorCode ); + Http::Response response = + Http::Response::createFakeResponse( Http::Response::FieldTable(), status, responseBody ); + wget->cb( *wget->http, wget->request, response ); + delete wget; +} +#endif + +Uint64 Http::sendAsyncRequest( const Http::AsyncResponseCallback& cb, const Http::Request& request, + Time timeout ) { + Uint64 id = Http::AsyncRequest::IdCounter.fetch_add( 1, std::memory_order_relaxed ); +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + WGetAsyncRequest* wget = new WGetAsyncRequest(); + wget->http = this; + wget->cb = cb; + wget->request = Http::Request( request ); + emscripten_async_wget2_data( ( getURI().toString() + request.getUri() ).c_str(), + Request::methodToString( request.getMethod() ).c_str(), + URI( request.getUri() ).getQuery().c_str(), wget, 1, + emscripten_async_wget2_got_data, + emscripten_async_wget2_got_error_data, NULL ); + return id; +#else + if ( sGlobalThreadPool ) { + sGlobalThreadPool->run( [this, cb, request, timeout, id] { + AsyncRequest asyncRequest( id, this, cb, request, timeout, false ); + { + Lock l( mCurRequestsMutex ); + mCurRequests[id] = &asyncRequest; + } + asyncRequest.run(); + { + Lock l( mCurRequestsMutex ); + mCurRequests.erase( id ); + } + } ); + return id; + } + AsyncRequest* thread = eeNew( AsyncRequest, ( id, this, cb, request, timeout, true ) ); + { + Lock l( mCurRequestsMutex ); + mCurRequests[id] = thread; + } + thread->launch(); + { + Lock l( mThreadsMutex ); + mThreads.push_back( thread ); + } + return id; +#endif +} + +Uint64 Http::downloadAsyncRequest( const Http::AsyncResponseCallback& cb, + const Http::Request& request, IOStream& writeTo, Time timeout ) { + Uint64 id = Http::AsyncRequest::IdCounter.fetch_add( 1, std::memory_order_relaxed ); +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + WGetAsyncRequest* wget = new WGetAsyncRequest(); + wget->http = this; + wget->cb = cb; + wget->writeTo = &writeTo; + wget->request = Http::Request( request ); + emscripten_async_wget2_data( ( getURI().toString() + request.getUri() ).c_str(), + Request::methodToString( request.getMethod() ).c_str(), + URI( request.getUri() ).getQuery().c_str(), wget, 1, + emscripten_async_wget2_got_data, + emscripten_async_wget2_got_error_data, NULL ); + return id; +#else + if ( sGlobalThreadPool ) { + sGlobalThreadPool->run( [this, cb, request, &writeTo, timeout, id] { + AsyncRequest asyncRequest( id, this, cb, request, writeTo, timeout, false ); + { + Lock l( mCurRequestsMutex ); + mCurRequests[id] = &asyncRequest; + } + asyncRequest.run(); + { + Lock l( mCurRequestsMutex ); + mCurRequests.erase( id ); + } + } ); + return id; + } + AsyncRequest* thread = eeNew( AsyncRequest, ( id, this, cb, request, writeTo, timeout, true ) ); + { + Lock l( mCurRequestsMutex ); + mCurRequests[id] = thread; + } + thread->launch(); + { + Lock l( mThreadsMutex ); + mThreads.push_back( thread ); + } + return id; +#endif +} + +Uint64 Http::downloadAsyncRequest( const Http::AsyncResponseCallback& cb, + const Http::Request& request, std::string writePath, + Time timeout ) { + Uint64 id = Http::AsyncRequest::IdCounter.fetch_add( 1, std::memory_order_relaxed ); +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + WGetAsyncRequest* wget = new WGetAsyncRequest(); + wget->http = this; + wget->cb = cb; + wget->request = Http::Request( request ); + emscripten_async_wget2( ( getURI().toString() + request.getUri() ).c_str(), writePath.c_str(), + Request::methodToString( request.getMethod() ).c_str(), + URI( request.getUri() ).getQuery().c_str(), wget, + emscripten_async_wget2_got_file, emscripten_async_wget2_got_error_file, + NULL ); +#else + if ( sGlobalThreadPool ) { + sGlobalThreadPool->run( [this, cb, request, writePath, timeout, id] { + AsyncRequest asyncRequest( id, this, cb, request, writePath, timeout, false ); + { + Lock l( mCurRequestsMutex ); + mCurRequests[id] = &asyncRequest; + } + asyncRequest.run(); + { + Lock l( mCurRequestsMutex ); + mCurRequests.erase( id ); + } + } ); + return id; + } + AsyncRequest* thread = + eeNew( AsyncRequest, ( id, this, cb, request, writePath, timeout, true ) ); + { + Lock l( mCurRequestsMutex ); + mCurRequests[id] = thread; + } + thread->launch(); + { + Lock l( mThreadsMutex ); + mThreads.push_back( thread ); + } + return id; +#endif +} + +const IpAddress& Http::getHost() const { + return mHost; +} + +const std::string& Http::getHostName() const { + return mHostName; +} + +const unsigned short& Http::getPort() const { + return mPort; +} + +const bool& Http::isSSL() const { + return mIsSSL; +} + +URI Http::getURI() const { + return URI( + String::format( "%s://%s:%d", mIsSSL ? "https" : "http", mHostName.c_str(), mPort ) ); +} + +Http::HttpConnection::HttpConnection() : + mSocket( NULL ), + mIsConnected( false ), + mIsTunneled( false ), + mIsSSL( false ), + mIsKeepAlive( false ) {} + +Http::HttpConnection::HttpConnection( TcpSocket* socket ) : + mSocket( socket ), mIsConnected( false ), mIsTunneled( false ), mIsSSL( false ) {} + +Http::HttpConnection::~HttpConnection() { + eeSAFE_DELETE( mSocket ); +} + +void Http::HttpConnection::setSocket( TcpSocket* socket ) { + mSocket = socket; +} + +TcpSocket* Http::HttpConnection::getSocket() const { + return mSocket; +} + +void Http::HttpConnection::disconnect() { + if ( NULL != mSocket ) + mSocket->disconnect(); + + mIsConnected = false; +} + +const bool& Http::HttpConnection::isConnected() const { + return mIsConnected; +} + +void Http::HttpConnection::setConnected( const bool& connected ) { + mIsConnected = connected; +} + +const bool& Http::HttpConnection::isTunneled() const { + return mIsTunneled; +} + +void Http::HttpConnection::setTunneled( const bool& tunneled ) { + mIsTunneled = tunneled; +} + +const bool& Http::HttpConnection::isSSL() const { + return mIsSSL; +} + +void Http::HttpConnection::setSSL( const bool& ssl ) { + mIsSSL = ssl; +} + +const bool& Http::HttpConnection::isKeepAlive() const { + return mIsKeepAlive; +} + +void Http::HttpConnection::setKeepAlive( const bool& isKeepAlive ) { + mIsKeepAlive = isKeepAlive; +} + +Http::Pool& Http::Pool::getGlobal() { + return sGlobalHttpPool; +} + +Http::Pool::Pool() {} + +Http::Pool::~Pool() { + clear(); +} + +void Http::Pool::clear() { + Lock l( mMutex ); + mHttps.clear(); +} + +std::string Http::Pool::getHostKey( const URI& host, const URI& proxy ) { + return proxy.empty() ? host.getSchemeAndAuthority() + : String::format( "%s-%s", host.getSchemeAndAuthority().c_str(), + proxy.getSchemeAndAuthority().c_str() ); +} + +String::HashType Http::Pool::getHostHash( const URI& host, const URI& proxy ) { + return String::hash( Http::Pool::getHostKey( host, proxy ) ); +} + +bool Http::Pool::exists( const URI& host, const URI& proxy ) { + Lock l( mMutex ); + return mHttps.find( getHostHash( host, proxy ) ) != mHttps.end(); +} + +std::shared_ptr Http::Pool::get( const URI& host, const URI& proxy ) { + { + Lock l( mMutex ); + auto hostInstance = mHttps.find( Http::Pool::getHostHash( host, proxy ) ); + + if ( hostInstance != mHttps.end() ) { + return hostInstance->second; + } + } + + auto http = + std::make_shared( host.getHost(), host.getPort(), host.getScheme() == "https" ); + Lock l( mMutex ); + mHttps[getHostHash( host, proxy )] = http; + return http; +} + +static constexpr const char* TWO_HYPHENS = "--"; +static constexpr const char* LINE_END = "\r\n"; + +Http::MultipartEntitiesBuilder::MultipartEntitiesBuilder() : + MultipartEntitiesBuilder( "eepp-client-boundary-" + + String::toString( (Uint64)Sys::getSystemTime() ) ) {} + +Http::MultipartEntitiesBuilder::MultipartEntitiesBuilder( const std::string& boundary ) : + mBoundary( boundary ) {} + +std::string Http::MultipartEntitiesBuilder::getContentType() { + return "multipart/form-data;boundary=" + getBoundary(); +} + +const std::string& Http::MultipartEntitiesBuilder::getBoundary() const { + return mBoundary; +} + +void Http::MultipartEntitiesBuilder::addParameter( const std::string& name, + const std::string& value ) { + mParams[name] = value; +} + +void Http::MultipartEntitiesBuilder::addFile( const std::string& parameterName, + const std::string& fileName, IOStream* stream ) { + auto pair = std::make_pair( fileName, stream ); + + mStreamParams[parameterName] = pair; +} + +void Http::MultipartEntitiesBuilder::addFile( const std::string& parameterName, + const std::string& filePath ) { + mFileParams[parameterName] = filePath; +} + +std::string Http::MultipartEntitiesBuilder::build() { + std::ostringstream ostream; + + for ( auto& file : mStreamParams ) { + buildFilePart( ostream, file.second.second, file.first, file.second.first, "" ); + } + + for ( auto& file : mFileParams ) { + IOStreamFile f( file.second ); + buildFilePart( ostream, &f, file.first, FileSystem::fileNameFromPath( file.second ), "" ); + } + + for ( auto& text : mParams ) { + buildTextPart( ostream, text.first, text.second ); + } + + ostream << TWO_HYPHENS << getBoundary() << TWO_HYPHENS << LINE_END; + + return ostream.str(); +} + +void Http::MultipartEntitiesBuilder::buildFilePart( std::ostream& ostream, IOStream* stream, + const std::string& fieldName, + const std::string& fileName, + const std::string& contentType ) { + size_t initialPos = stream->tell(); + stream->seek( 0 ); + int bytesAvailable = stream->getSize(); + int maxBufferSize = 1024 * 1024; + int bufferSize = eemin( bytesAvailable, maxBufferSize ); + TScopedBuffer buffer( bufferSize ); + + ostream << TWO_HYPHENS << getBoundary() << LINE_END; + ostream << "Content-Disposition: form-data; name=\"" << fieldName << "\"; filename=\"" + << fileName << "\"" << LINE_END; + ostream << "Content-Transfer-Encoding: binary" << LINE_END; + ostream << "Content-Length: " << bytesAvailable << LINE_END; + if ( !contentType.empty() ) { + ostream << "Content-Type: " << contentType << LINE_END; + } + ostream << LINE_END; + + // read file and write it into form... + int bytesRead = stream->read( buffer.get(), bufferSize ); + + while ( bytesRead > 0 ) { + ostream.write( buffer.get(), bufferSize ); + bytesAvailable -= bytesRead; + bufferSize = eemin( bytesAvailable, maxBufferSize ); + bytesRead = stream->read( buffer.get(), bufferSize ); + } + + ostream << LINE_END; + stream->seek( initialPos ); +} + +void Http::MultipartEntitiesBuilder::buildTextPart( std::ostream& ostream, + const std::string& parameterName, + const std::string& parameterValue ) { + ostream << TWO_HYPHENS << getBoundary() << LINE_END; + ostream << "Content-Disposition: form-data; name=\"" << parameterName << "\"" << LINE_END; + ostream << "Content-Type: text/plain; charset=UTF-8" << LINE_END; + ostream << LINE_END; + ostream << parameterValue; + ostream << LINE_END; +} + +}} // namespace EE::Network diff --git a/src/eepp/system/thread.cpp b/src/eepp/system/thread.cpp index 70494379e..96cb6c4cd 100644 --- a/src/eepp/system/thread.cpp +++ b/src/eepp/system/thread.cpp @@ -3,17 +3,20 @@ namespace EE { namespace System { -Uint32 Thread::getCurrentThreadId() { +UintPtr Thread::getCurrentThreadId() { return Platform::ThreadImpl::getCurrentThreadId(); } Thread::Thread() : mThreadImpl( NULL ), mEntryPoint( NULL ) {} Thread::~Thread() { - wait(); + if ( mThreadImpl && mThreadImpl->getId() != Thread::getCurrentThreadId() ) + wait(); if ( NULL != mEntryPoint ) delete mEntryPoint; + + eeSAFE_DELETE( mThreadImpl ); } void Thread::launch() { diff --git a/src/eepp/window/engine.cpp b/src/eepp/window/engine.cpp index ad2567e5c..d268fab1e 100644 --- a/src/eepp/window/engine.cpp +++ b/src/eepp/window/engine.cpp @@ -42,7 +42,7 @@ namespace EE { namespace Window { -static Uint64 sMainThreadId{ 0 }; +static UintPtr sMainThreadId{ 0 }; SINGLETON_DECLARE_IMPLEMENTATION( Engine ) @@ -367,7 +367,7 @@ bool Engine::isThreaded() { #endif } -Uint64 Engine::getMainThreadId() { +UintPtr Engine::getMainThreadId() { return sMainThreadId; } diff --git a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp index 829fd2ad5..e174f82d6 100644 --- a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp +++ b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp @@ -7,8 +7,7 @@ namespace ecode { LLMChatCompletionRequest::LLMChatCompletionRequest( const std::string& uri, const std::string& auth, const std::string& reqBody, const std::string& provider ) : - mUrl( uri ) { - mHttp.setHost( mUrl ); + mUrl( uri ), mHttp( Http::Pool::getGlobal().get( URI( uri ) ) ) { mRequest.setUri( mUrl.getPathEtc() ); mRequest.setHeader( "Content-Type", "application/json" ); if ( provider == "anthropic" ) { @@ -23,7 +22,6 @@ LLMChatCompletionRequest::LLMChatCompletionRequest( const std::string& uri, cons mRequest.setProgressCallback( [this]( const Http&, const Http::Request&, const Http::Response&, const Http::Request::Status& status, size_t, size_t ) { if ( mCancel ) { - mRequest.cancel(); if ( cancelCb ) cancelCb( *this ); return false; @@ -103,14 +101,18 @@ LLMChatCompletionRequest::LLMChatCompletionRequest( const std::string& uri, cons } ); } +LLMChatCompletionRequest::~LLMChatCompletionRequest() { + cancel(); +} + void LLMChatCompletionRequest::request() { - Http::Response res = mHttp.downloadRequest( mRequest, mStream, Seconds( 5 ) ); + Http::Response res = mHttp->downloadRequest( mRequest, mStream, Seconds( 5 ) ); if ( doneCb ) doneCb( *this, res ); } void LLMChatCompletionRequest::requestAsync() { - mHttp.downloadAsyncRequest( + mRequestId = mHttp->downloadAsyncRequest( [this]( const Http&, Http::Request&, Http::Response& res ) { if ( doneCb ) doneCb( *this, res ); @@ -120,6 +122,8 @@ void LLMChatCompletionRequest::requestAsync() { void LLMChatCompletionRequest::cancel() { mCancel = true; + if ( mRequestId ) + mHttp->setCancelRequest( mRequestId ); } const std::string& LLMChatCompletionRequest::getStream() const { diff --git a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp index e805fe610..cc182ce9a 100644 --- a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp +++ b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp @@ -30,6 +30,8 @@ class LLMChatCompletionRequest { LLMChatCompletionRequest( const std::string& uri, const std::string& auth, const std::string& reqBody, const std::string& provider ); + ~LLMChatCompletionRequest(); + void request(); void requestAsync(); @@ -44,7 +46,7 @@ class LLMChatCompletionRequest { protected: URI mUrl; - Http mHttp; + std::shared_ptr mHttp; Http::Request mRequest; IOStreamString mStream; std::string mReasoningResponse; @@ -53,6 +55,7 @@ class LLMChatCompletionRequest { bool mCancel{ false }; bool mFirstMessage{ true }; bool mReasoning{ false }; + Uint64 mRequestId{ 0 }; }; } // namespace ecode diff --git a/src/tools/ecode/settingsactions.cpp b/src/tools/ecode/settingsactions.cpp index 96617cf99..ffec1cd85 100644 --- a/src/tools/ecode/settingsactions.cpp +++ b/src/tools/ecode/settingsactions.cpp @@ -6,7 +6,7 @@ using namespace std::string_literals; namespace ecode { -void SettingsActions::checkForUpdatesResponse( Http::Response response, bool fromStartup ) { +void SettingsActions::checkForUpdatesResponse( Http::Response&& response, bool fromStartup ) { auto updatesError = [this, fromStartup]() { if ( fromStartup ) return; @@ -103,8 +103,8 @@ void SettingsActions::checkForUpdates( bool fromStartup ) { [this, fromStartup]( const Http&, Http::Request&, Http::Response& response ) { if ( !SceneManager::existsSingleton() || SceneManager::instance()->isShuttingDown() ) return; - mApp->getUISceneNode()->runOnMainThread( [this, response, fromStartup]() { - checkForUpdatesResponse( response, fromStartup ); + mApp->getUISceneNode()->runOnMainThread( [this, res = response, fromStartup]() mutable { + checkForUpdatesResponse( std::move( res ), fromStartup ); } ); }, "https://api.github.com/repos/SpartanJ/ecode/releases/latest", Seconds( 30 ) ); diff --git a/src/tools/ecode/settingsactions.hpp b/src/tools/ecode/settingsactions.hpp index 57bcc4fd7..68a080b4d 100644 --- a/src/tools/ecode/settingsactions.hpp +++ b/src/tools/ecode/settingsactions.hpp @@ -42,7 +42,7 @@ class SettingsActions { String i18n( const std::string& key, const String& def ); - void checkForUpdatesResponse( Http::Response response, bool fromStartup ); + void checkForUpdatesResponse( Http::Response&& response, bool fromStartup ); };