diff --git a/CHANGELOG.md b/CHANGELOG.md index 38121340..f81b9769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Tidy up documentation * Add basic_parser_v1::reset * Fix handling of body_what::pause in basic_parser_v1 +* Add headers_parser API Changes: diff --git a/doc/quickref.xml b/doc/quickref.xml index 20feb6ab..38dadda9 100644 --- a/doc/quickref.xml +++ b/doc/quickref.xml @@ -34,6 +34,7 @@ basic_parser_v1 empty_body headers + headers_parser message message_headers parser_v1 @@ -65,6 +66,7 @@ prepare read swap + with_body write Type Traits diff --git a/include/beast/http/basic_parser_v1.hpp b/include/beast/http/basic_parser_v1.hpp index 039a42c1..0a5290a5 100644 --- a/include/beast/http/basic_parser_v1.hpp +++ b/include/beast/http/basic_parser_v1.hpp @@ -226,6 +226,9 @@ template class basic_parser_v1 : public detail::parser_base { private: + template + friend class basic_parser_v1; + using self = basic_parser_v1; typedef void(self::*pmf_t)(error_code&, boost::string_ref const&); @@ -284,15 +287,19 @@ private: bool upgrade_ : 1; // true if parser exited for upgrade public: - /// Copy constructor. - basic_parser_v1(basic_parser_v1 const&) = default; - - /// Copy assignment. - basic_parser_v1& operator=(basic_parser_v1 const&) = default; - /// Default constructor basic_parser_v1(); + /// Copy constructor. + template + basic_parser_v1(basic_parser_v1< + isRequest, OtherDerived> const& other); + + /// Copy assignment. + template + basic_parser_v1& operator=(basic_parser_v1< + isRequest, OtherDerived> const& other); + /** Set options on the parser. @param args One or more parser options to set. diff --git a/include/beast/http/headers_parser_v1.hpp b/include/beast/http/headers_parser_v1.hpp new file mode 100644 index 00000000..799d148d --- /dev/null +++ b/include/beast/http/headers_parser_v1.hpp @@ -0,0 +1,234 @@ +// +// Copyright (c) 2013-2016 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BEAST_HTTP_HEADERS_PARSER_V1_HPP +#define BEAST_HTTP_HEADERS_PARSER_V1_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace http { + +namespace detail { + +struct request_parser_base +{ + std::string method_; + std::string uri_; +}; + +struct response_parser_base +{ + std::string reason_; +}; + +} // detail + +/** A parser for HTTP/1 request and response headers. + + This class uses the HTTP/1 wire format parser to + convert a series of octets into a @ref request_headers + or @ref @response_headers. + + @note A new instance of the parser is required for each message. +*/ +template +class headers_parser_v1 + : public basic_parser_v1> + , private std::conditional::type +{ +public: + /// The type of message this parser produces. + using headers_type = + message_headers; + +private: + // VFALCO Check Headers requirements? + + std::string field_; + std::string value_; + headers_type h_; + bool flush_ = false; + +public: + /// Default constructor + headers_parser_v1() = default; + + /// Move constructor + headers_parser_v1(headers_parser_v1&&) = default; + + /// Copy constructor (disallowed) + headers_parser_v1(headers_parser_v1 const&) = delete; + + /// Move assignment (disallowed) + headers_parser_v1& operator=(headers_parser_v1&&) = delete; + + /// Copy assignment (disallowed) + headers_parser_v1& operator=(headers_parser_v1 const&) = delete; + + /** Construct the parser. + + @param args Forwarded to the message headers constructor. + */ +#if GENERATING_DOCS + template + explicit + headers_parser_v1(Args&&... args); +#else + template::type, headers_parser_v1>::value>> + explicit + headers_parser_v1(Arg1&& arg1, ArgN&&... argn) + : h_(std::forward(arg1), + std::forward(argn)...) + { + } +#endif + + /** Returns the parsed headers. + + Only valid if @ref complete would return `true`. + */ + headers_type const& + get() const + { + return h_; + } + + /** Returns the parsed headers. + + Only valid if @ref complete would return `true`. + */ + headers_type& + get() + { + return h_; + } + + /** Returns ownership of the parsed headers. + + Ownership is transferred to the caller. Only + valid if @ref complete would return `true`. + + Requires: + `message_headers` is @b MoveConstructible + */ + headers_type + release() + { + static_assert(std::is_move_constructible::value, + "MoveConstructible requirements not met"); + return std::move(h_); + } + +private: + friend class basic_parser_v1; + + void flush() + { + if(! flush_) + return; + flush_ = false; + BOOST_ASSERT(! field_.empty()); + h_.headers.insert(field_, value_); + field_.clear(); + value_.clear(); + } + + void on_start(error_code&) + { + } + + void on_method(boost::string_ref const& s, error_code&) + { + this->method_.append(s.data(), s.size()); + } + + void on_uri(boost::string_ref const& s, error_code&) + { + this->uri_.append(s.data(), s.size()); + } + + void on_reason(boost::string_ref const& s, error_code&) + { + this->reason_.append(s.data(), s.size()); + } + + void on_request_or_response(std::true_type) + { + h_.method = std::move(this->method_); + h_.url = std::move(this->uri_); + } + + void on_request_or_response(std::false_type) + { + h_.status = this->status_code(); + h_.reason = std::move(this->reason_); + } + + void on_request(error_code& ec) + { + on_request_or_response( + std::integral_constant{}); + } + + void on_response(error_code& ec) + { + on_request_or_response( + std::integral_constant{}); + } + + void on_field(boost::string_ref const& s, error_code&) + { + flush(); + field_.append(s.data(), s.size()); + } + + void on_value(boost::string_ref const& s, error_code&) + { + value_.append(s.data(), s.size()); + flush_ = true; + } + + void + on_headers(std::uint64_t, error_code&) + { + flush(); + h_.version = 10 * this->http_major() + this->http_minor(); + } + + body_what + on_body_what(std::uint64_t, error_code&) + { + return body_what::pause; + } + + void on_body(boost::string_ref const&, error_code&) + { + } + + void on_complete(error_code&) + { + } +}; + +} // http +} // beast + +#endif diff --git a/include/beast/http/impl/basic_parser_v1.ipp b/include/beast/http/impl/basic_parser_v1.ipp index a610a20b..069656b7 100644 --- a/include/beast/http/impl/basic_parser_v1.ipp +++ b/include/beast/http/impl/basic_parser_v1.ipp @@ -50,6 +50,55 @@ basic_parser_v1() init(); } +template +template +basic_parser_v1:: +basic_parser_v1(basic_parser_v1< + isRequest, OtherDerived> const& other) + : h_max_(other.h_max_) + , h_left_(other.h_left_) + , b_max_(other.b_max_) + , b_left_(other.b_left_) + , content_length_(other.content_length_) + , cb_(nullptr) + , s_(other.s_) + , flags_(other.flags_) + , fs_(other.fs_) + , pos_(other.pos_) + , http_major_(other.http_major_) + , http_minor_(other.http_minor_) + , status_code_(other.status_code_) + , upgrade_(other.upgrade_) +{ + BOOST_ASSERT(! other.cb_); +} + +template +template +auto +basic_parser_v1:: +operator=(basic_parser_v1< + isRequest, OtherDerived> const& other) -> + basic_parser_v1& +{ + BOOST_ASSERT(! other.cb_); + h_max_ = other.h_max_; + h_left_ = other.h_left_; + b_max_ = other.b_max_; + b_left_ = other.b_left_; + content_length_ = other.content_length_; + cb_ = nullptr; + s_ = other.s_; + flags_ = other.flags_; + fs_ = other.fs_; + pos_ = other.pos_; + http_major_ = other.http_major_; + http_minor_ = other.http_minor_; + status_code_ = other.status_code_; + upgrade_ = other.upgrade_; + return *this; +} + template bool basic_parser_v1:: @@ -942,6 +991,7 @@ write(boost::asio::const_buffer const& buffer, error_code& ec) case body_what::pause: return used(); } + --p; s_ = s_headers_done; // fall through } @@ -1198,6 +1248,7 @@ void basic_parser_v1:: reset() { + cb_ = nullptr; h_left_ = h_max_; b_left_ = b_max_; reset(std::integral_constant{}); diff --git a/include/beast/http/parser_v1.hpp b/include/beast/http/parser_v1.hpp index 7bbbc846..703326b2 100644 --- a/include/beast/http/parser_v1.hpp +++ b/include/beast/http/parser_v1.hpp @@ -8,8 +8,8 @@ #ifndef BEAST_HTTP_PARSER_V1_HPP #define BEAST_HTTP_PARSER_V1_HPP -#include #include +#include #include #include #include @@ -22,21 +22,6 @@ namespace beast { namespace http { -namespace detail { - -struct parser_request -{ - std::string method_; - std::string uri_; -}; - -struct parser_response -{ - std::string reason_; -}; - -} // detail - /** Skip body option. The options controls whether or not the parser expects to see a @@ -80,7 +65,8 @@ class parser_v1 : public basic_parser_v1> , private std::conditional::type + detail::request_parser_base, + detail::response_parser_base>::type { public: /// The type of message this parser produces. @@ -123,7 +109,7 @@ public: /** Construct the parser. - @param args A list of arguments forwarded to the message constructor. + @param args Forwarded to the message constructor. */ #if GENERATING_DOCS template @@ -141,7 +127,23 @@ public: } #endif - /// Set the expect body option. + /** Construct the parser from a headers parser. + + @param parser The headers parser to construct from. + @param args Forwarded to the message body constructor. + */ + template + explicit + parser_v1(headers_parser_v1&& parser, + Args&&... args) + : m_(parser.release(), std::forward(args)...) + { + static_cast>&>(*this) = parser; + } + + /// Set the skip body option. void set_option(skip_body const& o) { @@ -150,7 +152,7 @@ public: /** Returns the parsed message. - Only valid if `complete()` would return `true`. + Only valid if @ref complete would return `true`. */ message_type const& get() const @@ -160,7 +162,7 @@ public: /** Returns the parsed message. - Only valid if `complete()` would return `true`. + Only valid if @ref complete would return `true`. */ message_type& get() @@ -168,13 +170,13 @@ public: return m_; } - /** Returns the parsed message. + /** Returns ownership of the parsed message. - Ownership is transferred to the caller. - Only valid if `complete()` would return `true`. + Ownership is transferred to the caller. Only + valid if @ref complete` would return `true`. Requires: - `message` is MoveConstructible + `message` is @b MoveConstructible */ message_type release() @@ -217,6 +219,30 @@ private: this->reason_.append(s.data(), s.size()); } + void on_request_or_response(std::true_type) + { + m_.method = std::move(this->method_); + m_.url = std::move(this->uri_); + } + + void on_request_or_response(std::false_type) + { + m_.status = this->status_code(); + m_.reason = std::move(this->reason_); + } + + void on_request(error_code& ec) + { + on_request_or_response( + std::integral_constant{}); + } + + void on_response(error_code& ec) + { + on_request_or_response( + std::integral_constant{}); + } + void on_field(boost::string_ref const& s, error_code&) { flush(); @@ -229,18 +255,6 @@ private: flush_ = true; } - void set(std::true_type) - { - m_.method = std::move(this->method_); - m_.url = std::move(this->uri_); - } - - void set(std::false_type) - { - m_.status = this->status_code(); - m_.reason = std::move(this->reason_); - } - void on_headers(std::uint64_t, error_code& ec) { @@ -258,18 +272,6 @@ private: return body_what::normal; } - void on_request(error_code& ec) - { - set(std::integral_constant< - bool, isRequest>{}); - } - - void on_response(error_code& ec) - { - set(std::integral_constant< - bool, isRequest>{}); - } - void on_body(boost::string_ref const& s, error_code& ec) { r_->write(s.data(), s.size(), ec); @@ -280,6 +282,46 @@ private: } }; +/** Create a new parser from a headers parser. + + Associates a Body type with a headers parser, and returns + a new parser which parses a complete message object + containing the original message headers and a new body + of the specified body type. + + This function allows HTTP messages to be parsed in two stages. + First, the headers are parsed and control is returned. Then, + the caller can choose at run-time, the type of Body to + associate with the message. And finally, complete the parse + in a second call. + + @param parser The headers parser to construct from. Ownership + of the message headers in the headers parser is transferred + as if by call to @ref headers_parser_v1::release. + + @param args Forwarded to the body constructor of the message + in the new parser. + + @return A parser for a message with the specified @ref Body type. + + @par Example + @code + headers_parser ph; + ... + auto p = with_body(std::move(ph)); + ... + message m = p.release(); + @endcode +*/ +template +parser_v1 +with_body(headers_parser_v1& parser, + Args&&... args) +{ + return parser_v1( + std::move(parser), std::forward(args)...); +} + } // http } // beast diff --git a/test/Jamfile b/test/Jamfile index 41b94eac..a01bda0f 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -48,6 +48,7 @@ unit-test http-tests : http/concepts.cpp http/empty_body.cpp http/headers.cpp + http/headers_parser_v1.cpp http/message.cpp http/parse.cpp http/parse_error.cpp diff --git a/test/http/CMakeLists.txt b/test/http/CMakeLists.txt index eb47c628..03647196 100644 --- a/test/http/CMakeLists.txt +++ b/test/http/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable (http-tests concepts.cpp empty_body.cpp headers.cpp + headers_parser_v1.cpp message.cpp parse.cpp parse_error.cpp diff --git a/test/http/headers_parser_v1.cpp b/test/http/headers_parser_v1.cpp new file mode 100644 index 00000000..e15997dd --- /dev/null +++ b/test/http/headers_parser_v1.cpp @@ -0,0 +1,90 @@ +// +// Copyright (c) 2013-2016 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +// Test that header file is self-contained. +#include + +#include +#include +#include + +namespace beast { +namespace http { + +class headers_parser_v1_test : public beast::unit_test::suite +{ +public: + void testParser() + { + { + error_code ec; + headers_parser_v1 p; + BEAST_EXPECT(! p.complete()); + auto const n = p.write(boost::asio::buffer( + "GET / HTTP/1.1\r\n" + "User-Agent: test\r\n" + "\r\n" + ), ec); + BEAST_EXPECTS(! ec, ec.message()); + BEAST_EXPECT(p.complete()); + BEAST_EXPECT(n == 36); + } + { + error_code ec; + headers_parser_v1 p; + BEAST_EXPECT(! p.complete()); + auto const n = p.write(boost::asio::buffer( + "GET / HTTP/1.1\r\n" + "User-Agent: test\r\n" + "Content-Length: 5\r\n" + "\r\n" + "*****" + ), ec); + BEAST_EXPECT(n == 55); + BEAST_EXPECTS(! ec, ec.message()); + BEAST_EXPECT(p.complete()); + } + { + error_code ec; + headers_parser_v1 p; + BEAST_EXPECT(! p.complete()); + auto const n = p.write(boost::asio::buffer( + "HTTP/1.1 200 OK\r\n" + "Server: test\r\n" + "\r\n" + ), ec); + BEAST_EXPECT(n == 33); + BEAST_EXPECTS(! ec, ec.message()); + BEAST_EXPECT(p.complete()); + } + { + error_code ec; + headers_parser_v1 p; + BEAST_EXPECT(! p.complete()); + auto const n = p.write(boost::asio::buffer( + "HTTP/1.1 200 OK\r\n" + "Server: test\r\n" + "Content-Length: 5\r\n" + "\r\n" + "*****" + ), ec); + BEAST_EXPECT(n == 52); + BEAST_EXPECTS(! ec, ec.message()); + BEAST_EXPECT(p.complete()); + } + } + + void run() override + { + testParser(); + } +}; + +BEAST_DEFINE_TESTSUITE(headers_parser_v1,http,beast); + +} // http +} // beast diff --git a/test/http/parser_v1.cpp b/test/http/parser_v1.cpp index f684238e..7717d8ce 100644 --- a/test/http/parser_v1.cpp +++ b/test/http/parser_v1.cpp @@ -8,14 +8,21 @@ // Test that header file is self-contained. #include +#include #include +#include +#include #include +#include +#include #include namespace beast { namespace http { -class parser_v1_test : public beast::unit_test::suite +class parser_v1_test + : public beast::unit_test::suite + , public test::enable_yield_to { public: void testRegressions() @@ -46,6 +53,35 @@ public: } } + void testWithBody() + { + test::string_stream ss{ios_, + "GET / HTTP/1.1\r\n" + "User-Agent: test\r\n" + "Content-Length: 1\r\n" + "\r\n" + "*"}; + streambuf rb; + headers_parser_v1 p0; + parse(ss, rb, p0); + request_headers const& reqh = p0.get(); + BEAST_EXPECT(reqh.method == "GET"); + BEAST_EXPECT(reqh.url == "/"); + BEAST_EXPECT(reqh.version == 11); + BEAST_EXPECT(reqh.headers["User-Agent"] == "test"); + BEAST_EXPECT(reqh.headers["Content-Length"] == "1"); + parser_v1 p = + with_body(p0); + BEAST_EXPECT(p.get().method == "GET"); + BEAST_EXPECT(p.get().url == "/"); + BEAST_EXPECT(p.get().version == 11); + BEAST_EXPECT(p.get().headers["User-Agent"] == "test"); + BEAST_EXPECT(p.get().headers["Content-Length"] == "1"); + parse(ss, rb, p); + request req = p.release(); + BEAST_EXPECT(req.body == "*"); + } + void run() override { using boost::asio::buffer; @@ -104,6 +140,7 @@ public: } testRegressions(); + testWithBody(); } };