From 4365d834a342ab445eadb26b0474c8c0944caef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 22 Dec 2019 18:57:46 -0300 Subject: [PATCH] Added Http::MultipartEntitiesBuilder. This class allows the user to create a multipart/form-data request. Added multipart/form-data support for the http_request example. --HG-- branch : dev --- include/eepp/network/http.hpp | 55 +++++++++++ src/eepp/network/http.cpp | 102 ++++++++++++++++++++- src/examples/http_request/http_request.cpp | 38 +++++++- 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index 39904b3bc..b8d964256 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -193,6 +193,9 @@ class EE_API Http : NonCopyable { ** @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 @@ -450,6 +453,58 @@ class EE_API Http : NonCopyable { /** @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: + * ``` + * Http::Request request; + * Http::MultipartEntitiesBuilder builder; + * ... + * request.setField( "Content-Type", builder.getContentType() ); + * ``` + */ + 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. diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index 21ca5eaaf..02d83fa81 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -2,11 +2,13 @@ #include #include #include +#include +#include #include #include #include #include -#include +#include #include #include #include @@ -67,6 +69,10 @@ 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; } @@ -1187,4 +1193,98 @@ Http *Http::Pool::get(const URI & host, const URI & proxy) { return http; } +static constexpr const char* TWO_HYPHENS = "--"; +static constexpr const char* LINE_END = "\r\n"; + +Http::MultipartEntitiesBuilder::MultipartEntitiesBuilder() : + MultipartEntitiesBuilder( "eepp-client-boundary-" + String::toStr( (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; +} + }} diff --git a/src/examples/http_request/http_request.cpp b/src/examples/http_request/http_request.cpp index 69e60f3f9..ef95f72d8 100644 --- a/src/examples/http_request/http_request.cpp +++ b/src/examples/http_request/http_request.cpp @@ -20,6 +20,7 @@ EE_MAIN_FUNC int main (int argc, char * argv []) { args::Flag resume(parser, "continue", "Resume getting a partially-downloaded file", {'c',"continue"}); args::Flag compressed(parser, "compressed", "Request compressed response", {"compressed"}); args::ValueFlag postData(parser, "data", "HTTP POST data", {'d', "data"}); + args::ValueFlagList multipartData(parser, "multipart-data", "Specify multipart MIME data", {'F', "form"}); args::ValueFlagList headers(parser, "header", "Pass custom header(s) to server", {'H', "header"}); args::Flag includeHead(parser, "include", "Include protocol response headers in the output", {'i',"include"}); args::Flag insecure(parser, "insecure", "Allow insecure server connections when using SSL", {'k',"insecure"}); @@ -86,17 +87,44 @@ EE_MAIN_FUNC int main (int argc, char * argv []) { } } - // Set the request method - if ( requestMethod ) { - request.setMethod( Http::Request::methodFromString( requestMethod.Get() ) ); - } - // Set the post data / body if ( postData ) { request.setMethod( Http::Request::Method::Post ); request.setBody( postData.Get() ); } + // Set the multipart data + if ( multipartData ) { + Http::MultipartEntitiesBuilder builder; + for ( const std::string& part : args::get(multipartData) ) { + std::string::size_type pos = part.find_first_of( "=" ); + if ( std::string::npos != pos ) { + std::string name( part.substr( 0, pos ) ); + std::string val( part.substr( pos + 1 ) ); + if ( !val.empty() ) { + if ( val[0] == '@' ) { + val = val.substr( 1 ); + String::trimInPlace( val, '"' ); + if ( FileSystem::fileExists( val ) ) { + builder.addFile( name, val ); + } + } else { + String::trimInPlace( val, '"' ); + builder.addParameter( name, val ); + } + } + } + } + request.setMethod( Http::Request::Method::Post ); + request.setField( "Content-Type", builder.getContentType() ); + request.setBody( builder.build() ); + } + + // Set the request method + if ( requestMethod ) { + request.setMethod( Http::Request::methodFromString( requestMethod.Get() ) ); + } + // If progress requested print a progress on screen if ( progress ) { request.setProgressCallback( []( const Http&, const Http::Request&, const Http::Response&, const Http::Request::Status& status, size_t totalBytes, size_t currentBytes ) {