diff --git a/examples/file_upload_client.cc b/examples/file_upload_client.cc index e2ec736..495f6ac 100644 --- a/examples/file_upload_client.cc +++ b/examples/file_upload_client.cc @@ -14,9 +14,13 @@ bool kSslVerify = true; #endif void Help(const char* argv0) { - std::cout << "Usage: " << argv0 << " " << std::endl; + std::cout << "Usage: " << argv0 << " [url]" << std::endl; + std::cout << "Default Url: http://httpbin.org/post" << std::endl; std::cout << " E.g.," << std::endl; std::cout << " " << argv0 << "E:/github/webcc/data/upload" << std::endl; + std::cout << " " << argv0 + << "E:/github/webcc/data/upload http://httpbin.org/post" + << std::endl; } int main(int argc, char* argv[]) { @@ -29,6 +33,13 @@ int main(int argc, char* argv[]) { const webcc::Path upload_dir(argv[1]); + std::string url; + if (argc == 3) { + url = argv[2]; + } else { + url = "http://httpbin.org/post"; + } + namespace bfs = boost::filesystem; if (!bfs::is_directory(upload_dir) || !bfs::exists(upload_dir)) { @@ -38,9 +49,6 @@ int main(int argc, char* argv[]) { webcc::HttpClientSession session; - //std::string url = "http://httpbin.org/post"; - std::string url = "http://localhost:8080/upload"; - try { auto r = session.PostFile(url, "file", upload_dir / "remember.txt"); diff --git a/examples/file_upload_server.cc b/examples/file_upload_server.cc index e2922f7..5364684 100644 --- a/examples/file_upload_server.cc +++ b/examples/file_upload_server.cc @@ -12,11 +12,11 @@ public: void Handle(const webcc::RestRequest& request, webcc::RestResponse* response) final { if (request.http->method() == webcc::http::methods::kPost) { - std::cout << "files: " << request.http->files().size() << std::endl; + std::cout << "files: " << request.http->form_parts().size() << std::endl; - for (auto& pair : request.http->files()) { - std::cout << "name: " << pair.first << std::endl; - std::cout << "data: " << std::endl << pair.second.data() << std::endl; + for (auto& file : request.http->form_parts()) { + std::cout << "name: " << file.name() << std::endl; + std::cout << "data: " << std::endl << file.data() << std::endl; } response->content = "OK"; diff --git a/examples/http_client.cc b/examples/http_client.cc index aed97e2..02b8dec 100644 --- a/examples/http_client.cc +++ b/examples/http_client.cc @@ -18,8 +18,8 @@ void ExampleBasic() { auto r = session.Request(webcc::HttpRequestBuilder{} .Get() .Url("http://httpbin.org/get") - .Parameter("key1", "value1") - .Parameter("key2", "value2") + .Query("key1", "value1") + .Query("key2", "value2") .Header("Accept", "application/json")()); std::cout << r->content() << std::endl; @@ -44,7 +44,7 @@ void ExampleHttps() { auto r = session.Request(webcc::HttpRequestBuilder{} .Get() .Url("https://httpbin.org/get") - .Parameter("key1", "value1") + .Query("key1", "value1") .Header("Accept", "application/json")()); std::cout << r->content() << std::endl; diff --git a/webcc/CMakeLists.txt b/webcc/CMakeLists.txt index e89644e..0558b06 100644 --- a/webcc/CMakeLists.txt +++ b/webcc/CMakeLists.txt @@ -22,7 +22,6 @@ set(HEADERS http_client_session.h http_connection.h http_connection_pool.h - http_file.h http_message.h http_parser.h http_request.h @@ -53,7 +52,6 @@ set(SOURCES http_client_session.cc http_connection.cc http_connection_pool.cc - http_file.cc http_message.cc http_parser.cc http_request.cc diff --git a/webcc/common.cc b/webcc/common.cc index 668769f..39d7871 100644 --- a/webcc/common.cc +++ b/webcc/common.cc @@ -1,13 +1,25 @@ #include "webcc/common.h" #include "boost/algorithm/string.hpp" +#include "boost/filesystem/fstream.hpp" #include "webcc/logger.h" +namespace bfs = boost::filesystem; + namespace webcc { // ----------------------------------------------------------------------------- +namespace misc_strings { + +const char HEADER_SEPARATOR[] = { ':', ' ' }; +const char CRLF[] = { '\r', '\n' }; + +} // misc_strings + +// ----------------------------------------------------------------------------- + bool Split2(const std::string& str, char token, std::string* part1, std::string* part2) { std::size_t pos = str.find(token); @@ -24,6 +36,20 @@ bool Split2(const std::string& str, char token, std::string* part1, return true; } +bool ReadFile(const Path& path, std::string* output) { + bfs::ifstream ifs{ path, std::ios::binary | std::ios::ate }; + if (!ifs) { + return false; + } + + auto size = ifs.tellg(); + output->resize(static_cast(size), '\0'); + ifs.seekg(0); + ifs.read(&(*output)[0], size); // TODO: Error handling + + return true; +} + // ----------------------------------------------------------------------------- void HttpHeaders::Set(const std::string& key, const std::string& value) { @@ -169,4 +195,76 @@ bool ContentDisposition::Init(const std::string& str) { return true; } +// ----------------------------------------------------------------------------- + +FormPart::FormPart(const std::string& name, const Path& path, + const std::string& mime_type) + : name_(name), mime_type_(mime_type) { + if (!ReadFile(path, &data_)) { + throw Exception(kFileIOError, "Cannot read the file."); + } + + // Determine file name from file path. + // TODO: Encoding + file_name_ = path.filename().string(); + + // Determine content type from file extension. + if (mime_type_.empty()) { + std::string extension = path.extension().string(); + mime_type_ = http::media_types::FromExtension(extension, false); + } +} + +FormPart::FormPart(std::string&& data, const std::string& file_name, + const std::string& mime_type) { + data_ = std::move(data); + + file_name_ = file_name; + + mime_type_ = mime_type; + + // Determine content type from file extension. + if (mime_type_.empty()) { + std::size_t pos = file_name_.find_last_of('.'); + if (pos != std::string::npos) { + std::string extension = file_name_.substr(pos + 1); + mime_type_ = http::media_types::FromExtension(extension, false); + } + } +} + +void FormPart::Prepare(std::vector& payload) { + if (headers_.empty()) { + std::string value = "form-data"; + if (!name_.empty()) { + value.append("; name=\"" + name_ + "\""); + } + if (!file_name_.empty()) { + value.append("; filename=\"" + file_name_ + "\""); + } + headers_.Set(http::headers::kContentDisposition, value); + + if (!mime_type_.empty()) { + headers_.Set(http::headers::kContentType, mime_type_); + } + } + + using boost::asio::buffer; + + for (const HttpHeader& h : headers_.data()) { + payload.push_back(buffer(h.first)); + payload.push_back(buffer(misc_strings::HEADER_SEPARATOR)); + payload.push_back(buffer(h.second)); + payload.push_back(buffer(misc_strings::CRLF)); + } + + payload.push_back(buffer(misc_strings::CRLF)); + + if (!data_.empty()) { + payload.push_back(buffer(data_)); + } + + payload.push_back(buffer(misc_strings::CRLF)); +} + } // namespace webcc diff --git a/webcc/common.h b/webcc/common.h index bf84058..d458b32 100644 --- a/webcc/common.h +++ b/webcc/common.h @@ -6,14 +6,28 @@ #include #include +#include "boost/asio/buffer.hpp" // for const_buffer +#include "boost/filesystem/path.hpp" + +#include "webcc/globals.h" + namespace webcc { // ----------------------------------------------------------------------------- +using Path = boost::filesystem::path; + +using Payload = std::vector; + +// ----------------------------------------------------------------------------- + // Split a string to two parts by the given token. bool Split2(const std::string& str, char token, std::string* part1, std::string* part2); +// Read entire file into string. +bool ReadFile(const Path& path, std::string* output); + // ----------------------------------------------------------------------------- typedef std::pair HttpHeader; @@ -24,6 +38,10 @@ public: return headers_.size(); } + bool empty() const { + return headers_.empty(); + } + const std::vector& data() const { return headers_; } @@ -131,6 +149,95 @@ private: bool valid_ = false; }; +// ----------------------------------------------------------------------------- + +// Form data part. +class FormPart { +public: + FormPart() = default; + + explicit FormPart(const std::string& name, const Path& path, + const std::string& mime_type = ""); + + FormPart(std::string&& data, const std::string& file_name, + const std::string& mime_type = ""); + +#if WEBCC_DEFAULT_MOVE_COPY_ASSIGN + + FormPart(FormPart&&) = default; + FormPart& operator=(FormPart&&) = default; + +#else + + FormPart(FormPart&& rhs) + : name_(std::move(rhs.name_)), + file_name_(std::move(rhs.file_name_)), + mime_type_(std::move(rhs.mime_type_)), + data_(std::move(rhs.data_)) { + } + + FormPart& operator=(FormPart&& rhs) { + if (&rhs != this) { + name_ = std::move(rhs.name_); + file_name_ = std::move(rhs.file_name_); + mime_type_ = std::move(rhs.mime_type_); + data_ = std::move(rhs.data_); + } + return *this; + } + +#endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN + + const std::string& name() const { + return name_; + } + + void set_name(const std::string& name) { + name_ = name; + } + + const std::string& file_name() const { + return file_name_; + } + + void set_file_name(const std::string& file_name) { + file_name_ = file_name; + } + + const std::string& mime_type() const { + return mime_type_; + } + + const std::string& data() const { + return data_; + } + + void AppendData(const std::string& data) { + data_.append(data); + } + + void AppendData(const char* data, std::size_t size) { + data_.append(data, size); + } + + void Prepare(Payload& payload); + +private: + std::string name_; + + // E.g., example.jpg + // TODO: Unicode + std::string file_name_; + + // E.g., image/jpeg + std::string mime_type_; + + HttpHeaders headers_; + + // Binary file data. + std::string data_; +}; + } // namespace webcc #endif // WEBCC_COMMON_H_ diff --git a/webcc/http_client_session.cc b/webcc/http_client_session.cc index ca41c94..06588a5 100644 --- a/webcc/http_client_session.cc +++ b/webcc/http_client_session.cc @@ -45,7 +45,7 @@ HttpResponsePtr HttpClientSession::Get( assert(parameters.size() % 2 == 0); for (std::size_t i = 1; i < parameters.size(); i += 2) { - builder.Parameter(parameters[i - 1], parameters[i]); + builder.Query(parameters[i - 1], parameters[i]); } SetHeaders(headers, &builder); diff --git a/webcc/http_connection.cc b/webcc/http_connection.cc index 25bb81a..e8604d6 100644 --- a/webcc/http_connection.cc +++ b/webcc/http_connection.cc @@ -95,7 +95,7 @@ void HttpConnection::OnRead(boost::system::error_code ec, std::size_t length) { void HttpConnection::DoWrite() { LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str()); - boost::asio::async_write(socket_, response_->ToBuffers(), + boost::asio::async_write(socket_, response_->payload(), std::bind(&HttpConnection::OnWrite, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); diff --git a/webcc/http_file.cc b/webcc/http_file.cc deleted file mode 100644 index 50466aa..0000000 --- a/webcc/http_file.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "webcc/http_file.h" - -#include "boost/filesystem/fstream.hpp" - -namespace bfs = boost::filesystem; - -namespace webcc { - -// ----------------------------------------------------------------------------- - -// Read entire file into string. -static bool ReadFile(const Path& path, std::string* output) { - bfs::ifstream ifs{path, std::ios::binary | std::ios::ate}; - if (!ifs) { - return false; - } - - auto size = ifs.tellg(); - output->resize(static_cast(size), '\0'); - ifs.seekg(0); - ifs.read(&(*output)[0], size); // TODO: Error handling - - return true; -} - -// ----------------------------------------------------------------------------- - -HttpFile::HttpFile(const Path& path, const std::string& mime_type) { - if (!ReadFile(path, &data_)) { - throw Exception(kFileIOError, "Cannot read the file."); - } - - // Determine file name from file path. - // TODO: Encoding - file_name_ = path.filename().string(); - - // Determine content type from file extension. - if (mime_type.empty()) { - std::string extension = path.extension().string(); - mime_type_ = http::media_types::FromExtension(extension, false); - } else { - mime_type_ = mime_type; - } -} - -HttpFile::HttpFile(std::string&& data, const std::string& file_name, - const std::string& mime_type) { - data_ = std::move(data); - - file_name_ = file_name; - - mime_type_ = mime_type; - - // Determine content type from file extension. - if (mime_type_.empty()) { - std::size_t pos = file_name_.find_last_of('.'); - if (pos != std::string::npos) { - std::string extension = file_name_.substr(pos + 1); - mime_type_ = http::media_types::FromExtension(extension, false); - } - } -} - -} // namespace webcc diff --git a/webcc/http_file.h b/webcc/http_file.h deleted file mode 100644 index 98c5105..0000000 --- a/webcc/http_file.h +++ /dev/null @@ -1,87 +0,0 @@ -#ifndef WEBCC_HTTP_FILE_H_ -#define WEBCC_HTTP_FILE_H_ - -#include - -#include "boost/filesystem/path.hpp" - -#include "webcc/globals.h" - -namespace webcc { - -using Path = boost::filesystem::path; - -// File for HTTP transfer (upload/download). -class HttpFile { -public: - HttpFile() = default; - - explicit HttpFile(const Path& path, const std::string& mime_type = ""); - - HttpFile(std::string&& data, const std::string& file_name, - const std::string& mime_type = ""); - -#if WEBCC_DEFAULT_MOVE_COPY_ASSIGN - - HttpFile(HttpFile&&) = default; - HttpFile& operator=(HttpFile&&) = default; - -#else - - HttpFile(HttpFile&& rhs) - : data_(std::move(rhs.data_)), - file_name_(std::move(rhs.file_name_)), - mime_type_(std::move(rhs.mime_type_)) { - } - - HttpFile& operator=(HttpFile&& rhs) { - if (&rhs != this) { - data_ = std::move(rhs.data_); - file_name_ = std::move(rhs.file_name_); - mime_type_ = std::move(rhs.mime_type_); - } - return *this; - } - -#endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN - - const std::string& data() const { - return data_; - } - - void AppendData(const std::string& data) { - data_.append(data); - } - - void AppendData(const char* data, std::size_t size) { - data_.append(data, size); - } - - const std::string& file_name() const { - return file_name_; - } - - void set_file_name(const std::string& file_name) { - file_name_ = file_name; - } - - const std::string& mime_type() const { - return mime_type_; - } - -private: - // Binary file data. - // TODO: don't use std::string? - std::string data_; - - // E.g., example.jpg - // TODO: Unicode - std::string file_name_; - - // E.g., image/jpeg - std::string mime_type_; -}; - -} // namespace webcc - -#endif // WEBCC_HTTP_FILE_H_ diff --git a/webcc/http_message.cc b/webcc/http_message.cc index 1f9c766..25c8063 100644 --- a/webcc/http_message.cc +++ b/webcc/http_message.cc @@ -13,7 +13,7 @@ namespace webcc { namespace misc_strings { -const char NAME_VALUE_SEPARATOR[] = { ':', ' ' }; +const char HEADER_SEPARATOR[] = { ':', ' ' }; const char CRLF[] = { '\r', '\n' }; } // misc_strings @@ -79,28 +79,28 @@ void HttpMessage::SetContent(std::string&& content, bool set_length) { } } -// ATTENTION: The buffers don't hold the memory! -std::vector HttpMessage::ToBuffers() const { +void HttpMessage::Prepare() { assert(!start_line_.empty()); - std::vector buffers; + using boost::asio::buffer; - buffers.push_back(boost::asio::buffer(start_line_)); + payload_.clear(); + + payload_.push_back(buffer(start_line_)); + payload_.push_back(buffer(misc_strings::CRLF)); for (const HttpHeader& h : headers_.data()) { - buffers.push_back(boost::asio::buffer(h.first)); - buffers.push_back(boost::asio::buffer(misc_strings::NAME_VALUE_SEPARATOR)); - buffers.push_back(boost::asio::buffer(h.second)); - buffers.push_back(boost::asio::buffer(misc_strings::CRLF)); + payload_.push_back(buffer(h.first)); + payload_.push_back(buffer(misc_strings::HEADER_SEPARATOR)); + payload_.push_back(buffer(h.second)); + payload_.push_back(buffer(misc_strings::CRLF)); } - buffers.push_back(boost::asio::buffer(misc_strings::CRLF)); + payload_.push_back(buffer(misc_strings::CRLF)); - if (content_length_ > 0) { - buffers.push_back(boost::asio::buffer(content_)); + if (!content_.empty()) { + payload_.push_back(buffer(content_)); } - - return buffers; } void HttpMessage::Dump(std::ostream& os, std::size_t indent, @@ -111,7 +111,7 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent, } indent_str.append(prefix); - os << indent_str << start_line_; + os << indent_str << start_line_ << std::endl; for (const HttpHeader& h : headers_.data()) { os << indent_str << h.first << ": " << h.second << std::endl; diff --git a/webcc/http_message.h b/webcc/http_message.h index e271bd8..d6846d2 100644 --- a/webcc/http_message.h +++ b/webcc/http_message.h @@ -6,10 +6,7 @@ #include // for move() #include -#include "boost/asio/buffer.hpp" // for const_buffer - #include "webcc/common.h" -#include "webcc/http_file.h" #include "webcc/globals.h" namespace webcc { @@ -84,14 +81,12 @@ public: void SetContent(std::string&& content, bool set_length); - // Make the message (e.g., update start line). - // Must be called before ToBuffers()! - virtual bool Prepare() = 0; + // Prepare payload. + virtual void Prepare(); - // Convert the message into a vector of buffers. The buffers do not own the - // underlying memory blocks, therefore the message object must remain valid - // and not be changed until the write operation has completed. - std::vector ToBuffers() const; + const Payload& payload() const { + return payload_; + } // Dump to output stream. void Dump(std::ostream& os, std::size_t indent = 0, @@ -119,6 +114,9 @@ protected: std::size_t content_length_; HttpHeaders headers_; + + // NOTE: The payload itself doesn't hold the memory! + Payload payload_; }; } // namespace webcc diff --git a/webcc/http_parser.h b/webcc/http_parser.h index 60ea968..7094d0e 100644 --- a/webcc/http_parser.h +++ b/webcc/http_parser.h @@ -3,14 +3,14 @@ #include +#include "webcc/common.h" #include "webcc/globals.h" -#include "webcc/http_file.h" namespace webcc { class HttpMessage; -// HttpParser parses HTTP request and response. +// HTTP request and response parser. class HttpParser { public: explicit HttpParser(HttpMessage* message); @@ -80,19 +80,6 @@ protected: bool chunked_; std::size_t chunk_size_; bool finished_; - - struct Part { - enum Step { - kStart, - kBoundaryParsed, - kHeadersParsed, - kEnded, - }; - Step step = kStart; - std::string name; - HttpFile file; - }; - Part part_; }; } // namespace webcc diff --git a/webcc/http_request.cc b/webcc/http_request.cc index 32e1d03..491c0e1 100644 --- a/webcc/http_request.cc +++ b/webcc/http_request.cc @@ -1,13 +1,84 @@ #include "webcc/http_request.h" #include "webcc/logger.h" +#include "webcc/utility.h" namespace webcc { -bool HttpRequest::Prepare() { +// ----------------------------------------------------------------------------- + +namespace misc_strings { + +const char CRLF[] = { '\r', '\n' }; +const char DOUBLE_DASHES[] = { '-', '-' }; + +} // misc_strings + +// ----------------------------------------------------------------------------- + +void HttpRequest::Prepare() { + CreateStartLine(); + + if (url_.port().empty()) { + SetHeader(http::headers::kHost, url_.host()); + } else { + SetHeader(http::headers::kHost, url_.host() + ":" + url_.port()); + } + + if (form_parts_.empty()) { + HttpMessage::Prepare(); + } else { + // Multipart form data. + + // Another choice to generate the boundary is what Apache does. + // See: https://stackoverflow.com/a/5686863 + if (boundary_.empty()) { + boundary_ = RandomUuid(); + } + + SetContentType("multipart/form-data; boundary=" + boundary_); + + Payload data_payload; + + using boost::asio::buffer; + + for (auto& part : form_parts_) { + // Boundary + data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + data_payload.push_back(buffer(boundary_)); + data_payload.push_back(buffer(misc_strings::CRLF)); + + part.Prepare(data_payload); + } + + // Boundary end + data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + data_payload.push_back(buffer(boundary_)); + data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + data_payload.push_back(buffer(misc_strings::CRLF)); + + // Update Content-Length header. + std::size_t content_length = 0; + for (auto& buffer : data_payload) { + content_length += buffer.size(); + } + SetContentLength(content_length); + + // Prepare start line and headers. + HttpMessage::Prepare(); + + // Append payload of content data. + payload_.insert(payload_.end(), data_payload.begin(), data_payload.end()); + } +} + +void HttpRequest::CreateStartLine() { + if (!start_line_.empty()) { + return; + } + if (url_.host().empty()) { - LOG_ERRO("Invalid request: host is missing."); - return false; + throw Exception(kSchemaError, "Invalid request: host is missing."); } std::string target = "/" + url_.path(); @@ -20,15 +91,6 @@ bool HttpRequest::Prepare() { start_line_ += " "; start_line_ += target; start_line_ += " HTTP/1.1"; - start_line_ += kCRLF; - - if (url_.port().empty()) { - SetHeader(http::headers::kHost, url_.host()); - } else { - SetHeader(http::headers::kHost, url_.host() + ":" + url_.port()); - } - - return true; } } // namespace webcc diff --git a/webcc/http_request.h b/webcc/http_request.h index 7df564d..8b6eecc 100644 --- a/webcc/http_request.h +++ b/webcc/http_request.h @@ -1,7 +1,6 @@ #ifndef WEBCC_HTTP_REQUEST_H_ #define WEBCC_HTTP_REQUEST_H_ -#include #include #include #include @@ -32,9 +31,8 @@ public: url_.Init(url); } - // Add URL query parameter. - void AddParameter(const std::string& key, const std::string& value) { - url_.AddParameter(key, value); + void AddQuery(const std::string& key, const std::string& value) { + url_.AddQuery(key, value); } const std::string& method() const { @@ -57,18 +55,23 @@ public: return port().empty() ? default_port : port(); } - const std::map& files() const { - return files_; + const std::vector& form_parts() const { + return form_parts_; } - // Add a file to upload. - void AddFile(const std::string& name, HttpFile&& file) { - files_[name] = std::move(file); + void set_form_parts_(std::vector&& form_parts) { + form_parts_ = std::move(form_parts); + } + + void AddFormPart(FormPart&& form_part) { + form_parts_.push_back(std::move(form_part)); } // Prepare payload. - // Compose start line, set Host header, etc. - bool Prepare() final; + void Prepare() final; + +private: + void CreateStartLine(); private: std::string method_; @@ -76,7 +79,9 @@ private: Url url_; // Files to upload for a POST request. - std::map files_; + std::vector form_parts_; + + std::string boundary_; }; } // namespace webcc diff --git a/webcc/http_request_builder.cc b/webcc/http_request_builder.cc index f43c410..2cd0c60 100644 --- a/webcc/http_request_builder.cc +++ b/webcc/http_request_builder.cc @@ -14,7 +14,7 @@ HttpRequestPtr HttpRequestBuilder::Build() { auto request = std::make_shared(method_, url_); for (std::size_t i = 1; i < parameters_.size(); i += 2) { - request->AddParameter(parameters_[i - 1], parameters_[i]); + request->AddQuery(parameters_[i - 1], parameters_[i]); } for (std::size_t i = 1; i < headers_.size(); i += 2) { @@ -34,17 +34,19 @@ HttpRequestPtr HttpRequestBuilder::Build() { request->SetContentType(http::media_types::kApplicationJson, ""); } } else if (!files_.empty()) { - // Another choice to generate the boundary is what Apache does, see: - // https://stackoverflow.com/a/5686863 - const std::string boundary = RandomUuid(); + request->set_form_parts_(std::move(files_)); - request->SetContentType("multipart/form-data; boundary=" + boundary); + //// Another choice to generate the boundary is what Apache does. + //// See: https://stackoverflow.com/a/5686863 + //const std::string boundary = RandomUuid(); - std::string data; - CreateFormData(&data, boundary); + //request->SetContentType("multipart/form-data; boundary=" + boundary); - // Ingore gzip since most servers don't support it. - request->SetContent(std::move(data), true); + //std::string data; + //CreateFormData(&data, boundary); + + //// Ingore gzip since most servers don't support it. + //request->SetContent(std::move(data), true); } return request; @@ -55,7 +57,7 @@ HttpRequestBuilder& HttpRequestBuilder::File(const std::string& name, const std::string& mime_type) { assert(!name.empty()); - files_[name] = HttpFile(path, mime_type); + files_.push_back(FormPart{ name, path, mime_type }); return *this; } @@ -66,7 +68,8 @@ HttpRequestBuilder& HttpRequestBuilder::FileData(const std::string& name, const std::string& mime_type) { assert(!name.empty()); - files_[name] = HttpFile(std::move(file_data), file_name, mime_type); + // TODO + //files_[name] = HttpFile(std::move(file_data), file_name, mime_type); return *this; } @@ -100,37 +103,37 @@ void HttpRequestBuilder::SetContent(HttpRequestPtr request, request->SetContent(std::move(data), true); } -void HttpRequestBuilder::CreateFormData(std::string* data, - const std::string& boundary) { - for (auto& pair : files_) { - data->append("--" + boundary + kCRLF); - - // Content-Disposition header - data->append("Content-Disposition: form-data"); - if (!pair.first.empty()) { - data->append("; name=\"" + pair.first + "\""); - } - if (!pair.second.file_name().empty()) { - data->append("; filename=\"" + pair.second.file_name() + "\""); - } - data->append(kCRLF); - - // Content-Type header - if (!pair.second.mime_type().empty()) { - data->append("Content-Type: " + pair.second.mime_type()); - data->append(kCRLF); - } - - data->append(kCRLF); - - // Payload - data->append(pair.second.data()); - - data->append(kCRLF); - } - - data->append("--" + boundary + "--"); - data->append(kCRLF); -} +//void HttpRequestBuilder::CreateFormData(std::string* data, +// const std::string& boundary) { +// for (auto& pair : files_) { +// data->append("--" + boundary + kCRLF); +// +// // Content-Disposition header +// data->append("Content-Disposition: form-data"); +// if (!pair.first.empty()) { +// data->append("; name=\"" + pair.first + "\""); +// } +// if (!pair.second.file_name().empty()) { +// data->append("; filename=\"" + pair.second.file_name() + "\""); +// } +// data->append(kCRLF); +// +// // Content-Type header +// if (!pair.second.mime_type().empty()) { +// data->append("Content-Type: " + pair.second.mime_type()); +// data->append(kCRLF); +// } +// +// data->append(kCRLF); +// +// // Payload +// data->append(pair.second.data()); +// +// data->append(kCRLF); +// } +// +// data->append("--" + boundary + "--"); +// data->append(kCRLF); +//} } // namespace webcc diff --git a/webcc/http_request_builder.h b/webcc/http_request_builder.h index deabc9c..ff80bdb 100644 --- a/webcc/http_request_builder.h +++ b/webcc/http_request_builder.h @@ -5,7 +5,6 @@ #include #include -#include "webcc/http_file.h" #include "webcc/http_request.h" namespace webcc { @@ -16,6 +15,11 @@ public: : method_(method) { } + ~HttpRequestBuilder() = default; + + HttpRequestBuilder(const HttpRequestBuilder&) = delete; + HttpRequestBuilder& operator=(const HttpRequestBuilder&) = delete; + // Build the request. HttpRequestPtr Build(); @@ -44,8 +48,8 @@ public: return *this; } - HttpRequestBuilder& Parameter(const std::string& key, - const std::string& value) { + HttpRequestBuilder& Query(const std::string& key, + const std::string& value) { parameters_.push_back(key); parameters_.push_back(value); return *this; @@ -70,8 +74,8 @@ public: HttpRequestBuilder& File(const std::string& name, const Path& path, const std::string& mime_type = ""); - HttpRequestBuilder& File(const std::string& name, HttpFile&& file) { - files_[name] = std::move(file); + HttpRequestBuilder& File(FormPart&& file) { + files_.push_back(std::move(file)); return *this; } @@ -108,7 +112,7 @@ public: private: void SetContent(HttpRequestPtr request, std::string&& data); - void CreateFormData(std::string* data, const std::string& boundary); + //void CreateFormData(std::string* data, const std::string& boundary); private: std::string method_; @@ -125,7 +129,7 @@ private: bool json_ = false; // Files to upload for a POST request. - std::map files_; + std::vector files_; // Compress the request content. // NOTE: Most servers don't support compressed requests. diff --git a/webcc/http_request_parser.cc b/webcc/http_request_parser.cc index eb8f2ea..6175987 100644 --- a/webcc/http_request_parser.cc +++ b/webcc/http_request_parser.cc @@ -61,7 +61,7 @@ bool HttpRequestParser::ParseMultipartContent() { break; } - if (part_.step == Part::Step::kStart) { + if (step_ == Step::kStart) { std::string line; if (!GetNextLine(0, &line, true)) { break; // Not enough data @@ -72,15 +72,15 @@ bool HttpRequestParser::ParseMultipartContent() { } LOG_INFO("Boundary line: %s", line.c_str()); // Go to next step. - part_.step = Part::Step::kBoundaryParsed; + step_ = Step::kBoundaryParsed; continue; } - if (part_.step == Part::Step::kBoundaryParsed) { + if (step_ == Step::kBoundaryParsed) { bool need_more_data = false; if (ParsePartHeaders(&need_more_data)) { // Go to next step. - part_.step = Part::Step::kHeadersParsed; + step_ = Step::kHeadersParsed; LOG_INFO("Part headers just ended."); continue; } else { @@ -93,13 +93,13 @@ bool HttpRequestParser::ParseMultipartContent() { } } - if (part_.step == Part::Step::kHeadersParsed) { + if (step_ == Step::kHeadersParsed) { std::size_t off = 0; std::size_t count = 0; bool ended = false; if (!GetNextBoundaryLine(&off, &count, &ended)) { // All pending data belongs to this part. - part_.file.AppendData(pending_data_); + part_.AppendData(pending_data_); pending_data_.clear(); break; } @@ -109,24 +109,24 @@ bool HttpRequestParser::ParseMultipartContent() { // This part has ended. if (off > 2) { // -2 for exluding the CRLF after the data. - part_.file.AppendData(pending_data_.data(), off - 2); + part_.AppendData(pending_data_.data(), off - 2); } - request_->AddFile(part_.name, std::move(part_.file)); + request_->AddFormPart(std::move(part_)); if (ended) { // Go to the end step. - part_.step = Part::Step::kEnded; + step_ = Step::kEnded; break; } else { // Go to next step. - part_.step = Part::Step::kBoundaryParsed; + step_ = Step::kBoundaryParsed; continue; } } } - if (part_.step == Part::Step::kEnded) { + if (step_ == Step::kEnded) { LOG_INFO("Multipart data has ended."); Finish(); } @@ -169,10 +169,10 @@ bool HttpRequestParser::ParsePartHeaders(bool* need_more_data) { header.second.c_str()); return false; } - part_.name = content_disposition.name(); - part_.file.set_file_name(content_disposition.file_name()); + part_.set_name(content_disposition.name()); + part_.set_file_name(content_disposition.file_name()); LOG_INFO("Content-Disposition (name=%s; filename=%s)", - part_.name.c_str(), part_.file.file_name().c_str()); + part_.name().c_str(), part_.file_name().c_str()); } // TODO: Parse other headers. diff --git a/webcc/http_request_parser.h b/webcc/http_request_parser.h index 8e27584..efc2f32 100644 --- a/webcc/http_request_parser.h +++ b/webcc/http_request_parser.h @@ -34,6 +34,16 @@ private: private: HttpRequest* request_; + + enum Step { + kStart, + kBoundaryParsed, + kHeadersParsed, + kEnded, + }; + Step step_ = kStart; + + FormPart part_; }; } // namespace webcc diff --git a/webcc/http_response.cc b/webcc/http_response.cc index 493d1a9..dbc1739 100644 --- a/webcc/http_response.cc +++ b/webcc/http_response.cc @@ -6,17 +6,17 @@ namespace webcc { namespace status_strings { -const std::string OK = "HTTP/1.1 200 OK\r\n"; -const std::string CREATED = "HTTP/1.1 201 Created\r\n"; -const std::string ACCEPTED = "HTTP/1.1 202 Accepted\r\n"; -const std::string NO_CONTENT = "HTTP/1.1 204 No Content\r\n"; -const std::string NOT_MODIFIED = "HTTP/1.1 304 Not Modified\r\n"; -const std::string BAD_REQUEST = "HTTP/1.1 400 Bad Request\r\n"; -const std::string NOT_FOUND = "HTTP/1.1 404 Not Found\r\n"; +const std::string OK = "HTTP/1.1 200 OK"; +const std::string CREATED = "HTTP/1.1 201 Created"; +const std::string ACCEPTED = "HTTP/1.1 202 Accepted"; +const std::string NO_CONTENT = "HTTP/1.1 204 No Content"; +const std::string NOT_MODIFIED = "HTTP/1.1 304 Not Modified"; +const std::string BAD_REQUEST = "HTTP/1.1 400 Bad Request"; +const std::string NOT_FOUND = "HTTP/1.1 404 Not Found"; const std::string INTERNAL_SERVER_ERROR = - "HTTP/1.1 500 Internal Server Error\r\n"; -const std::string NOT_IMPLEMENTED = "HTTP/1.1 501 Not Implemented\r\n"; -const std::string SERVICE_UNAVAILABLE = "HTTP/1.1 503 Service Unavailable\r\n"; + "HTTP/1.1 500 Internal Server Error"; +const std::string NOT_IMPLEMENTED = "HTTP/1.1 501 Not Implemented"; +const std::string SERVICE_UNAVAILABLE = "HTTP/1.1 503 Service Unavailable"; const std::string& ToString(int status) { switch (status) { @@ -57,13 +57,15 @@ const std::string& ToString(int status) { } // namespace status_strings -bool HttpResponse::Prepare() { - start_line_ = status_strings::ToString(status_); +void HttpResponse::Prepare() { + if (start_line_.empty()) { + start_line_ = status_strings::ToString(status_); + } SetHeader(http::headers::kServer, http::UserAgent()); SetHeader(http::headers::kDate, GetHttpDateTimestamp()); - return true; + HttpMessage::Prepare(); } } // namespace webcc diff --git a/webcc/http_response.h b/webcc/http_response.h index 88754cd..aafbba7 100644 --- a/webcc/http_response.h +++ b/webcc/http_response.h @@ -19,13 +19,17 @@ public: ~HttpResponse() override = default; - int status() const { return status_; } + int status() const { + return status_; + } - void set_status(int status) { status_ = status; } + void set_status(int status) { + status_ = status; + } // Set start line according to status code. - bool Prepare() final; - + void Prepare() final; + private: int status_; }; diff --git a/webcc/http_socket.cc b/webcc/http_socket.cc index aee1a56..206359c 100644 --- a/webcc/http_socket.cc +++ b/webcc/http_socket.cc @@ -24,7 +24,7 @@ void HttpSocket::Connect(const std::string& /*host*/, void HttpSocket::Write(const HttpRequest& request, boost::system::error_code* ec) { - boost::asio::write(socket_, request.ToBuffers(), *ec); + boost::asio::write(socket_, request.payload(), *ec); } void HttpSocket::AsyncReadSome(ReadHandler&& handler, std::vector* buffer) { @@ -60,7 +60,7 @@ void HttpSslSocket::Connect(const std::string& host, void HttpSslSocket::Write(const HttpRequest& request, boost::system::error_code* ec) { - boost::asio::write(ssl_socket_, request.ToBuffers(), *ec); + boost::asio::write(ssl_socket_, request.payload(), *ec); } void HttpSslSocket::AsyncReadSome(ReadHandler&& handler, std::vector* buffer) { diff --git a/webcc/url.cc b/webcc/url.cc index 76782ae..fcb0be8 100644 --- a/webcc/url.cc +++ b/webcc/url.cc @@ -268,7 +268,7 @@ void Url::Init(const std::string& str, bool decode, bool clear) { } } -void Url::AddParameter(const std::string& key, const std::string& value) { +void Url::AddQuery(const std::string& key, const std::string& value) { if (!query_.empty()) { query_ += "&"; } diff --git a/webcc/url.h b/webcc/url.h index ed4fa06..f7bc80f 100644 --- a/webcc/url.h +++ b/webcc/url.h @@ -111,8 +111,8 @@ public: return query_; } - // Add a parameter to the query string. - void AddParameter(const std::string& key, const std::string& value); + // Add a query parameter. + void AddQuery(const std::string& key, const std::string& value); private: void Parse(const std::string& str);