Remove HttpFile to FormPart; refine payload prepare.
This commit is contained in:
@@ -14,9 +14,13 @@ bool kSslVerify = true;
|
||||
#endif
|
||||
|
||||
void Help(const char* argv0) {
|
||||
std::cout << "Usage: " << argv0 << " <upload_dir>" << std::endl;
|
||||
std::cout << "Usage: " << argv0 << " <upload_dir> [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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<std::size_t>(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<boost::asio::const_buffer>& 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
|
||||
|
||||
+107
@@ -6,14 +6,28 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<boost::asio::const_buffer>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 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<std::string, std::string> HttpHeader;
|
||||
@@ -24,6 +38,10 @@ public:
|
||||
return headers_.size();
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
return headers_.empty();
|
||||
}
|
||||
|
||||
const std::vector<HttpHeader>& 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_
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<std::size_t>(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
|
||||
@@ -1,87 +0,0 @@
|
||||
#ifndef WEBCC_HTTP_FILE_H_
|
||||
#define WEBCC_HTTP_FILE_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#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_
|
||||
+15
-15
@@ -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<boost::asio::const_buffer> HttpMessage::ToBuffers() const {
|
||||
void HttpMessage::Prepare() {
|
||||
assert(!start_line_.empty());
|
||||
|
||||
std::vector<boost::asio::const_buffer> 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;
|
||||
|
||||
+8
-10
@@ -6,10 +6,7 @@
|
||||
#include <utility> // for move()
|
||||
#include <vector>
|
||||
|
||||
#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<boost::asio::const_buffer> 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
|
||||
|
||||
+2
-15
@@ -3,14 +3,14 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#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
|
||||
|
||||
+74
-12
@@ -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
|
||||
|
||||
+17
-12
@@ -1,7 +1,6 @@
|
||||
#ifndef WEBCC_HTTP_REQUEST_H_
|
||||
#define WEBCC_HTTP_REQUEST_H_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -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<std::string, HttpFile>& files() const {
|
||||
return files_;
|
||||
const std::vector<FormPart>& 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<FormPart>&& 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<std::string, HttpFile> files_;
|
||||
std::vector<FormPart> form_parts_;
|
||||
|
||||
std::string boundary_;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
@@ -14,7 +14,7 @@ HttpRequestPtr HttpRequestBuilder::Build() {
|
||||
auto request = std::make_shared<HttpRequest>(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
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string, HttpFile> files_;
|
||||
std::vector<FormPart> files_;
|
||||
|
||||
// Compress the request content.
|
||||
// NOTE: Most servers don't support compressed requests.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -34,6 +34,16 @@ private:
|
||||
|
||||
private:
|
||||
HttpRequest* request_;
|
||||
|
||||
enum Step {
|
||||
kStart,
|
||||
kBoundaryParsed,
|
||||
kHeadersParsed,
|
||||
kEnded,
|
||||
};
|
||||
Step step_ = kStart;
|
||||
|
||||
FormPart part_;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+15
-13
@@ -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
|
||||
|
||||
@@ -19,12 +19,16 @@ 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_;
|
||||
|
||||
@@ -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<char>* 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<char>* buffer) {
|
||||
|
||||
+1
-1
@@ -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_ += "&";
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user