diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..2f1ac46 --- /dev/null +++ b/data/README.md @@ -0,0 +1,2 @@ + +**upload**: files for testing upload. diff --git a/data/upload/baby_l.jpg b/data/upload/baby_l.jpg new file mode 100644 index 0000000..3e2e1d7 Binary files /dev/null and b/data/upload/baby_l.jpg differ diff --git a/data/upload/baby_s.jpg b/data/upload/baby_s.jpg new file mode 100644 index 0000000..e1c3269 Binary files /dev/null and b/data/upload/baby_s.jpg differ diff --git a/data/upload/remember.txt b/data/upload/remember.txt new file mode 100644 index 0000000..f563cc7 --- /dev/null +++ b/data/upload/remember.txt @@ -0,0 +1,17 @@ +Remember +BY CHRISTINA ROSSETTI + +Remember me when I am gone away, + Gone far away into the silent land; + When you can no more hold me by the hand, +Nor I half turn to go yet turning stay. +Remember me when no more day by day + You tell me of our future that you plann'd: + Only remember me; you understand +It will be late to counsel then or pray. +Yet if you should forget me for a while + And afterwards remember, do not grieve: + For if the darkness and corruption leave + A vestige of the thoughts that once I had, +Better by far you should forget and smile + Than that you should remember and be sad. diff --git a/data/upload/wolf.jpg b/data/upload/wolf.jpg new file mode 100644 index 0000000..a4e824c Binary files /dev/null and b/data/upload/wolf.jpg differ diff --git a/examples/http_client.cc b/examples/http_client.cc index c407757..1ca2079 100644 --- a/examples/http_client.cc +++ b/examples/http_client.cc @@ -4,8 +4,6 @@ #include "webcc/http_client_session.h" #include "webcc/logger.h" -using namespace webcc; - #if (defined(WIN32) || defined(_WIN64)) // You need to set environment variable SSL_CERT_FILE properly to enable // SSL verification. @@ -65,7 +63,7 @@ void ExampleHttps() { // ExampleKeepAlive("https://api.github.com/events"); // void ExampleKeepAlive(const std::string& url) { - HttpClientSession session; + webcc::HttpClientSession session; session.set_ssl_verify(kSslVerify); // Keep-Alive @@ -79,7 +77,7 @@ void ExampleKeepAlive(const std::string& url) { } void ExampleCompression() { - HttpClientSession session; + webcc::HttpClientSession session; auto r = session.Get("http://httpbin.org/gzip"); @@ -93,7 +91,7 @@ void ExampleCompression() { // Get an image from HttpBin.org and save to the given file path. // E.g., ExampleImage("E:\\example.jpg") void ExampleImage(const std::string& path) { - HttpClientSession session; + webcc::HttpClientSession session; auto r = session.Get("http://httpbin.org/image/jpeg"); @@ -107,9 +105,9 @@ void ExampleImage(const std::string& path) { // Post/upload files. void ExamplePostFiles() { - HttpClientSession session; + webcc::HttpClientSession session; - auto r = session.Request(HttpRequestBuilder{} + auto r = session.Request(webcc::HttpRequestBuilder{} .Post() .Url("http://httpbin.org/post") .FileData("file1", "report.xls", "", "application/vnd.ms-excel") @@ -119,29 +117,28 @@ void ExamplePostFiles() { } // Post/upload files by file path. -void ExamplePostFiles(const std::string& name, +void ExamplePostFiles(const std::string& url, + const std::string& name, const std::string& file_name, const std::string& file_path, const std::string& content_type) { - HttpClientSession session; + webcc::HttpClientSession session; - auto r = - session.Request(HttpRequestBuilder{} - .Post() - .Url("http://httpbin.org/post") - .File(name, file_name, file_path, content_type)()); + auto r = session.Request(webcc::HttpRequestBuilder{}.Post(). + Url(url). + File(name, file_name, file_path, content_type)()); std::cout << r->content() << std::endl; } int main() { - WEBCC_LOG_INIT("", LOG_CONSOLE); + WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); try { ExampleBasic(); - } catch (const Exception& e) { + } catch (const webcc::Exception& e) { std::cout << "Exception: " << e.what() << std::endl; } diff --git a/webcc/globals.cc b/webcc/globals.cc index 660a129..f0ed99f 100644 --- a/webcc/globals.cc +++ b/webcc/globals.cc @@ -1,9 +1,33 @@ #include "webcc/globals.h" +#include +#include + +#include "boost/filesystem/path.hpp" + #include "webcc/version.h" namespace webcc { +// ----------------------------------------------------------------------------- + +// Read entire file into string. +static bool ReadFile(const std::string& path, std::string* output) { + std::ifstream ifs{path, std::ios::binary | std::ios::ate}; + if (!ifs) { + return false; + } + + auto size = ifs.tellg(); + output->resize(size, '\0'); + ifs.seekg(0); + ifs.read(&(*output)[0], size); // TODO: Error handling + + return true; +} + +// ----------------------------------------------------------------------------- + namespace http { const std::string& UserAgent() { @@ -11,6 +35,66 @@ const std::string& UserAgent() { return s_user_agent; } +File::File(const std::string& file_path) { + if (!ReadFile(file_path, &data)) { + throw Exception(kFileIOError, "Cannot read the file."); + } + + namespace bfs = boost::filesystem; + + // Determine file name from file path. + //if (file_name.empty()) { + file_name = bfs::path(file_path).filename().string(); + //} else { + // file_name = file_name; + //} + + // Determine content type from file extension. + //if (mime_type.empty()) { + std::string extension = bfs::path(file_path).extension().string(); + mime_type = http::media_types::FromExtension(extension, false); + //} else { + //mime_type = mime_type; + //} +} + +namespace media_types { + +// TODO: Add more. +static void InitMap(std::map& map) { + map["gif"] = "image/gif"; + map["htm"] = "text/html"; + map["html"] = "text/html"; + map["jpg"] = "image/jpeg"; + map["jpeg"] = "image/jpeg"; + map["png"] = "image/png"; + map["txt"] = "text/plain"; + map[""] = ""; +} + +// TODO: Ignore case on Windows. +std::string FromExtension(const std::string& extension, + bool default_to_plain_text) { + static std::map s_map; + + if (s_map.empty()) { + InitMap(s_map); + } + + auto it = s_map.find(extension); + if (it != s_map.end()) { + return it->second; + } + + if (default_to_plain_text) { + return "text/plain"; + } else { + return ""; + } +} + +} // namespace media_types + } // namespace http const char* DescribeError(Error error) { diff --git a/webcc/globals.h b/webcc/globals.h index 891ce58..e5c7c41 100644 --- a/webcc/globals.h +++ b/webcc/globals.h @@ -24,8 +24,9 @@ const std::size_t kMaxDumpSize = 2048; // Default buffer size for socket reading. const std::size_t kBufferSize = 1024; -// Default ports. +// Default port for HTTP. const char* const kPort80 = "80"; +// Default port for HTTPS. const char* const kPort443 = "443"; // Why 1400? See the following page: @@ -36,7 +37,6 @@ const std::size_t kGzipThreshold = 1400; // ----------------------------------------------------------------------------- -// HTTP headers. namespace http { namespace methods { @@ -44,15 +44,15 @@ namespace methods { // HTTP methods (verbs) in string. // Don't use enum to avoid converting back and forth. -const char* const kGet = "GET"; -const char* const kHead = "HEAD"; -const char* const kPost = "POST"; -const char* const kPut = "PUT"; -const char* const kDelete = "DELETE"; -const char* const kConnect = "CONNECT"; -const char* const kOptions = "OPTIONS"; -const char* const kTrace = "TRACE"; -const char* const kPatch = "PATCH"; +const char* const kGet = "GET"; +const char* const kHead = "HEAD"; +const char* const kPost = "POST"; +const char* const kPut = "PUT"; +const char* const kDelete = "DELETE"; +const char* const kConnect = "CONNECT"; +const char* const kOptions = "OPTIONS"; +const char* const kTrace = "TRACE"; +const char* const kPatch = "PATCH"; } // namespace methods @@ -95,16 +95,17 @@ const char* const kServer = "Server"; namespace media_types { -// NOTE: -// According to www.w3.org when placing SOAP messages in HTTP bodies, the HTTP -// Content-type header must be chosen as "application/soap+xml" [RFC 3902]. -// But in practice, many web servers cannot understand it. -// See: https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L26854 +// See the following link for the full list of media types: +// https://www.iana.org/assignments/media-types/media-types.xhtml const char* const kApplicationJson = "application/json"; const char* const kApplicationSoapXml = "application/soap+xml"; const char* const kTextXml = "text/xml"; +// Get media type from file extension. +std::string FromExtension(const std::string& extension, + bool default_to_plain_text = true); + } // namespace media_types namespace charsets { @@ -122,6 +123,24 @@ enum class ContentEncoding { // Return default user agent for HTTP headers. const std::string& UserAgent(); +// File for HTTP transfer (upload/download). +class File { +public: + File() = default; + + File(const std::string& file_path); + + // Binary file data. + // TODO: don't use std::string? + std::string data; + + // E.g., example.jpg + std::string file_name; + + // E.g., image/jpeg + std::string mime_type; +}; + } // namespace http // ----------------------------------------------------------------------------- diff --git a/webcc/http_client.cc b/webcc/http_client.cc index 2bb70bc..f945c68 100644 --- a/webcc/http_client.cc +++ b/webcc/http_client.cc @@ -165,6 +165,7 @@ void HttpClient::DoReadResponse(Error* error) { // Stop the deadline timer once the read has started (or failed). CancelTimer(); + // TODO: Is it necessary to check `length == 0`? if (ec || length == 0) { Close(); *error = kSocketReadError; diff --git a/webcc/http_client_session.cc b/webcc/http_client_session.cc index 631b8a9..1a34ff6 100644 --- a/webcc/http_client_session.cc +++ b/webcc/http_client_session.cc @@ -67,6 +67,20 @@ HttpResponsePtr HttpClientSession::Post( return Request(builder()); } +HttpResponsePtr HttpClientSession::PostFile(const std::string& url, + const std::string& name, + http::File&& file, + const std::vector& headers) { + HttpRequestBuilder builder{http::methods::kPost}; + builder.Url(url); + + SetHeaders(headers, &builder); + + builder.File(name, std::move(file)); + + return Request(builder()); +} + HttpResponsePtr HttpClientSession::Put( const std::string& url, std::string&& data, bool json, const std::vector& headers) { diff --git a/webcc/http_client_session.h b/webcc/http_client_session.h index a53413c..6cb2f5f 100644 --- a/webcc/http_client_session.h +++ b/webcc/http_client_session.h @@ -59,6 +59,12 @@ public: HttpResponsePtr Post(const std::string& url, std::string&& data, bool json, const std::vector& headers = {}); + // Post a file. + HttpResponsePtr PostFile(const std::string& url, + const std::string& name, + http::File&& file, + const std::vector& headers = {}); + // Shortcut for PUT request. HttpResponsePtr Put(const std::string& url, std::string&& data, bool json, const std::vector& headers = {}); diff --git a/webcc/http_message.cc b/webcc/http_message.cc index f461962..23a56d5 100644 --- a/webcc/http_message.cc +++ b/webcc/http_message.cc @@ -166,7 +166,9 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent, os << indent_str << std::endl; - // NOTE: The content will be truncated if it's too large to display. + // NOTE: + // - The content will be truncated if it's too large to display. + // - Binary content will not be dumped (TODO). if (!content_.empty()) { if (indent == 0) { diff --git a/webcc/http_request_builder.cc b/webcc/http_request_builder.cc index af2f999..a0a9fc1 100644 --- a/webcc/http_request_builder.cc +++ b/webcc/http_request_builder.cc @@ -1,7 +1,5 @@ #include "webcc/http_request_builder.h" -#include - #include "webcc/base64.h" #include "webcc/logger.h" #include "webcc/utility.h" @@ -9,25 +7,6 @@ namespace webcc { -// ----------------------------------------------------------------------------- - -// Read entire file into string. -static bool ReadFile(const std::string& path, std::string* output) { - std::ifstream ifs{path, std::ios::binary | std::ios::ate}; - if (!ifs) { - return false; - } - - auto size = ifs.tellg(); - output->resize(size, '\0'); - ifs.seekg(0); - ifs.read(&(*output)[0], size); // TODO: Error handling - - return true; -} - -// ----------------------------------------------------------------------------- - HttpRequestPtr HttpRequestBuilder::Build() { assert(parameters_.size() % 2 == 0); assert(headers_.size() % 2 == 0); @@ -72,15 +51,33 @@ HttpRequestPtr HttpRequestBuilder::Build() { } HttpRequestBuilder& HttpRequestBuilder::File(const std::string& name, - const std::string& file_name, const std::string& file_path, - const std::string& content_type) { - std::string file_data; - if (!ReadFile(file_path, &file_data)) { - throw Exception(kFileIOError, "Cannot read the file."); - } + const std::string& file_name, + const std::string& mime_type) { + assert(!name.empty()); + + // TODO + files_[name] = http::File(file_path/*, file_name, mime_type*/); + + return *this; +} + +HttpRequestBuilder& HttpRequestBuilder::File(const std::string& name, + http::File&& file) { + files_[name] = std::move(file); + return *this; +} + +HttpRequestBuilder& HttpRequestBuilder::FileData(const std::string& name, + std::string&& file_data, + const std::string& file_name, + const std::string& mime_type) { + http::File file; + file.data = std::move(file_data); + file.file_name = file_name; + file.mime_type = mime_type; - files_.push_back({name, file_name, std::move(file_data), content_type}); + files_[name] = std::move(file); return *this; } @@ -116,29 +113,29 @@ void HttpRequestBuilder::SetContent(HttpRequestPtr request, void HttpRequestBuilder::CreateFormData(std::string* data, const std::string& boundary) { - for (UploadFile& file : files_) { + for (auto& pair : files_) { data->append("--" + boundary + kCRLF); // Content-Disposition header data->append("Content-Disposition: form-data"); - if (!file.name.empty()) { - data->append("; name=\"" + file.name + "\""); + if (!pair.first.empty()) { + data->append("; name=\"" + pair.first + "\""); } - if (!file.file_name.empty()) { - data->append("; filename=\"" + file.file_name + "\""); + if (!pair.second.file_name.empty()) { + data->append("; filename=\"" + pair.second.file_name + "\""); } data->append(kCRLF); // Content-Type header - if (!file.content_type.empty()) { - data->append("Content-Type: " + file.content_type); + if (!pair.second.mime_type.empty()) { + data->append("Content-Type: " + pair.second.mime_type); data->append(kCRLF); } data->append(kCRLF); // Payload - data->append(file.file_data); + data->append(pair.second.data); data->append(kCRLF); } diff --git a/webcc/http_request_builder.h b/webcc/http_request_builder.h index 880344f..b78bd57 100644 --- a/webcc/http_request_builder.h +++ b/webcc/http_request_builder.h @@ -1,6 +1,7 @@ #ifndef WEBCC_HTTP_REQUEST_BUILDER_H_ #define WEBCC_HTTP_REQUEST_BUILDER_H_ +#include #include #include @@ -67,18 +68,17 @@ public: // Upload a file with its path. // TODO: UNICODE file path. HttpRequestBuilder& File(const std::string& name, - const std::string& file_name, const std::string& file_path, - const std::string& content_type = ""); + const std::string& file_name = "", + const std::string& mime_type = ""); + + HttpRequestBuilder& File(const std::string& name, http::File&& file); // Upload a file with its data. HttpRequestBuilder& FileData(const std::string& name, - const std::string& file_name, std::string&& file_data, - const std::string& content_type = "") { - files_.push_back({name, file_name, file_data, content_type}); - return *this; - } + const std::string& file_name = "", + const std::string& mime_type = ""); HttpRequestBuilder& Gzip(bool gzip = true) { gzip_ = gzip; @@ -122,19 +122,8 @@ private: // Is the data to send a JSON string? bool json_ = false; - // A file to upload. - // Examples: - // { "images", "example.jpg", "BinaryData", "image/jpeg" } - // { "file", "report.csv", "BinaryData", "" } - struct UploadFile { - std::string name; - std::string file_name; - std::string file_data; // Binary file data - std::string content_type; - }; - // Files to upload for a POST (or PUT?) request. - std::vector files_; + std::map files_; // Compress the request content. // NOTE: Most servers don't support compressed requests. diff --git a/webcc/soap_client.cc b/webcc/soap_client.cc index 1acf249..9efb985 100644 --- a/webcc/soap_client.cc +++ b/webcc/soap_client.cc @@ -51,6 +51,12 @@ bool SoapClient::Request(const std::string& operation, http_request->SetContent(std::move(http_content), true); + // NOTE: + // According to www.w3.org when placing SOAP messages in HTTP bodies, the HTTP + // Content-type header must be chosen as "application/soap+xml" [RFC 3902]. + // But in practice, many web servers cannot understand it. + // See: https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L26854 + if (soap_version_ == kSoapV11) { http_request->SetContentType(http::media_types::kTextXml, http::charsets::kUtf8);