diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index 882f57f5f..ab6bfa3c3 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -15,6 +15,10 @@ #include #include +namespace EE { namespace System { +class IOStream; +}} + using namespace EE::System; namespace EE { namespace Network { @@ -128,6 +132,9 @@ class EE_API Http : NonCopyable { class EE_API Response { public: + // Types + typedef std::map FieldTable; + /** @brief Enumerate all the valid status codes for a response */ enum Status { // 2xx: success @@ -168,6 +175,8 @@ class EE_API Http : NonCopyable { ** 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 @@ -217,9 +226,6 @@ class EE_API Http : NonCopyable { ** @param in String stream containing the header values */ void parseFields(std::istream &in); - // Types - typedef std::map FieldTable; - // Member data FieldTable mFields; ///< Fields of the header Status mStatus; ///< Status code @@ -259,7 +265,7 @@ class EE_API Http : NonCopyable { void setHost(const std::string& host, unsigned short port = 0, bool useSSL = false); /** @brief Send a HTTP request and return the server's response. - ** You must have a valid host before sending a request (see SetHost). + ** 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 @@ -272,6 +278,34 @@ class EE_API Http : NonCopyable { ** @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 defaut timeout + ** (which is usually pretty long). + ** @param request Request to send + ** @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 defaut timeout + ** (which is usually pretty long). + ** @param request Request to send + ** @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 cb::Callback3 AsyncResponseCallback; @@ -280,6 +314,11 @@ class EE_API Http : NonCopyable { ** @see SendRequest */ void sendAsyncRequest( 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 SendRequest */ + void downloadAsyncRequest( AsyncResponseCallback cb, const Http::Request& request, IOStream& writeTo, Time timeout = Time::Zero ); + /** @return The host address */ const IpAddress& getHost() const; @@ -293,6 +332,12 @@ class EE_API Http : NonCopyable { public: AsyncRequest( Http * http, AsyncResponseCallback cb, Http::Request request, Time timeout ); + AsyncRequest( Http * http, AsyncResponseCallback cb, Http::Request request, IOStream& writeTo, Time timeout ); + + AsyncRequest( Http * http, AsyncResponseCallback cb, Http::Request request, std::string writePath, Time timeout ); + + ~AsyncRequest(); + void run(); protected: friend class Http; @@ -301,6 +346,9 @@ class EE_API Http : NonCopyable { Http::Request mRequest; Time mTimeout; bool mRunning; + bool mStreamed; + bool mStreamOwned; + IOStream * mStream; }; friend class AsyncRequest; ThreadLocalPtr mConnection; ///< Connection to the host @@ -312,6 +360,8 @@ class EE_API Http : NonCopyable { bool mIsSSL; void removeOldThreads(); + + Request prepareFields(const Http::Request& request); }; }} diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index 19d2e1e17..3b424299b 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -108,6 +110,10 @@ Http::Response::Response() : { } +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()) { @@ -299,33 +305,7 @@ Http::Response Http::sendRequest(const Http::Request& request, Time timeout) { } // First make sure that the request is valid -- add missing mandatory fields - Request toSend(request); - - if (!toSend.hasField("From")) { - toSend.setField("From", "user@eepp.com.ar"); - } - - if (!toSend.hasField("User-Agent")) { - toSend.setField("User-Agent", "eepp-network"); - } - - if (!toSend.hasField("Host")) { - toSend.setField("Host", mHostName); - } - - if (!toSend.hasField("Content-Length")) { - 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")) { - toSend.setField("Connection", "close"); - } + Request toSend(prepareFields(request)); // Prepare the response Response received; @@ -359,20 +339,167 @@ Http::Response Http::sendRequest(const Http::Request& request, Time timeout) { return received; } + +Http::Response Http::downloadRequest(const Http::Request & request, IOStream & writeTo, Time timeout) { + if ( 0 == mHost.toInteger() ) { + return Response(); + } + + if ( NULL == mConnection ) { + TcpSocket * Conn = mIsSSL ? eeNew( SSLSocket, ( mHostName, request.getValidateCertificate(), request.getValidateHostname() ) ) : eeNew( TcpSocket, () ); + mConnection = Conn; + } + + Request toSend(prepareFields(request)); + Response received; + + if (mConnection->connect(mHost, mPort, timeout) == Socket::Done) { + std::string requestStr = toSend.prepare(); + + if (!requestStr.empty()) { + if (mConnection->send(requestStr.c_str(), requestStr.size()) == Socket::Done) { + int isnheader = 0; + size_t len = 0; + char * eol; // end of line + char * bol; // beginning of line + std::size_t size = 0; + size_t bufferSize = 1024; + char buffer[bufferSize+1]; + std::string header; + + while (mConnection->receive(buffer, bufferSize, size) == Socket::Done) { + if ( isnheader != 0 ) + writeTo.write( buffer, size ); + + if ( isnheader == 0 ) { + // calculate combined length of unprocessed data and new data + len += size; + + // NULL terminate buffer for string functions + buffer[len] = '\0'; + + // checks if the header break happened to be the first line of the buffer + if ( !( strncmp( buffer, "\r\n", 2 ) ) ) { + if (len > 2) + writeTo.write(buffer, (len-2)); + + continue; + } + + if ( !( strncmp( buffer, "\n", 1 ) ) ) { + if ( len > 1 ) + writeTo.write(buffer, (len-1)); + + continue; + } + + // process each line in buffer looking for header break + bol = buffer; + + while( ( 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 ( ( !( strncmp( bol, "\r\n", 2 ) ) ) || ( ! ( strncmp( bol, "\n", 1) ) ) ) { + // note that end of headers has been reached + isnheader = 1; + + // 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 = len - ( bol - buffer ); + + // write remaining data to FILE stream + if ( len > 0 ) + writeTo.write( bol, len ); + + header.append( buffer, ( bol - buffer ) ); + + // reset length of left over data to zero and continue processing + // non-header information + len = 0; + } + } + + if ( isnheader == 0 ) { + header.append( buffer, ( bol - buffer ) ); + } + } + } + + if ( !header.empty() ) + received.parse(header); + } + } + + // Close the connection + mConnection->disconnect(); + } + + return received; +} + +Http::Response Http::downloadRequest(const Http::Request & request, std::string writePath, Time timeout) { + IOStreamFile file( writePath, "wb" ); + return downloadRequest( request, file, timeout ); +} + Http::AsyncRequest::AsyncRequest(Http *http, AsyncResponseCallback cb, Http::Request request, Time timeout) : mHttp( http ), mCb( cb ), mRequest( request ), mTimeout( timeout ), - mRunning( true ) + mRunning( true ), + mStreamed( false ), + mStreamOwned( false ), + mStream(NULL) { } +Http::AsyncRequest::AsyncRequest(Http * http, 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, 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( eeNew( IOStreamFile, ( writePath, "wb" ) ) ) +{ +} + +Http::AsyncRequest::~AsyncRequest() { + if ( mStreamOwned ) + eeSAFE_DELETE( mStream ); +} + void Http::AsyncRequest::run() { - Http::Response response = mHttp->sendRequest( mRequest, mTimeout ); + Http::Response response = mStreamed ? mHttp->downloadRequest( mRequest, *mStream, mTimeout ) : mHttp->sendRequest( mRequest, mTimeout ); mCb( *mHttp, mRequest, response ); + if ( mStreamed && mStreamOwned ) { + eeSAFE_DELETE( mStream ); + } + // The Async Request destroys the socket used to create the request TcpSocket * tcp = mHttp->mConnection; eeSAFE_DELETE( tcp ); @@ -404,6 +531,38 @@ void Http::removeOldThreads() { } } +Http::Request Http::prepareFields(const Http::Request & request) { + Request toSend(request); + + if (!toSend.hasField("From")) { + toSend.setField("From", "user@eepp.com.ar"); + } + + if (!toSend.hasField("User-Agent")) { + toSend.setField("User-Agent", "eepp-network"); + } + + if (!toSend.hasField("Host")) { + toSend.setField("Host", mHostName); + } + + if (!toSend.hasField("Content-Length")) { + 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")) { + toSend.setField("Connection", "close"); + } + + return toSend; +} + void Http::sendAsyncRequest( AsyncResponseCallback cb, const Http::Request& request, Time timeout ) { AsyncRequest * thread = eeNew( AsyncRequest, ( this, cb, request, timeout ) ); @@ -417,6 +576,19 @@ void Http::sendAsyncRequest( AsyncResponseCallback cb, const Http::Request& requ mThreads.push_back( thread ); } +void Http::downloadAsyncRequest(Http::AsyncResponseCallback cb, const Http::Request & request, IOStream & writeTo, Time timeout) { + AsyncRequest * thread = eeNew( AsyncRequest, ( this, cb, request, timeout ) ); + + thread->launch(); + + // Clean old threads + Lock l( mThreadsMutex ); + + removeOldThreads(); + + mThreads.push_back( thread ); +} + const IpAddress &Http::getHost() const { return mHost; } diff --git a/src/examples/http_request/http_request.cpp b/src/examples/http_request/http_request.cpp index 08e84210b..f76649df0 100644 --- a/src/examples/http_request/http_request.cpp +++ b/src/examples/http_request/http_request.cpp @@ -50,6 +50,16 @@ EE_MAIN_FUNC int main (int argc, char * argv []) { Http::Response::Status status = response.getStatus(); if ( status == Http::Response::Ok ) { + Http::Response::FieldTable headers = response.getHeaders(); + + std::cout << "Headers: " << std::endl; + + for ( auto head = headers.begin(); head != headers.end(); ++head ) { + std::cout << "\t" << head->first << ": " << head->second << std::endl; + } + + std::cout << std::endl << "Body: " << std::endl; + std::cout << response.getBody() << std::endl; } else { std::cout << "Error " << status << std::endl;