diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d45b13c..b22fe7d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ API Changes: * parser requires basic_fields * Refine FieldsReader concept +* message::prepare_payload replaces message::prepare Actions Required: @@ -21,6 +22,10 @@ Actions Required: * Implement chunked() and keep_alive() for user defined FieldsReader types. +* Change calls to msg.prepare to msg.prepare_payload. For messages + with a user-defined Fields, provide the function prepare_payload_impl + in the fields type according to the Fields requirements. + -------------------------------------------------------------------------------- Version 61: diff --git a/doc/5_02_message.qbk b/doc/5_02_message.qbk index f9391bbd..ab2a22df 100644 --- a/doc/5_02_message.qbk +++ b/doc/5_02_message.qbk @@ -151,7 +151,7 @@ request. Here we create an HTTP response indicating success. Note that this message has a body. The function -[link beast.ref.beast__http__message.prepare prepare] +[link beast.ref.beast__http__message.prepare_payload `message::prepare_payload`] automatically sets the Content-Length or Transfer-Encoding field depending on the content and type of the `body` member. The use of prepare is optional; these fields may also be set explicitly. diff --git a/doc/concept/Body.qbk b/doc/concept/Body.qbk index 15bcab8c..006600d8 100644 --- a/doc/concept/Body.qbk +++ b/doc/concept/Body.qbk @@ -53,7 +53,7 @@ In this table: [ If present, returns the serialized size of `v` not including any chunked transfer encoding. When this function is provided, - [link beast.ref.beast__http__message.prepare `message::prepare`] + [link beast.ref.beast__http__message.prepare_payload `message::prepare_payload`] will automatically set the content length field based on the value. Otherwise, the chunked transfer encoding will be set. ] diff --git a/doc/concept/Fields.qbk b/doc/concept/Fields.qbk index 9d4497ad..b97afa9d 100644 --- a/doc/concept/Fields.qbk +++ b/doc/concept/Fields.qbk @@ -25,6 +25,10 @@ In this table: * `s` is a value of type [link beast.ref.beast__string_view `string_view`]. +* `b` is a value of type `bool` + +* `n` is a value of type `boost::optional`. + [table Fields requirements [[expression][type][semantics, pre/post-conditions]] [ @@ -88,6 +92,29 @@ In this table: This function may throw `std::invalid_argument` if the operation is not supported by the container. ] +][ + [`a.prepare_payload_impl(b,n)`] + [] + [ + Adjusts the Content-Length and Transfer-Encoding fields to + account for the payload metadata indicated by `b` and `n` as + follows: + + * If `b` is `true`, the chunked Transfer-Encoding should be applied + to the end of the list of encodings if it is not already there + or if the field was not present. Any Content-Length fields should + be removed. + + * If `b` is `false` and `n` contains a value, set the Content-Length + field to `*n`, replacing any previous Content-Length fields. Remove + the chunked encoding from the end of the Transfer-Encoding field + if the field is present ahd chunked is the last encoding. + + * If `b` is `false` and `n` contains no value, remove all Content-Length + fields, and remove the chunked encoding from the end of the + Transfer-Encoding field if the field is present ahd chunked is the + last encoding. + ] ] ] @@ -132,6 +159,14 @@ protected: @note Only called for responses. */ string_view get_reason_impl() const; + + /** Updates the payload metadata. + + @param b `true` if chunked + + @param n The content length if known, otherwise `boost::none` + */ + void prepare_payload_impl(bool b, boost::optional n) }; ``` diff --git a/example/doc/http_examples.hpp b/example/doc/http_examples.hpp index 6279fb73..5716fbb1 100644 --- a/example/doc/http_examples.hpp +++ b/example/doc/http_examples.hpp @@ -320,7 +320,7 @@ void do_server_head( // set of headers that would be sent for a GET request, // including the Content-Length, except for the body. res.result(status::ok); - res.content_length(payload.size()); + res.set(field::content_length, payload.size()); // For GET requests, we include the body if(req.method() == verb::get) diff --git a/example/http-client-ssl/http_client_ssl.cpp b/example/http-client-ssl/http_client_ssl.cpp index 8c5cab72..59bcb83d 100644 --- a/example/http-client-ssl/http_client_ssl.cpp +++ b/example/http-client-ssl/http_client_ssl.cpp @@ -64,7 +64,7 @@ int main() req.set(http::field::host, host + ":" + boost::lexical_cast(sock.remote_endpoint().port())); req.set(http::field::user_agent, "Beast"); - req.prepare(); + req.prepare_payload(); // Write the HTTP request to the remote host http::write(stream, req, ec); diff --git a/example/http-client/http_client.cpp b/example/http-client/http_client.cpp index a17ddb9d..fe405462 100644 --- a/example/http-client/http_client.cpp +++ b/example/http-client/http_client.cpp @@ -55,7 +55,7 @@ int main() req.set(http::field::host, host + ":" + boost::lexical_cast(sock.remote_endpoint().port())); req.set(http::field::user_agent, "Beast"); - req.prepare(); + req.prepare_payload(); // Write the HTTP request to the remote host http::write(sock, req, ec); diff --git a/example/server-framework/file_service.hpp b/example/server-framework/file_service.hpp index 1b29f8eb..2fa2b1ca 100644 --- a/example/server-framework/file_service.hpp +++ b/example/server-framework/file_service.hpp @@ -225,7 +225,7 @@ private: res.set(beast::http::field::server, server_); res.set(beast::http::field::content_type, "text/html"); res.body = "The file was not found"; // VFALCO append rel_path - res.prepare(); + res.prepare_payload(); return res; } @@ -243,7 +243,7 @@ private: res.set(beast::http::field::server, server_); res.set(beast::http::field::content_type, mime_type(full_path)); res.body = full_path; - res.prepare(); + res.prepare_payload(); return res; } diff --git a/example/server-framework/http_base.hpp b/example/server-framework/http_base.hpp index aa95cd4f..6dddb079 100644 --- a/example/server-framework/http_base.hpp +++ b/example/server-framework/http_base.hpp @@ -50,7 +50,7 @@ protected: res.set(beast::http::field::server, server_name_); res.set(beast::http::field::content_type, "text/html"); res.body = "Bad request"; - res.prepare(); + res.prepare_payload(); return res; } diff --git a/include/beast/http/empty_body.hpp b/include/beast/http/empty_body.hpp index 684cac53..8ee8bb71 100644 --- a/include/beast/http/empty_body.hpp +++ b/include/beast/http/empty_body.hpp @@ -38,7 +38,7 @@ struct empty_body /// Returns the content length of the body in a message. static std::uint64_t - size(empty_body) + size(value_type) { return 0; } diff --git a/include/beast/http/fields.hpp b/include/beast/http/fields.hpp index 418cd989..623508f3 100644 --- a/include/beast/http/fields.hpp +++ b/include/beast/http/fields.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,6 @@ namespace http { as a `std::multiset`; there will be a separate value for each occurrence of the field name. - Meets the requirements of @b Fields @tparam Allocator The allocator to use. This must meet the @@ -48,7 +48,7 @@ namespace http { template class basic_fields { - static std::size_t constexpr max_static_start_line = 4096; + static std::size_t constexpr max_static_buffer = 4096; using off_t = std::uint16_t; @@ -591,22 +591,11 @@ protected: */ string_view get_reason_impl() const; - //-------------------------------------------------------------------------- - // - // for container - // - - /** Set the Content-Length field to the specified value. - - @note This is called by the @ref header implementation. + /** Adjusts the payload related fields */ - void content_length_impl(std::uint64_t n); - - /** Add chunked to the Transfer-Encoding field. - - @note This is called by the @ref header implementation. - */ - void set_chunked_impl(bool v); + void + prepare_payload_impl(bool chunked, + boost::optional size); private: template diff --git a/include/beast/http/impl/fields.ipp b/include/beast/http/impl/fields.ipp index 64cf1efc..bbfce4ce 100644 --- a/include/beast/http/impl/fields.ipp +++ b/include/beast/http/impl/fields.ipp @@ -9,6 +9,7 @@ #define BEAST_HTTP_IMPL_FIELDS_IPP #include +#include #include #include #include @@ -149,7 +150,7 @@ public: unsigned version, unsigned code); basic_fields const& f_; - static_string ss_; + static_string ss_; string_view sv_; std::string s_; bool chunked_; @@ -199,7 +200,7 @@ get_chunked() const f_[field::transfer_encoding]}; for(auto it = te.begin(); it != te.end();) { - auto next = std::next(it); + auto const next = std::next(it); if(next == te.end()) return iequals(*it, "chunked"); it = next; @@ -866,32 +867,119 @@ get_reason_impl() const return target_or_reason_; } -//--- +namespace detail { -template -inline +// Builds a new string with "chunked" maybe taken off the end +template void -basic_fields:: -content_length_impl(std::uint64_t n) +without_chunked_last(String& s, string_view const& tokens) { - erase(field::content_length); - insert(field::content_length, n); + token_list te{tokens}; + if(te.begin() != te.end()) + { + auto it = te.begin(); + auto next = std::next(it); + if(next == te.end()) + { + if(! iequals(*it, "chunked")) + s.append(it->data(), it->size()); + return; + } + s.append(it->data(), it->size()); + for(;;) + { + it = next; + next = std::next(it); + if(next == te.end()) + { + if(! iequals(*it, "chunked")) + { + s.append(", "); + s.append(it->data(), it->size()); + } + return; + } + s.append(", "); + s.append(it->data(), it->size()); + } + } } +} // detail + template -inline void basic_fields:: -set_chunked_impl(bool v) +prepare_payload_impl(bool chunked, + boost::optional size) { - // VFALCO We need to handle removing the chunked as well - BOOST_ASSERT(v); - auto it = find(field::transfer_encoding); - if(it == end()) - insert(field::transfer_encoding, "chunked"); + if(chunked) + { + BOOST_ASSERT(! size); + erase(field::content_length); + auto it = find(field::transfer_encoding); + if(it == end()) + { + set(field::transfer_encoding, "chunked"); + return; + } + + static_string temp; + if(it->value().size() <= temp.size() + 9) + { + temp.append(it->value().data(), it->value().size()); + temp.append(", chunked", 9); + set(field::transfer_encoding, temp); + } + else + { + std::string s; + s.reserve(it->value().size() + 9); + s.append(it->value().data(), it->value().size()); + s.append(", chunked", 9); + set(field::transfer_encoding, s); + } + return; + } + auto const clear_chunked = + [this]() + { + auto it = find(field::transfer_encoding); + if(it == end()) + return; + + // We have to just try it because we can't + // know ahead of time if there's enough room. + try + { + static_string temp; + detail::without_chunked_last(temp, it->value()); + if(! temp.empty()) + set(field::transfer_encoding, temp); + else + erase(field::transfer_encoding); + } + catch(std::length_error const&) + { + std::string s; + s.reserve(it->value().size()); + detail::without_chunked_last(s, it->value()); + if(! s.empty()) + set(field::transfer_encoding, s); + else + erase(field::transfer_encoding); + } + }; + if(size) + { + clear_chunked(); + set(field::content_length, *size); + } else - set(field::transfer_encoding, - it->value().to_string() + ", chunked"); + { + clear_chunked(); + erase(field::content_length); + } } //------------------------------------------------------------------------------ diff --git a/include/beast/http/impl/message.ipp b/include/beast/http/impl/message.ipp index 4660b2a1..37216f03 100644 --- a/include/beast/http/impl/message.ipp +++ b/include/beast/http/impl/message.ipp @@ -252,69 +252,53 @@ message(std::piecewise_construct_t, template boost::optional message:: -size() const +payload_size() const { - static_assert(is_body_reader::value, - "BodyReader requirements not met"); - - return size(detail::is_body_sized{}); + return payload_size(detail::is_body_sized{}); } template void message:: -content_length(std::uint64_t n) +prepare_payload(std::true_type) { - this->content_length_impl(n); -} - -template -void -message:: -prepare() -{ - prepare(typename header_type::is_request{}); -} - -template -inline -void -message:: -prepare(std::true_type) -{ - auto const n = size(); - if(this->method_ == verb::trace && ( - ! n || *n > 0)) + auto const n = payload_size(); + if(this->method() == verb::trace && (! n || *n > 0)) BOOST_THROW_EXCEPTION(std::invalid_argument{ "invalid request body"}); if(n) { if(*n > 0 || - this->method_ == verb::options || - this->method_ == verb::put || - this->method_ == verb::post - ) + this->method() == verb::options || + this->method() == verb::put || + this->method() == verb::post) { - this->content_length_impl(*n); + this->prepare_payload_impl(false, *n); + } + else + { + this->prepare_payload_impl(false, boost::none); } } else if(this->version >= 11) { - this->set_chunked_impl(true); + this->prepare_payload_impl(true, boost::none); + } + else + { + this->prepare_payload_impl(false, boost::none); } } template -inline void message:: -prepare(std::false_type) +prepare_payload(std::false_type) { - auto const n = size(); - if((status_class(this->result_) == - status_class::informational || - this->result_ == status::no_content || - this->result_ == status::not_modified)) + auto const n = payload_size(); + if((status_class(this->result()) == status_class::informational || + this->result() == status::no_content || + this->result() == status::not_modified)) { if(! n || *n > 0) // The response body MUST BE empty for this case @@ -322,9 +306,13 @@ prepare(std::false_type) "invalid response body"}); } if(n) - this->content_length_impl(*n); - else if(this->version >= 11) - this->set_chunked_impl(true); + { + this->prepare_payload_impl(false, *n); + } + else + { + this->prepare_payload_impl(true, boost::none); + } } //------------------------------------------------------------------------------ diff --git a/include/beast/http/message.hpp b/include/beast/http/message.hpp index 827f3618..f0e821c7 100644 --- a/include/beast/http/message.hpp +++ b/include/beast/http/message.hpp @@ -496,17 +496,8 @@ struct message : header is not inspected. */ boost::optional - size() const; + payload_size() const; - /** Set the Content-Length field. - - The value of the Content-Length field will be unconditionally - set to the specified number of octets. - - @param n The number of octets to set for the Content-Length field. - */ - void - content_length(std::uint64_t n); /** Prepare the message payload fields for the body. @@ -522,11 +513,14 @@ struct message : header req.target("/"); req.set(field::user_agent, "Beast"); req.body = "Hello, world!"; - req.prepare(); + req.prepare_payload(); @endcode */ void - prepare(); + prepare_payload() + { + prepare_payload(typename header_type::is_request{}); + } private: static_assert(is_body::value, @@ -552,22 +546,22 @@ private: } boost::optional - size(std::true_type) const + payload_size(std::true_type) const { return Body::size(body); } boost::optional - size(std::false_type) const + payload_size(std::false_type) const { return boost::none; } void - prepare(std::true_type); + prepare_payload(std::true_type); void - prepare(std::false_type); + prepare_payload(std::false_type); }; /// A typical HTTP request diff --git a/include/beast/websocket/impl/stream.ipp b/include/beast/websocket/impl/stream.ipp index a12bfbb7..048f4fc0 100644 --- a/include/beast/websocket/impl/stream.ipp +++ b/include/beast/websocket/impl/stream.ipp @@ -218,7 +218,7 @@ build_response(http::header +#include +#include #include #include #include @@ -397,6 +399,188 @@ public: } } + struct sized_body + { + using value_type = std::uint64_t; + + static + std::uint64_t + size(value_type const& v) + { + return v; + } + }; + + struct unsized_body + { + struct value_type {}; + }; + + void + testPreparePayload() + { + // GET, empty + { + request req; + req.version = 11; + req.method(verb::get); + + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req.count(field::transfer_encoding) == 0); + + req.set(field::content_length, "0"); + req.set(field::transfer_encoding, "chunked"); + req.prepare_payload(); + + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req.count(field::transfer_encoding) == 0); + + req.set(field::transfer_encoding, "deflate"); + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate"); + + req.set(field::transfer_encoding, "deflate, chunked"); + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate"); + } + + // GET, sized + { + request req; + req.version = 11; + req.method(verb::get); + req.body = 50; + + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "50"); + BEAST_EXPECT(req[field::transfer_encoding] == ""); + + req.set(field::content_length, "0"); + req.set(field::transfer_encoding, "chunked"); + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "50"); + BEAST_EXPECT(req.count(field::transfer_encoding) == 0); + + req.set(field::transfer_encoding, "deflate, chunked"); + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "50"); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate"); + } + + // PUT, empty + { + request req; + req.version = 11; + req.method(verb::put); + + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "0"); + BEAST_EXPECT(req.count(field::transfer_encoding) == 0); + + req.set(field::content_length, "50"); + req.set(field::transfer_encoding, "deflate, chunked"); + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "0"); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate"); + } + + // PUT, sized + { + request req; + req.version = 11; + req.method(verb::put); + req.body = 50; + + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "50"); + BEAST_EXPECT(req.count(field::transfer_encoding) == 0); + + req.set(field::content_length, "25"); + req.set(field::transfer_encoding, "deflate, chunked"); + req.prepare_payload(); + BEAST_EXPECT(req[field::content_length] == "50"); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate"); + } + + // POST, unsized + { + request req; + req.version = 11; + req.method(verb::post); + + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req[field::transfer_encoding] == "chunked"); + + req.set(field::transfer_encoding, "deflate"); + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate, chunked"); + } + + // POST, unsized HTTP/1.0 + { + request req; + req.version = 10; + req.method(verb::post); + + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req.count(field::transfer_encoding) == 0); + + req.set(field::transfer_encoding, "deflate"); + req.prepare_payload(); + BEAST_EXPECT(req.count(field::content_length) == 0); + BEAST_EXPECT(req[field::transfer_encoding] == "deflate"); + } + + // OK, empty + { + response res; + res.version = 11; + + res.prepare_payload(); + BEAST_EXPECT(res[field::content_length] == "0"); + BEAST_EXPECT(res.count(field::transfer_encoding) == 0); + + res.erase(field::content_length); + res.set(field::transfer_encoding, "chunked"); + res.prepare_payload(); + BEAST_EXPECT(res[field::content_length] == "0"); + BEAST_EXPECT(res.count(field::transfer_encoding) == 0); + } + + // OK, sized + { + response res; + res.version = 11; + res.body = 50; + + res.prepare_payload(); + BEAST_EXPECT(res[field::content_length] == "50"); + BEAST_EXPECT(res.count(field::transfer_encoding) == 0); + + res.erase(field::content_length); + res.set(field::transfer_encoding, "chunked"); + res.prepare_payload(); + BEAST_EXPECT(res[field::content_length] == "50"); + BEAST_EXPECT(res.count(field::transfer_encoding) == 0); + } + + // OK, unsized + { + response res; + res.version = 11; + + res.prepare_payload(); + BEAST_EXPECT(res.count(field::content_length) == 0); + BEAST_EXPECT(res[field::transfer_encoding] == "chunked"); + } + } + void run() override { testMembers(); @@ -404,6 +588,7 @@ public: testRFC2616(); testErase(); testContainer(); + testPreparePayload(); } }; diff --git a/test/http/string_view_body.cpp b/test/http/string_view_body.cpp index 3f5eb1a7..f709a8b9 100644 --- a/test/http/string_view_body.cpp +++ b/test/http/string_view_body.cpp @@ -31,7 +31,7 @@ public: req.version = 11; req.method(verb::post); req.target("/"); - req.prepare(); + req.prepare_payload(); static_buffer_n<512> b; ostream(b) << req; string_view const s{ diff --git a/test/http/write.cpp b/test/http/write.cpp index 6846eb88..0bccb8dd 100644 --- a/test/http/write.cpp +++ b/test/http/write.cpp @@ -532,7 +532,7 @@ public: m.version = 10; m.insert(field::user_agent, "test"); m.body = "*"; - m.prepare(); + m.prepare_payload(); BEAST_EXPECT(str(m) == "GET / HTTP/1.0\r\n" "User-Agent: test\r\n" @@ -549,7 +549,7 @@ public: m.version = 10; m.insert(field::user_agent, "test"); m.body = "*"; - m.prepare(); + m.prepare_payload(); test::string_ostream ss(ios_); error_code ec; write(ss, m, ec); @@ -569,7 +569,7 @@ public: m.version = 11; m.insert(field::user_agent, "test"); m.body = "*"; - m.prepare(); + m.prepare_payload(); BEAST_EXPECT(str(m) == "GET / HTTP/1.1\r\n" "User-Agent: test\r\n" @@ -586,7 +586,7 @@ public: m.version = 11; m.insert(field::user_agent, "test"); m.body = "*"; - m.prepare(); + m.prepare_payload(); test::string_ostream ss(ios_); error_code ec; write(ss, m, ec);