diff --git a/autotest/client_autotest.cc b/autotest/client_autotest.cc index d778cf2..7ad17cb 100644 --- a/autotest/client_autotest.cc +++ b/autotest/client_autotest.cc @@ -33,10 +33,7 @@ TEST(ClientTest, Head_RequestFunc) { try { auto r = session.Request(webcc::RequestBuilder{}. - Head("http://httpbin.org/get"). - Query("key1", "value1"). - Query("key2", "value2"). - Header("Accept", "application/json") + Head("http://httpbin.org/get") ()); EXPECT_EQ(webcc::Status::kOK, r->status()); @@ -65,6 +62,31 @@ TEST(ClientTest, Head_Shortcut) { } } +// Force Accept-Encoding to be "identity" so that HttpBin.org will include +// a Content-Length header in the response. +// This tests that the response with Content-Length while no body could be +// correctly parsed. +TEST(ClientTest, Head_AcceptEncodingIdentity) { + webcc::ClientSession session; + + try { + auto r = session.Request(webcc::RequestBuilder{}. + Head("http://httpbin.org/get"). + Header("Accept-Encoding", "identity") + ()); + + EXPECT_EQ(webcc::Status::kOK, r->status()); + EXPECT_EQ("OK", r->reason()); + + EXPECT_TRUE(r->HasHeader(webcc::headers::kContentLength)); + + EXPECT_EQ("", r->data()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} + // ----------------------------------------------------------------------------- static void AssertGet(webcc::ResponsePtr r) { diff --git a/examples/client_basics.cc b/examples/client_basics.cc index 8609f20..1d2ca45 100644 --- a/examples/client_basics.cc +++ b/examples/client_basics.cc @@ -8,14 +8,18 @@ int main() { webcc::ClientSession session; + webcc::ResponsePtr r; + try { - auto r = session.Request(webcc::RequestBuilder{}. - Get("http://httpbin.org/get"). - Query("key1", "value1"). - Query("key2", "value2"). - Date(). - Header("Accept", "application/json") - ()); + r = session.Head("http://httpbin.org/get"); + + r = session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/get"). + Query("key1", "value1"). + Query("key2", "value2"). + Date(). + Header("Accept", "application/json") + ()); r = session.Get("http://httpbin.org/get", { "key1", "value1", "key2", "value2" }, diff --git a/examples/file_upload_server.py b/examples/file_upload_server.py new file mode 100644 index 0000000..ac4e1c7 --- /dev/null +++ b/examples/file_upload_server.py @@ -0,0 +1,37 @@ +# A demo HTTP file upload server modified from: +# http://flask.pocoo.org/docs/1.0/patterns/fileuploads/#uploading-files +# Run: +# (Windows) +# $ set FLASK_APP=file_upload_server.py +# $ flask + +import os +from flask import Flask, flash, request, redirect, url_for +from flask import send_from_directory +from werkzeug.utils import secure_filename + + +UPLOAD_FOLDER = '/path/to/the/uploads' +app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + + +@app.route('/upload', methods=['POST']) +def upload_file(): + if request.method == 'POST': + for name, data in request.form.items(): + print(f'form: {name}, {data}') + + for name, file in request.files.items(): + if file: + secured_filename = secure_filename(file.filename) + print(f"file: {name}, {file.filename} ({secured_filename})") + file.save(os.path.join(app.config['UPLOAD_FOLDER'], + secured_filename)) + + return "OK" + + +@app.route('/uploads/') +def uploaded_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 1143b85..f82e544 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -2,6 +2,7 @@ set(UT_SRCS base64_unittest.cc + body_unittest.cc request_parser_unittest.cc url_unittest.cc utility_unittest.cc diff --git a/unittest/body_unittest.cc b/unittest/body_unittest.cc new file mode 100644 index 0000000..ce1bc6a --- /dev/null +++ b/unittest/body_unittest.cc @@ -0,0 +1,19 @@ +#include "gtest/gtest.h" + +#include "webcc/body.h" + +TEST(FormBodyTest, Payload) { + std::vector parts{ + std::make_shared("json", "{}", "application/json") + }; + + webcc::FormBody form_body{ parts, "123456" }; + + form_body.InitPayload(); + + auto payload = form_body.NextPayload(); + EXPECT_EQ(false, payload.empty()); + + payload = form_body.NextPayload(); + EXPECT_EQ(true, payload.empty()); +} diff --git a/webcc/body.cc b/webcc/body.cc index 2480df2..8433cc8 100644 --- a/webcc/body.cc +++ b/webcc/body.cc @@ -1,6 +1,7 @@ #include "webcc/body.h" #include "boost/algorithm/string.hpp" +#include "boost/core/ignore_unused.hpp" #include "webcc/logger.h" #include "webcc/utility.h" @@ -13,17 +14,6 @@ namespace webcc { // ----------------------------------------------------------------------------- -namespace misc_strings { - -// Literal strings can't be used because they have an extra '\0'. - -const char CRLF[] = { '\r', '\n' }; -const char DOUBLE_DASHES[] = { '-', '-' }; - -} // misc_strings - -// ----------------------------------------------------------------------------- - #if WEBCC_ENABLE_GZIP bool StringBody::Compress() { if (data_.size() <= kGzipThreshold) { @@ -45,7 +35,9 @@ void StringBody::InitPayload() { index_ = 0; } -Payload StringBody::NextPayload() { +Payload StringBody::NextPayload(bool free_previous) { + boost::ignore_unused(free_previous); + if (index_ == 0) { index_ = 1; return Payload{ boost::asio::buffer(data_) }; @@ -57,27 +49,8 @@ Payload StringBody::NextPayload() { // - The data will be truncated if it's too large to display. // - Binary content will not be dumped (TODO). void StringBody::Dump(std::ostream& os, const std::string& prefix) const { - if (data_.empty()) { - return; - } - - // Split by EOL to achieve more readability. - std::vector lines; - boost::split(lines, data_, boost::is_any_of("\n")); - - std::size_t size = 0; - - for (const std::string& line : lines) { - os << prefix; - - if (line.size() + size > kMaxDumpSize) { - os.write(line.c_str(), kMaxDumpSize - size); - os << "..." << std::endl; - break; - } else { - os << line << std::endl; - size += line.size(); - } + if (!data_.empty()) { + utility::DumpByLine(data_, os, prefix); } } @@ -102,43 +75,62 @@ std::size_t FormBody::GetSize() const { } void FormBody::Dump(std::ostream& os, const std::string& prefix) const { - // TODO + for (auto& part : parts_) { + os << prefix << "--" << boundary_ << std::endl; + part->Dump(os, prefix); + } + os << prefix << "--" << boundary_ << "--" << std::endl; } void FormBody::InitPayload() { index_ = 0; } -// TODO: Clear previous payload memory. -Payload FormBody::NextPayload() { +Payload FormBody::NextPayload(bool free_previous) { Payload payload; + + // Free previous payload. + if (free_previous) { + if (index_ > 0) { + Free(index_ - 1); + } + } if (index_ < parts_.size()) { - auto& part = parts_[index_]; AddBoundary(&payload); - part->Prepare(&payload); + parts_[index_]->Prepare(&payload); if (index_ + 1 == parts_.size()) { AddBoundaryEnd(&payload); } } + ++index_; + return payload; } void FormBody::AddBoundary(Payload* payload) { using boost::asio::buffer; - payload->push_back(buffer(misc_strings::DOUBLE_DASHES)); + + payload->push_back(buffer(literal_buffers::DOUBLE_DASHES)); payload->push_back(buffer(boundary_)); - payload->push_back(buffer(misc_strings::CRLF)); + payload->push_back(buffer(literal_buffers::CRLF)); } void FormBody::AddBoundaryEnd(Payload* payload) { using boost::asio::buffer; - payload->push_back(buffer(misc_strings::DOUBLE_DASHES)); + + payload->push_back(buffer(literal_buffers::DOUBLE_DASHES)); payload->push_back(buffer(boundary_)); - payload->push_back(buffer(misc_strings::DOUBLE_DASHES)); - payload->push_back(buffer(misc_strings::CRLF)); + payload->push_back(buffer(literal_buffers::DOUBLE_DASHES)); + payload->push_back(buffer(literal_buffers::CRLF)); +} + +void FormBody::Free(std::size_t index) { + if (index < parts_.size()) { + parts_[index]->Free(); + } } } // namespace webcc diff --git a/webcc/body.h b/webcc/body.h index c5874a0..6401e25 100644 --- a/webcc/body.h +++ b/webcc/body.h @@ -3,7 +3,7 @@ #include #include -#include // for move() +#include #include "webcc/common.h" @@ -38,11 +38,12 @@ public: // InitPayload(); // for (auto p = NextPayload(); !p.empty(); p = NextPayload()) { // } - virtual void InitPayload() {} + virtual void InitPayload() { + } // Get the next payload. // An empty payload returned indicates the end. - virtual Payload NextPayload() { + virtual Payload NextPayload(bool free_previous = false) { return {}; } @@ -77,7 +78,7 @@ public: void InitPayload() override; - Payload NextPayload() override; + Payload NextPayload(bool free_previous = false) override; void Dump(std::ostream& os, const std::string& prefix) const override; @@ -93,8 +94,7 @@ private: // Multi-part form body for request. class FormBody : public Body { public: - FormBody(const std::vector& parts, - const std::string& boundary); + FormBody(const std::vector& parts, const std::string& boundary); std::size_t GetSize() const override; @@ -104,7 +104,7 @@ public: void InitPayload() override; - Payload NextPayload() override; + Payload NextPayload(bool free_previous = false) override; void Dump(std::ostream& os, const std::string& prefix) const override; @@ -112,6 +112,8 @@ private: void AddBoundary(Payload* payload); void AddBoundaryEnd(Payload* payload); + void Free(std::size_t index); + private: std::vector parts_; std::string boundary_; diff --git a/webcc/client.cc b/webcc/client.cc index 46a0a1e..c0a10e3 100644 --- a/webcc/client.cc +++ b/webcc/client.cc @@ -18,6 +18,17 @@ Client::Client() Error Client::Request(RequestPtr request, bool connect) { Restart(); + // Response to HEAD could also have Content-Length. + // Set this flag to skip the reading and parsing of the body. + // The test against HttpBin.org shows that: + // - If request.Accept-Encoding is "gzip, deflate", the response doesn't + // have Content-Length; + // - If request.Accept-Encoding is "identity", the response do have + // Content-Length. + if (request->method() == methods::kHead) { + response_parser_.set_ignroe_body(true); + } + if (connect) { // No existing socket connection was specified, create a new one. Connect(request); @@ -131,13 +142,12 @@ void Client::WriteReqeust(RequestPtr request) { if (socket_->Write(request->GetPayload(), &ec)) { // Write request body. - if (request->body()) { - auto body = request->body(); - body->InitPayload(); - for (auto p = body->NextPayload(); !p.empty(); p = body->NextPayload()) { - if (!socket_->Write(p, &ec)) { - break; - } + auto body = request->body(); + body->InitPayload(); + for (auto p = body->NextPayload(true); !p.empty(); + p = body->NextPayload(true)) { + if (!socket_->Write(p, &ec)) { + break; } } } diff --git a/webcc/common.cc b/webcc/common.cc index c567aa6..7e7ef8b 100644 --- a/webcc/common.cc +++ b/webcc/common.cc @@ -3,47 +3,14 @@ #include #include "boost/algorithm/string.hpp" -#include "boost/filesystem/fstream.hpp" #include "webcc/logger.h" #include "webcc/utility.h" -namespace bfs = boost::filesystem; - namespace webcc { // ----------------------------------------------------------------------------- -namespace misc_strings { - -// Literal strings can't be used because they have an extra '\0'. - -const char HEADER_SEPARATOR[] = { ':', ' ' }; -const char CRLF[] = { '\r', '\n' }; - -} // misc_strings - -// ----------------------------------------------------------------------------- - -bool ReadFile(const Path& path, std::string* output) { - // Flag "ate": seek to the end of stream immediately after open. - bfs::ifstream stream{ path, std::ios::binary | std::ios::ate }; - if (stream.fail()) { - return false; - } - - auto size = stream.tellg(); - output->resize(static_cast(size), '\0'); - stream.seekg(std::ios::beg); - stream.read(&(*output)[0], size); - if (stream.fail()) { - return false; - } - return true; -} - -// ----------------------------------------------------------------------------- - void Headers::Set(const std::string& key, const std::string& value) { auto it = Find(key); if (it != headers_.end()) { @@ -66,8 +33,7 @@ bool Headers::Has(const std::string& key) const { return const_cast(this)->Find(key) != headers_.end(); } -const std::string& Headers::Get(const std::string& key, - bool* existed) const { +const std::string& Headers::Get(const std::string& key, bool* existed) const { auto it = const_cast(this)->Find(key); if (existed != nullptr) { @@ -204,11 +170,7 @@ bool ContentDisposition::Init(const std::string& str) { FormPart::FormPart(const std::string& name, const Path& path, const std::string& media_type) - : name_(name), media_type_(media_type) { - if (!ReadFile(path, &data_)) { - throw Error{ Error::kFileError, "Cannot read the file." }; - } - + : name_(name), path_(path), media_type_(media_type) { // Determine file name from file path. // TODO: encoding file_name_ = path.filename().string(std::codecvt_utf8()); @@ -229,6 +191,12 @@ FormPart::FormPart(const std::string& name, std::string&& data, void FormPart::Prepare(Payload* payload) { using boost::asio::buffer; + if (data_.empty() && !path_.empty()) { + if (!utility::ReadFile(path_, &data_)) { + throw Error{ Error::kFileError, "Cannot read the file" }; + } + } + // NOTE: // The payload buffers don't own the memory. // It depends on some existing variables/objects to keep the memory. @@ -240,18 +208,23 @@ void FormPart::Prepare(Payload* payload) { for (const Header& h : headers_.data()) { payload->push_back(buffer(h.first)); - payload->push_back(buffer(misc_strings::HEADER_SEPARATOR)); + payload->push_back(buffer(literal_buffers::HEADER_SEPARATOR)); payload->push_back(buffer(h.second)); - payload->push_back(buffer(misc_strings::CRLF)); + payload->push_back(buffer(literal_buffers::CRLF)); } - payload->push_back(buffer(misc_strings::CRLF)); + payload->push_back(buffer(literal_buffers::CRLF)); if (!data_.empty()) { payload->push_back(buffer(data_)); } - payload->push_back(buffer(misc_strings::CRLF)); + payload->push_back(buffer(literal_buffers::CRLF)); +} + +void FormPart::Free() { + data_.clear(); + data_.shrink_to_fit(); } std::size_t FormPart::GetSize() { @@ -263,19 +236,46 @@ std::size_t FormPart::GetSize() { for (const Header& h : headers_.data()) { size += h.first.size(); - size += sizeof(misc_strings::HEADER_SEPARATOR); + size += sizeof(literal_buffers::HEADER_SEPARATOR); size += h.second.size(); - size += sizeof(misc_strings::CRLF); + size += sizeof(literal_buffers::CRLF); } - size += sizeof(misc_strings::CRLF); + size += sizeof(literal_buffers::CRLF); - size += data_.size(); + size += GetDataSize(); - size += sizeof(misc_strings::CRLF); + size += sizeof(literal_buffers::CRLF); return size; } +std::size_t FormPart::GetDataSize() { + if (!data_.empty()) { + return data_.size(); + } + + auto size = utility::TellSize(path_); + if (size == kInvalidLength) { + throw Error{ Error::kFileError, "Cannot read the file" }; + } + + return size; +} + +void FormPart::Dump(std::ostream& os, const std::string& prefix) const { + for (auto& h : headers_.data()) { + os << prefix << h.first << ": " << h.second << std::endl; + } + + os << prefix << std::endl; + + if (!path_.empty()) { + os << prefix << "" << std::endl; + } else { + utility::DumpByLine(data_, os, prefix); + } +} + void FormPart::SetHeaders() { // Header: Content-Disposition diff --git a/webcc/common.h b/webcc/common.h index e9021d7..2cd3e55 100644 --- a/webcc/common.h +++ b/webcc/common.h @@ -6,24 +6,12 @@ #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; - -// Read entire file into string. -bool ReadFile(const Path& path, std::string* output); - -// ----------------------------------------------------------------------------- - using Header = std::pair; class Headers { @@ -211,10 +199,19 @@ public: // API: CLIENT void Prepare(Payload* payload); - // Get the payload size. + // Free the memory of the data. + void Free(); + + // Get the size of the whole payload. // Used by the request to calculate content length. std::size_t GetSize(); + // Get the size of the data. + std::size_t GetDataSize(); + + // Dump to output stream for logging purpose. + void Dump(std::ostream& os, const std::string& prefix) const; + private: // Generate headers from properties. void SetHeaders(); @@ -226,6 +223,9 @@ private: // the name will be "file1". std::string name_; + // The path of the file to post. + Path path_; + // The original local file name. // E.g., "baby.jpg". std::string file_name_; diff --git a/webcc/connection.cc b/webcc/connection.cc index cb1c79b..f2934d3 100644 --- a/webcc/connection.cc +++ b/webcc/connection.cc @@ -1,6 +1,6 @@ #include "webcc/connection.h" -#include // for move() +#include #include "boost/asio/write.hpp" diff --git a/webcc/globals.cc b/webcc/globals.cc index 45ecb44..97bbec5 100644 --- a/webcc/globals.cc +++ b/webcc/globals.cc @@ -8,6 +8,16 @@ namespace webcc { // ----------------------------------------------------------------------------- +namespace literal_buffers { + +const char HEADER_SEPARATOR[2] = { ':', ' ' }; +const char CRLF[2] = { '\r', '\n' }; +const char DOUBLE_DASHES[2] = { '-', '-' }; + +} // namespace literal_buffers + +// ----------------------------------------------------------------------------- + namespace media_types { std::string FromExtension(const std::string& ext) { diff --git a/webcc/globals.h b/webcc/globals.h index d854fc0..acf37ca 100644 --- a/webcc/globals.h +++ b/webcc/globals.h @@ -7,6 +7,9 @@ #include #include +#include "boost/asio/buffer.hpp" // for const_buffer +#include "boost/filesystem/path.hpp" + #include "webcc/config.h" // ----------------------------------------------------------------------------- @@ -50,6 +53,10 @@ using Strings = std::vector; // Could also be considered as arguments, so named as UrlArgs. using UrlArgs = std::vector; +using Path = boost::filesystem::path; + +using Payload = std::vector; + // ----------------------------------------------------------------------------- const char* const kCRLF = "\r\n"; @@ -74,6 +81,19 @@ const std::size_t kGzipThreshold = 1400; // ----------------------------------------------------------------------------- +namespace literal_buffers { + +// Buffers for composing payload. +// Literal strings can't be used because they have an extra '\0'. + +extern const char HEADER_SEPARATOR[2]; +extern const char CRLF[2]; +extern const char DOUBLE_DASHES[2]; + +} // namespace literal_buffers + +// ----------------------------------------------------------------------------- + namespace methods { // HTTP methods (verbs) in string. @@ -160,7 +180,7 @@ enum class ContentEncoding { // Error (or exception) for the client. class Error { -public: + public: enum Code { kOK = 0, kSyntaxError, @@ -174,27 +194,37 @@ public: kDataError, }; -public: + public: Error(Code code = kOK, const std::string& message = "") : code_(code), message_(message), timeout_(false) { } - Code code() const { return code_; } + Code code() const { + return code_; + } - const std::string& message() const { return message_; } + const std::string& message() const { + return message_; + } void Set(Code code, const std::string& message) { code_ = code; message_ = message; } - bool timeout() const { return timeout_; } + bool timeout() const { + return timeout_; + } - void set_timeout(bool timeout) { timeout_ = timeout; } + void set_timeout(bool timeout) { + timeout_ = timeout; + } - operator bool() const { return code_ != kOK; } + operator bool() const { + return code_ != kOK; + } -private: + private: Code code_; std::string message_; bool timeout_; diff --git a/webcc/message.cc b/webcc/message.cc index 191ee78..ed7cd26 100644 --- a/webcc/message.cc +++ b/webcc/message.cc @@ -9,19 +9,6 @@ namespace webcc { -// ----------------------------------------------------------------------------- - -namespace misc_strings { - -// Literal strings can't be used because they have an extra '\0'. - -const char HEADER_SEPARATOR[] = { ':', ' ' }; -const char CRLF[] = { '\r', '\n' }; - -} // misc_strings - -// ----------------------------------------------------------------------------- - Message::Message() : body_(new Body{}), content_length_(kInvalidLength) { } @@ -111,16 +98,16 @@ Payload Message::GetPayload() const { Payload payload; payload.push_back(buffer(start_line_)); - payload.push_back(buffer(misc_strings::CRLF)); + payload.push_back(buffer(literal_buffers::CRLF)); for (const Header& h : headers_.data()) { payload.push_back(buffer(h.first)); - payload.push_back(buffer(misc_strings::HEADER_SEPARATOR)); + payload.push_back(buffer(literal_buffers::HEADER_SEPARATOR)); payload.push_back(buffer(h.second)); - payload.push_back(buffer(misc_strings::CRLF)); + payload.push_back(buffer(literal_buffers::CRLF)); } - payload.push_back(buffer(misc_strings::CRLF)); + payload.push_back(buffer(literal_buffers::CRLF)); return payload; } diff --git a/webcc/message.h b/webcc/message.h index 8b027b8..1d02dc6 100644 --- a/webcc/message.h +++ b/webcc/message.h @@ -3,7 +3,7 @@ #include #include -#include // for move() +#include #include #include "webcc/body.h" diff --git a/webcc/parser.cc b/webcc/parser.cc index 2ce8f44..50d4011 100644 --- a/webcc/parser.cc +++ b/webcc/parser.cc @@ -60,11 +60,12 @@ bool Parser::Parse(const char* data, std::size_t length) { if (!header_ended_) { LOG_INFO("HTTP headers will continue in next read."); return true; - } else { - LOG_INFO("HTTP headers just ended."); - // NOTE: The left data, if any, is still in the pending data. - return ParseContent("", 0); } + + LOG_INFO("HTTP headers just ended."); + + // The left data, if any, is still in the pending data. + return ParseContent("", 0); } void Parser::Reset() { diff --git a/webcc/request_handler.cc b/webcc/request_handler.cc index 60c84a3..d2d089d 100644 --- a/webcc/request_handler.cc +++ b/webcc/request_handler.cc @@ -2,7 +2,7 @@ #include #include -#include // for move() +#include #include "boost/algorithm/string.hpp" #include "boost/filesystem/fstream.hpp" @@ -11,6 +11,7 @@ #include "webcc/request.h" #include "webcc/response.h" #include "webcc/url.h" +#include "webcc/utility.h" #if WEBCC_ENABLE_GZIP #include "webcc/gzip.h" @@ -20,8 +21,7 @@ namespace bfs = boost::filesystem; namespace webcc { -RequestHandler::RequestHandler(const Path& doc_root) - : doc_root_(doc_root) { +RequestHandler::RequestHandler(const Path& doc_root) : doc_root_(doc_root) { } bool RequestHandler::Route(const std::string& url, ViewPtr view, @@ -184,7 +184,7 @@ bool RequestHandler::ServeStatic(ConnectionPtr connection) { Path p = doc_root_ / path; std::string data; - if (!ReadFile(p, &data)) { + if (!utility::ReadFile(p, &data)) { connection->SendResponse(Status::kNotFound); return false; } diff --git a/webcc/response_parser.cc b/webcc/response_parser.cc index f149d5e..d48f9fd 100644 --- a/webcc/response_parser.cc +++ b/webcc/response_parser.cc @@ -73,4 +73,12 @@ bool ResponseParser::ParseStartLine(const std::string& line) { return true; } +bool ResponseParser::ParseContent(const char* data, std::size_t length) { + if (ignroe_body_) { + Finish(); + return true; + } + return Parser::ParseContent(data, length); +} + } // namespace webcc diff --git a/webcc/response_parser.h b/webcc/response_parser.h index 57f98bd..5498a06 100644 --- a/webcc/response_parser.h +++ b/webcc/response_parser.h @@ -17,12 +17,24 @@ public: void Init(Response* response); + void set_ignroe_body(bool ignroe_body) { + ignroe_body_ = ignroe_body; + } + private: // Parse HTTP start line; E.g., "HTTP/1.1 200 OK". bool ParseStartLine(const std::string& line) override; + // Override to allow to ignore the body of the response for HEAD request. + bool ParseContent(const char* data, std::size_t length) override; + +private: // The result response message. Response* response_; + + // The response for HEAD request could also have `Content-Length` header, + // set this flag to ignore it. + bool ignroe_body_ = false; }; } // namespace webcc diff --git a/webcc/utility.cc b/webcc/utility.cc index 0edfac3..ec25386 100644 --- a/webcc/utility.cc +++ b/webcc/utility.cc @@ -5,11 +5,14 @@ #include #include "boost/algorithm/string.hpp" +#include "boost/filesystem/fstream.hpp" #include "boost/uuid/random_generator.hpp" #include "boost/uuid/uuid_io.hpp" #include "webcc/version.h" +namespace bfs = boost::filesystem; + namespace webcc { namespace utility { @@ -48,5 +51,52 @@ bool SplitKV(const std::string& str, char delimiter, return true; } +std::size_t TellSize(const Path& path) { + // Flag "ate": seek to the end of stream immediately after open. + bfs::ifstream stream{ path, std::ios::binary | std::ios::ate }; + if (stream.fail()) { + return kInvalidLength; + } + return static_cast(stream.tellg()); +} + +bool ReadFile(const Path& path, std::string* output) { + // Flag "ate": seek to the end of stream immediately after open. + bfs::ifstream stream{ path, std::ios::binary | std::ios::ate }; + if (stream.fail()) { + return false; + } + + auto size = stream.tellg(); + output->resize(static_cast(size), '\0'); + stream.seekg(std::ios::beg); + stream.read(&(*output)[0], size); + if (stream.fail()) { + return false; + } + return true; +} + +void DumpByLine(const std::string& data, std::ostream& os, + const std::string& prefix) { + std::vector lines; + boost::split(lines, data, boost::is_any_of("\n")); + + std::size_t size = 0; + + for (const std::string& line : lines) { + os << prefix; + + if (line.size() + size > kMaxDumpSize) { + os.write(line.c_str(), kMaxDumpSize - size); + os << "..." << std::endl; + break; + } else { + os << line << std::endl; + size += line.size(); + } + } +} + } // namespace utility } // namespace webcc diff --git a/webcc/utility.h b/webcc/utility.h index 01a89f3..9c76cc2 100644 --- a/webcc/utility.h +++ b/webcc/utility.h @@ -3,6 +3,8 @@ #include +#include "webcc/globals.h" + namespace webcc { namespace utility { @@ -22,6 +24,18 @@ std::string GetTimestamp(); bool SplitKV(const std::string& str, char delimiter, std::string* key, std::string* value); +// Tell the size in bytes of the given file. +// Return kInvalidLength (-1) on failure. +std::size_t TellSize(const Path& path); + +// Read entire file into string. +bool ReadFile(const Path& path, std::string* output); + +// Dump the string data line by line to achieve more readability. +// Also limit the maximum size of the data to be dumped. +void DumpByLine(const std::string& data, std::ostream& os, + const std::string& prefix); + } // namespace utility } // namespace webcc