From 5b8bb822ba6f12cc8ad0f7ee72693a98c075d7c2 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Thu, 31 Jul 2014 15:45:27 -0700 Subject: [PATCH] HTTP support improvements: * RFC2616 compliance * Case insensitive equality, inequality operators for strings * Improvements to http::parser * Tidy up HTTP method enumeration --- beast/http/HTTP.unity.cpp | 5 +- beast/http/basic_message.h | 509 ++++++++++++++++++++++ beast/http/impl/message_parser.cpp | 224 ---------- beast/http/impl/method.cpp | 124 ++++++ beast/http/impl/parser.cpp | 232 ++++++++++ beast/http/method.h | 15 + beast/http/{message_parser.h => parser.h} | 122 +++++- beast/http/rfc2616.h | 239 ++++++++++ beast/http/tests/basic_message.test.cpp | 45 +- beast/http/tests/rfc2616.test.cpp | 93 ++++ beast/utility/ci_char_traits.h | 43 ++ 11 files changed, 1389 insertions(+), 262 deletions(-) create mode 100644 beast/http/basic_message.h delete mode 100644 beast/http/impl/message_parser.cpp create mode 100644 beast/http/impl/method.cpp create mode 100644 beast/http/impl/parser.cpp rename beast/http/{message_parser.h => parser.h} (53%) create mode 100644 beast/http/rfc2616.h create mode 100644 beast/http/tests/rfc2616.test.cpp diff --git a/beast/http/HTTP.unity.cpp b/beast/http/HTTP.unity.cpp index 07bd3cd69f..0e748e61c1 100644 --- a/beast/http/HTTP.unity.cpp +++ b/beast/http/HTTP.unity.cpp @@ -24,13 +24,16 @@ #include #include #include -#include +#include #include +#include #include #include +#include #include #include #include +#include #include diff --git a/beast/http/basic_message.h b/beast/http/basic_message.h new file mode 100644 index 0000000000..84629466c8 --- /dev/null +++ b/beast/http/basic_message.h @@ -0,0 +1,509 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef BEAST_HTTP_MESSAGE_H_INCLUDED +#define BEAST_HTTP_MESSAGE_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace http { + +class basic_message +{ +public: + class parser; + + typedef boost::system::error_code error_code; + +private: + class element + : public boost::intrusive::set_base_hook < + boost::intrusive::link_mode < + boost::intrusive::normal_link> + > + , public boost::intrusive::list_base_hook < + boost::intrusive::link_mode < + boost::intrusive::normal_link> + > + { + public: + element (std::string const& f, std::string const& v) + : field (f) + , value (v) + { + } + + std::string field; + std::string value; + }; + + struct less : private beast::ci_less + { + template + bool + operator() (String const& lhs, element const& rhs) const + { + return beast::ci_less::operator() (lhs, rhs.field); + } + + template + bool + operator() (element const& lhs, String const& rhs) const + { + return beast::ci_less::operator() (lhs.field, rhs); + } + }; + + class headers_t : private less + { + private: + typedef boost::intrusive::make_list + >::type list_t; + + typedef boost::intrusive::make_set + >::type set_t; + + list_t list_; + set_t set_; + + public: + typedef list_t::const_iterator iterator; + + ~headers_t() + { + clear(); + } + + headers_t() = default; + + headers_t (headers_t&& other) + : list_ (std::move(other.list_)) + , set_ (std::move(other.set_)) + { + + } + + headers_t (headers_t const& other) + { + for (auto const& e : other.list_) + append (e.field, e.value); + } + + headers_t& + operator= (headers_t&& other) + { + list_ = std::move(other.list_); + set_ = std::move(other.set_); + } + + headers_t& + operator= (headers_t const& other) + { + clear(); + for (auto const& e : other.list_) + append (e.field, e.value); + } + + void + clear() + { + for (auto iter (list_.begin()); iter != list_.end();) + { + element* const p (&*iter); + ++iter; + delete p; + } + } + + iterator + begin() const + { + return list_.cbegin(); + } + + iterator + end() const + { + return list_.cend(); + } + + iterator + cbegin() const + { + return list_.cbegin(); + } + + iterator + cend() const + { + return list_.cend(); + } + + iterator + find (std::string const& field) const + { + auto const iter (set_.find (field, + std::cref(static_cast(*this)))); + if (iter == set_.end()) + return list_.end(); + return list_.iterator_to (*iter); + } + + std::string const& + operator[] (std::string const& field) const + { + static std::string none; + auto const found (find (field)); + if (found == end()) + return none; + return found->value; + } + + void + append (std::string const& field, std::string const& value) + { + set_t::insert_commit_data d; + auto const result (set_.insert_check (field, + std::cref(static_cast(*this)), d)); + if (result.second) + { + element* const p (new element (field, value)); + list_.push_back (*p); + auto const iter (set_.insert_commit (*p, d)); + return; + } + // If field already exists, append comma + // separated value as per RFC2616 section 4.2 + auto& cur (result.first->value); + cur.reserve (cur.size() + 1 + value.size()); + cur.append (1, ','); + cur.append (value); + } + }; + + bool request_; + + // request + beast::http::method_t method_; + std::string url_; + + // response + int status_; + std::string reason_; + + // message + std::pair version_; + bool keep_alive_; + bool upgrade_; + +public: + ~basic_message() = default; + + basic_message() + : request_ (true) + , method_ (beast::http::method_t::http_get) + , url_ ("/") + , status_ (200) + , version_ (1, 1) + , keep_alive_ (false) + , upgrade_ (false) + { + } + + basic_message (basic_message&& other) + : request_ (true) + , method_ (std::move(other.method_)) + , url_ (std::move(other.url_)) + , status_ (other.status_) + , reason_ (std::move(other.reason_)) + , version_ (other.version_) + , keep_alive_ (other.keep_alive_) + , upgrade_ (other.upgrade_) + , headers (std::move(other.headers)) + { + } + + bool + request() const + { + return request_; + } + + void + request (bool value) + { + request_ = value; + } + + // Request + + void + method (beast::http::method_t http_method) + { + method_ = http_method; + } + + beast::http::method_t + method() const + { + return method_; + } + + void + url (std::string const& s) + { + url_ = s; + } + + std::string const& + url() const + { + return url_; + } + + /** Returns `false` if this is not the last message. + When keep_alive returns `false`: + * Server roles respond with a "Connection: close" header. + * Client roles close the connection. + */ + bool + keep_alive() const + { + return keep_alive_; + } + + /** Set the keep_alive setting. */ + void + keep_alive (bool value) + { + keep_alive_ = value; + } + + /** Returns `true` if this is an HTTP Upgrade message. + @note Upgrade messages have no content body. + */ + bool + upgrade() const + { + return upgrade_; + } + + /** Set the upgrade setting. */ + void + upgrade (bool value) + { + upgrade_ = value; + } + + int + status() const + { + return status_; + } + + void + status (int code) + { + status_ = code; + } + + std::string const& + reason() const + { + return reason_; + } + + void + reason (std::string const& text) + { + reason_ = text; + } + + // Message + + void + version (int major, int minor) + { + version_ = std::make_pair (major, minor); + } + + std::pair + version() const + { + return version_; + } + + // Memberspace + headers_t headers; +}; + +//------------------------------------------------------------------------------ + +class basic_message::parser : public beast::http::parser +{ +private: + basic_message& message_; + +public: + parser (basic_message& message, bool request) + : beast::http::parser (request) + , message_ (message) + { + message_.request(request); + } + +private: + void + on_start () override + { + } + + bool + on_request (method_t method, std::string const& url, + int major, int minor, bool keep_alive, bool upgrade) override + { + message_.method (method); + message_.url (url); + message_.version (major, minor); + message_.keep_alive(keep_alive); + message_.upgrade(upgrade); + return upgrade ? false : false; + } + + bool + on_response (int status, std::string const& text, + int major, int minor, bool keep_alive, bool upgrade) override + { + message_.status (status); + message_.reason (text); + message_.version (major, minor); + message_.keep_alive(keep_alive); + message_.upgrade(upgrade); + return upgrade ? false : false; + } + + void + on_field (std::string const& field, std::string const& value) override + { + message_.headers.append (field, value); + } + + void + on_body (void const* data, std::size_t bytes) override + { + } + + void + on_complete() override + { + } +}; + +//------------------------------------------------------------------------------ + +template +void +xwrite (AsioStreamBuf& stream, std::string const& s) +{ + stream.commit (boost::asio::buffer_copy ( + stream.prepare (s.size()), boost::asio::buffer(s))); +} + +template +void +xwrite (AsioStreamBuf& stream, char const* s) +{ + auto const len (::strlen(s)); + stream.commit (boost::asio::buffer_copy ( + stream.prepare (len), boost::asio::buffer (s, len))); +} + +template +void +xwrite (AsioStreamBuf& stream, basic_message const& m) +{ + if (m.request()) + { + xwrite (stream, to_string(m.method())); + xwrite (stream, " "); + xwrite (stream, m.url()); + xwrite (stream, " HTTP/"); + xwrite (stream, std::to_string(m.version().first)); + xwrite (stream, "."); + xwrite (stream, std::to_string(m.version().second)); + } + else + { + xwrite (stream, "HTTP/"); + xwrite (stream, std::to_string(m.version().first)); + xwrite (stream, "."); + xwrite (stream, std::to_string(m.version().second)); + xwrite (stream, " "); + xwrite (stream, std::to_string(m.status())); + xwrite (stream, " "); + xwrite (stream, m.reason()); + } + xwrite (stream, "\r\n"); + for (auto const& header : m.headers) + { + xwrite (stream, header.field); + xwrite (stream, ": "); + xwrite (stream, header.value); + xwrite (stream, "\r\n"); + } + xwrite (stream, "\r\n"); +} + +template +std::string +to_string (basic_message const& m) +{ + std::stringstream ss; + if (m.request()) + ss << to_string(m.method()) << " " << m.url() << " HTTP/" << + std::to_string(m.version().first) << "." << + std::to_string(m.version().second) << "\r\n"; + else + ss << "HTTP/" << std::to_string(m.version().first) << "." << + std::to_string(m.version().second) << " " << + std::to_string(m.status()) << " " << m.reason() << "\r\n"; + for (auto const& header : m.headers) + ss << header.field << ": " << header.value << "\r\n"; + ss << "\r\n"; + return ss.str(); +} + +} // http +} // beast + +#endif \ No newline at end of file diff --git a/beast/http/impl/message_parser.cpp b/beast/http/impl/message_parser.cpp deleted file mode 100644 index 61db041d48..0000000000 --- a/beast/http/impl/message_parser.cpp +++ /dev/null @@ -1,224 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of Beast: https://github.com/vinniefalco/Beast - Copyright 2013, Vinnie Falco - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -namespace beast { -namespace http { - -message_parser::message_parser (bool request) - : complete_ (false) - , checked_url_ (false) -{ - static_assert (sizeof(joyent::http_parser) == sizeof(state_t), - "state_t size must match http_parser size"); - - static_assert (sizeof(joyent::http_parser_settings) == sizeof(hooks_t), - "hooks_t size must match http_parser_settings size"); - - auto s (reinterpret_cast (&state_)); - s->data = this; - - auto h (reinterpret_cast (&hooks_)); - h->on_message_begin = &message_parser::cb_message_start; - h->on_url = &message_parser::cb_url; - h->on_status = &message_parser::cb_status; - h->on_header_field = &message_parser::cb_header_field; - h->on_header_value = &message_parser::cb_header_value; - h->on_headers_complete = &message_parser::cb_headers_done; - h->on_body = &message_parser::cb_body; - h->on_message_complete = &message_parser::cb_message_complete; - - joyent::http_parser_init (s, request - ? joyent::http_parser_type::HTTP_REQUEST - : joyent::http_parser_type::HTTP_RESPONSE); -} - -std::pair -message_parser::write_one (void const* in, std::size_t bytes) -{ - std::pair result (error_code(), 0); - auto s (reinterpret_cast (&state_)); - auto h (reinterpret_cast (&hooks_)); - result.second = joyent::http_parser_execute (s, h, - static_cast (in), bytes); - result.first = ec_; - return result; -} - -//------------------------------------------------------------------------------ - -int -message_parser::check_url() -{ - if (! checked_url_) - { - checked_url_ = true; - auto const p (reinterpret_cast (&state_)); - ec_ = on_request ( - joyent::convert_http_method (joyent::http_method(p->method)), - p->http_major, p->http_minor, url_); - if (ec_) - return 1; - } - return 0; -} - -int -message_parser::do_message_start () -{ - return ec_ ? 1 : 0; -} - -int -message_parser::do_url (char const* in, std::size_t bytes) -{ - url_.append (static_cast (in), bytes); - return 0; -} - -int -message_parser::do_status (char const* in, std::size_t bytes) -{ - return ec_ ? 1 : 0; -} - -int -message_parser::do_header_field (char const* in, std::size_t bytes) -{ - if (check_url()) - return 1; - if (! value_.empty()) - { - ec_ = on_field (field_, value_); - if (ec_) - return 1; - field_.clear(); - value_.clear(); - } - field_.append (static_cast (in), bytes); - return 0; -} - -int -message_parser::do_header_value (char const* in, std::size_t bytes) -{ - value_.append (static_cast (in), bytes); - return 0; -} - -// Returning 1 from here tells the joyent parser -// that the message has no body (e.g. a HEAD request). -// -int -message_parser::do_headers_done () -{ - if (check_url()) - return 1; - if (! value_.empty()) - { - ec_ = on_field (field_, value_); - if (ec_) - return 1; - field_.clear(); - value_.clear(); - } - return ec_ ? 1 : 0; -} - -int -message_parser::do_body (char const* in, std::size_t bytes) -{ - return ec_ ? 1 : 0; -} - -int -message_parser::do_message_complete () -{ - complete_ = true; - return 0; -} - -//------------------------------------------------------------------------------ - -int -message_parser::cb_message_start (joyent::http_parser* p) -{ - return reinterpret_cast ( - p->data)->do_message_start(); -} - -int -message_parser::cb_url (joyent::http_parser* p, - char const* in, std::size_t bytes) -{ - return reinterpret_cast ( - p->data)->do_url (in, bytes); -} - -int -message_parser::cb_status (joyent::http_parser* p, - char const* in, std::size_t bytes) -{ - return reinterpret_cast ( - p->data)->do_status (in, bytes); -} - -int -message_parser::cb_header_field (joyent::http_parser* p, - char const* in, std::size_t bytes) -{ - return reinterpret_cast ( - p->data)->do_header_field (in, bytes); -} - -int -message_parser::cb_header_value (joyent::http_parser* p, - char const* in, std::size_t bytes) -{ - return reinterpret_cast ( - p->data)->do_header_value (in, bytes); -} - -int -message_parser::cb_headers_done (joyent::http_parser* p) -{ - return reinterpret_cast ( - p->data)->do_headers_done(); -} - -int -message_parser::cb_body (joyent::http_parser* p, - char const* in, std::size_t bytes) -{ - return reinterpret_cast ( - p->data)->do_body ( - in, bytes); -} - -int -message_parser::cb_message_complete (joyent::http_parser* p) -{ - return reinterpret_cast ( - p->data)->do_message_complete(); -} - -} // http -} // beast diff --git a/beast/http/impl/method.cpp b/beast/http/impl/method.cpp new file mode 100644 index 0000000000..9cc119c19b --- /dev/null +++ b/beast/http/impl/method.cpp @@ -0,0 +1,124 @@ +//------------------------------------------------------------------------------ +/* + This file is part of Beast: https://github.com/vinniefalco/Beast + Copyright 2013: Vinnie Falco + + Permission to use: copy: modify: and/or distribute this software for any + purpose with or without fee is hereby granted: provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL : DIRECT: INDIRECT: OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE: DATA OR PROFITS: WHETHER IN AN + ACTION OF CONTRACT: NEGLIGENCE OR OTHER TORTIOUS ACTION: ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace beast { +namespace http { + +std::string +to_string (method_t m) +{ + switch(m) + { + case method_t::http_delete: return "DELETE"; + case method_t::http_get: return "GET"; + case method_t::http_head: return "HEAD"; + case method_t::http_post: return "POST"; + case method_t::http_put: return "PUT"; + + case method_t::http_connect: return "CONNECT"; + case method_t::http_options: return "OPTIONS"; + case method_t::http_trace: return "TRACE"; + + case method_t::http_copy: return "COPY"; + case method_t::http_lock: return "LOCK"; + case method_t::http_mkcol: return "MKCOL"; + case method_t::http_move: return "MOVE"; + case method_t::http_propfind: return "PROPFIND"; + case method_t::http_proppatch: return "PROPPATCH"; + case method_t::http_search: return "SEARCH"; + case method_t::http_unlock: return "UNLOCK"; + + case method_t::http_report: return "REPORT"; + case method_t::http_mkactivity: return "MKACTIVITY"; + case method_t::http_checkout: return "CHECKOUT"; + case method_t::http_merge: return "MERGE"; + + case method_t::http_msearch: return "MSEARCH"; + case method_t::http_notify: return "NOTIFY"; + case method_t::http_subscribe: return "SUBSCRIBE"; + case method_t::http_unsubscribe: return "UNSUBSCRIBE"; + + case method_t::http_patch: return "PATCH"; + case method_t::http_purge: return "PURGE"; + + default: + assert(false); + break; + }; + + return "GET"; +} + +std::string +status_text (int status) +{ + switch(status) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + //case 306: return ""; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-URI Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + default: + break; + } + return "Unknown HTTP status"; +} + +} +} diff --git a/beast/http/impl/parser.cpp b/beast/http/impl/parser.cpp new file mode 100644 index 0000000000..d34709c1b8 --- /dev/null +++ b/beast/http/impl/parser.cpp @@ -0,0 +1,232 @@ +//------------------------------------------------------------------------------ +/* + This file is part of Beast: https://github.com/vinniefalco/Beast + Copyright 2013, Vinnie Falco + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace beast { +namespace http { + +parser::parser (bool request) +{ + static_assert (sizeof(joyent::http_parser) == sizeof(state_t), + "state_t size must match http_parser size"); + + static_assert (sizeof(joyent::http_parser_settings) == sizeof(hooks_t), + "hooks_t size must match http_parser_settings size"); + + auto s (reinterpret_cast (&state_)); + s->data = this; + + auto h (reinterpret_cast (&hooks_)); + h->on_message_begin = &parser::cb_message_start; + h->on_url = &parser::cb_url; + h->on_status = &parser::cb_status; + h->on_header_field = &parser::cb_header_field; + h->on_header_value = &parser::cb_header_value; + h->on_headers_complete = &parser::cb_headers_complete; + h->on_body = &parser::cb_body; + h->on_message_complete = &parser::cb_message_complete; + + joyent::http_parser_init (s, request + ? joyent::http_parser_type::HTTP_REQUEST + : joyent::http_parser_type::HTTP_RESPONSE); +} + +std::pair +parser::write (void const* data, std::size_t bytes) +{ + std::pair result (error_code(), 0); + auto s (reinterpret_cast (&state_)); + auto h (reinterpret_cast (&hooks_)); + result.second = joyent::http_parser_execute (s, h, + static_cast (data), bytes); + result.first = ec_; + return result; +} + +parser::error_code +parser::eof() +{ + auto s (reinterpret_cast (&state_)); + auto h (reinterpret_cast (&hooks_)); + joyent::http_parser_execute (s, h, nullptr, 0); + return ec_; +} + +//------------------------------------------------------------------------------ + +void +parser::check_header() +{ + if (! value_.empty()) + { + rfc2616::trim_right_in_place (value_); + on_field (field_, value_); + field_.clear(); + value_.clear(); + } +} + +int +parser::do_message_start () +{ + complete_ = false; + url_.clear(); + status_.clear(); + field_.clear(); + value_.clear(); + on_start(); + return 0; +} + +int +parser::do_url (char const* in, std::size_t bytes) +{ + url_.append (static_cast (in), bytes); + return 0; +} + +int +parser::do_status (char const* in, std::size_t bytes) +{ + status_.append (static_cast (in), bytes); + return 0; +} + +int +parser::do_header_field (char const* in, std::size_t bytes) +{ + check_header(); + field_.append (static_cast (in), bytes); + return 0; +} + +int +parser::do_header_value (char const* in, std::size_t bytes) +{ + value_.append (static_cast (in), bytes); + return 0; +} + +/* Called when all the headers are complete but before + the content body, if present. + Returning 1 from here tells the joyent parser + that the message has no body (e.g. a HEAD request). +*/ +int +parser::do_headers_complete() +{ + check_header(); + auto const p (reinterpret_cast (&state_)); + bool const keep_alive (joyent::http_should_keep_alive (p) != 0); + if (p->type == joyent::http_parser_type::HTTP_REQUEST) + return on_request (joyent::convert_http_method ( + joyent::http_method(p->method)), url_, + p->http_major, p->http_minor, keep_alive, p->upgrade) ? 0 : 1; + return on_response (p->status_code, status_, + p->http_major, p->http_minor, keep_alive, p->upgrade) ? 0 : 1; +} + +/* Called repeatedly for the content body. The passed buffer + has already had the transfer-encoding removed. +*/ +int +parser::do_body (char const* in, std::size_t bytes) +{ + on_body (in, bytes); + return 0; +} + +/* Called when the both the headers and content body (if any) are complete. */ +int +parser::do_message_complete () +{ + complete_ = true; + on_complete(); + return 0; +} + +//------------------------------------------------------------------------------ + +int +parser::cb_message_start (joyent::http_parser* p) +{ + return reinterpret_cast ( + p->data)->do_message_start(); +} + +int +parser::cb_url (joyent::http_parser* p, + char const* in, std::size_t bytes) +{ + return reinterpret_cast ( + p->data)->do_url (in, bytes); +} + +int +parser::cb_status (joyent::http_parser* p, + char const* in, std::size_t bytes) +{ + return reinterpret_cast ( + p->data)->do_status (in, bytes); +} + +int +parser::cb_header_field (joyent::http_parser* p, + char const* in, std::size_t bytes) +{ + return reinterpret_cast ( + p->data)->do_header_field (in, bytes); +} + +int +parser::cb_header_value (joyent::http_parser* p, + char const* in, std::size_t bytes) +{ + return reinterpret_cast ( + p->data)->do_header_value (in, bytes); +} + +int +parser::cb_headers_complete (joyent::http_parser* p) +{ + return reinterpret_cast ( + p->data)->do_headers_complete(); +} + +int +parser::cb_body (joyent::http_parser* p, + char const* in, std::size_t bytes) +{ + return reinterpret_cast ( + p->data)->do_body ( + in, bytes); +} + +int +parser::cb_message_complete (joyent::http_parser* p) +{ + return reinterpret_cast ( + p->data)->do_message_complete(); +} + +} // http +} // beast diff --git a/beast/http/method.h b/beast/http/method.h index 1a95c5d447..e7f7d63c11 100644 --- a/beast/http/method.h +++ b/beast/http/method.h @@ -21,6 +21,7 @@ #define BEAST_HTTP_METHOD_H_INCLUDED #include +#include namespace beast { namespace http { @@ -65,6 +66,20 @@ enum class method_t http_purge }; +std::string +to_string (method_t m); + +template +Stream& +operator<< (Stream& s, method_t m) +{ + return s << to_string(m); +} + +/** Returns the string corresponding to the numeric HTTP status code. */ +std::string +status_text (int status); + } } diff --git a/beast/http/message_parser.h b/beast/http/parser.h similarity index 53% rename from beast/http/message_parser.h rename to beast/http/parser.h index 7ff7d6e241..dba185e24a 100644 --- a/beast/http/message_parser.h +++ b/beast/http/parser.h @@ -17,10 +17,11 @@ */ //============================================================================== -#ifndef BEAST_HTTP_MESSAGE_PARSER_H_INCLUDED -#define BEAST_HTTP_MESSAGE_PARSER_H_INCLUDED +#ifndef BEAST_HTTP_PARSER_H_INCLUDED +#define BEAST_HTTP_PARSER_H_INCLUDED #include +#include #include #include #include @@ -35,7 +36,7 @@ struct http_parser; namespace http { -class message_parser +class parser { public: typedef boost::system::error_code error_code; @@ -82,9 +83,9 @@ private: char state_ [sizeof(state_t)]; char hooks_ [sizeof(hooks_t)]; - bool complete_; + bool complete_ = false; std::string url_; - bool checked_url_; + std::string status_; std::string field_; std::string value_; @@ -94,7 +95,7 @@ protected: process an HTTP request. */ explicit - message_parser (bool request); + parser (bool request); public: /** Returns `true` if parsing is complete. @@ -109,18 +110,18 @@ public: /** Write data to the parser. The return value includes the error code if any, and the number of bytes consumed in the input sequence. + @param data A buffer containing the data to write + @param bytes The size of the buffer pointed to by data. */ std::pair - write_one (void const* in, std::size_t bytes); - - template - std::pair - write_one (ConstBuffer const& buffer) - { - return write_one (boost::asio::buffer_cast (buffer), - boost::asio::buffer_size (buffer)); - } + write (void const* data, std::size_t bytes); + /** Write a set of buffer data to the parser. + The return value includes the error code if any, + and the number of bytes consumed in the input sequence. + @param buffers The buffers to write. These must meet the + requirements of ConstBufferSequence. + */ template std::pair write (ConstBufferSequence const& buffers) @@ -129,7 +130,9 @@ public: for (auto const& buffer : buffers) { std::size_t bytes_consumed; - std::tie (result.first, bytes_consumed) = write_one (buffer); + std::tie (result.first, bytes_consumed) = + write (boost::asio::buffer_cast (buffer), + boost::asio::buffer_size (buffer)); if (result.first) break; result.second += bytes_consumed; @@ -137,25 +140,96 @@ public: return result; } -protected: - virtual + /** Called to indicate the end of file. + HTTP needs to know where the end of the stream is. For example, + sometimes servers send responses without Content-Length and + expect the client to consume input (for the body) until EOF. + Callbacks and errors will still be processed as usual. + @note Typically this should be called when the + socket indicates a closure. + */ error_code - on_request (method_t method, int http_major, - int http_minor, std::string const& url) = 0; + eof(); +protected: + /** Called once when a new message begins. */ virtual - error_code + void + on_start() = 0; + + /** Called for each header field. */ + virtual + void on_field (std::string const& field, std::string const& value) = 0; + /** Called for requests when all the headers have been received. + This will precede any content body. + When keep_alive is false: + * Server roles respond with a "Connection: close" header. + * Client roles close the connection. + When upgrade is true, no content-body is expected, and the + return value is ignored. + + @param method The HTTP method specified in the request line + @param major The HTTP major version number + @param minor The HTTP minor version number + @param url The URL specified in the request line + @param keep_alive `false` if this is the last message. + @param upgrade `true` if the Upgrade header is specified + @return `true` If upgrade is false and a content body is expected. + */ + virtual + bool + on_request (method_t method, std::string const& url, + int major, int minor, bool keep_alive, bool upgrade) = 0; + + /** Called for responses when all the headers have been received. + This will precede any content body. + When keep_alive is `false`: + * Client roles close the connection. + * Server roles respond with a "Connection: close" header. + When upgrade is true, no content-body is expected, and the + return value is ignored. + + @param status The numerical HTTP status code in the response line + @param text The status text in the response line + @param major The HTTP major version number + @param minor The HTTP minor version number + @param keep_alive `false` if this is the last message. + @param upgrade `true` if the Upgrade header is specified + @return `true` If upgrade is false and a content body is expected. + */ + virtual + bool + on_response (int status, std::string const& text, + int major, int minor, bool keep_alive, bool upgrade) = 0; + + /** Called zero or more times for the content body. + Any transfer encoding is already decoded in the + memory pointed to by data. + + @param data A memory block containing the next decoded + chunk of the content body. + @param bytes The number of bytes pointed to by data. + */ + virtual + void + on_body (void const* data, std::size_t bytes) = 0; + + /** Called once when the message is complete. */ + virtual + void + on_complete() = 0; + private: - int check_url(); + void check_header(); int do_message_start (); int do_url (char const* in, std::size_t bytes); int do_status (char const* in, std::size_t bytes); int do_header_field (char const* in, std::size_t bytes); int do_header_value (char const* in, std::size_t bytes); - int do_headers_done (); + int do_headers_complete (); int do_body (char const* in, std::size_t bytes); int do_message_complete (); @@ -164,7 +238,7 @@ private: static int cb_status (joyent::http_parser*, char const*, std::size_t); static int cb_header_field (joyent::http_parser*, char const*, std::size_t); static int cb_header_value (joyent::http_parser*, char const*, std::size_t); - static int cb_headers_done (joyent::http_parser*); + static int cb_headers_complete (joyent::http_parser*); static int cb_body (joyent::http_parser*, char const*, std::size_t); static int cb_message_complete (joyent::http_parser*); }; diff --git a/beast/http/rfc2616.h b/beast/http/rfc2616.h new file mode 100644 index 0000000000..caa57acabb --- /dev/null +++ b/beast/http/rfc2616.h @@ -0,0 +1,239 @@ +//------------------------------------------------------------------------------ +/* + This file is part of Beast: https://github.com/vinniefalco/Beast + Copyright 2013, Vinnie Falco + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef BEAST_HTTP_RFC2616_H_INCLUDED +#define BEAST_HTTP_RFC2616_H_INCLUDED + +#include +#include +#include + +namespace beast { +namespace http { + +/** Routines for performing RFC2616 compliance. + RFC2616: + Hypertext Transfer Protocol -- HTTP/1.1 + http://www.w3.org/Protocols/rfc2616/rfc2616 +*/ +namespace rfc2616 { + +/** Returns `true` if `c` is linear white space. + This excludes the CRLF sequence allowed for line continuations. +*/ +template +bool +is_lws (CharT c) +{ + return c == ' ' || c == '\t'; +} + +/** Returns `true` if `c` is any whitespace character. */ +template +bool +is_white (CharT c) +{ + switch (c) + { + case ' ': case '\f': case '\n': + case '\r': case '\t': case '\v': + return true; + }; + return false; +} + +/** Returns `true` if `c` is a control character. */ +template +bool +is_ctl (CharT c) +{ + return c <= 31 || c >= 127; +} + +/** Returns `true` if `c` is a separator. */ +template +bool +is_sep (CharT c) +{ + switch (c) + { + case '(': case ')': case '<': case '>': case '@': + case ',': case ';': case ':': case '\\': case '"': + case '{': case '}': case ' ': case '\t': + return true; + }; + return false; +} + +template +FwdIter +trim_left (FwdIter first, FwdIter last) +{ + return std::find_if_not (first, last, + &is_white ); +} + +template +FwdIter +trim_right (FwdIter first, FwdIter last) +{ + if (first == last) + return last; + do + { + --last; + if (! is_white (*last)) + return ++last; + } + while (last != first); + return first; +} + +template +void +trim_right_in_place (std::basic_string < + CharT, Traits, Allocator>& s) +{ + s.resize (std::distance (s.begin(), + trim_right (s.begin(), s.end()))); +} + +template +std::pair +trim (FwdIter first, FwdIter last) +{ + first = trim_left (first, last); + last = trim_right (first, last); + return std::make_pair (first, last); +} + +template +String +trim (String const& s) +{ + using std::begin; + using std::end; + auto first (begin(s)); + auto last (end(s)); + std::tie (first, last) = trim (first, last); + return { first, last }; +} + +template +String +trim_right (String const& s) +{ + using std::begin; + using std::end; + auto first (begin(s)); + auto last (end(s)); + last = trim_right (first, last); + return { first, last }; +} + +inline +std::string +trim (std::string const& s) +{ + return trim (s); +} + +/** Call a functor for each comma delimited element. + Quotes and escape sequences will be parsed and converted appropriately. + Excess white space, commas, double quotes, and empty elements are not + passed to func. + Format: + #(token|quoted-string) + Reference: + http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2 +*/ +template +void +for_each_element (FwdIter first, FwdIter last, Function func) +{ + FwdIter iter (first); + std::string e; + while (iter != last) + { + if (*iter == '"') + { + // quoted-string + ++iter; + while (iter != last) + { + if (*iter == '"') + { + ++iter; + break; + } + + if (*iter == '\\') + { + // quoted-pair + ++iter; + if (iter != last) + e.append (1, *iter++); + } + else + { + // qdtext + e.append (1, *iter++); + } + } + if (! e.empty()) + { + func (e); + e.clear(); + } + } + else if (*iter == ',') + { + e = trim_right (e); + if (! e.empty()) + { + func (e); + e.clear(); + } + ++iter; + } + else if (is_lws (*iter)) + { + ++iter; + } + else + { + e.append (1, *iter++); + } + } + + if (! e.empty()) + { + e = trim_right (e); + if (! e.empty()) + func (e); + } +} + +} // rfc2616 + +} // http +} // beast + +#endif + diff --git a/beast/http/tests/basic_message.test.cpp b/beast/http/tests/basic_message.test.cpp index ebc0361286..07bdf5d16e 100644 --- a/beast/http/tests/basic_message.test.cpp +++ b/beast/http/tests/basic_message.test.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* - This file is part of Beast: https://github.com/vinniefalco/Beast - Copyright 2013, Vinnie Falco + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -17,29 +17,48 @@ */ //============================================================================== -#if BEAST_INCLUDE_BEASTCONFIG -#include "../../BeastConfig.h" -#endif - +#include #include namespace beast { namespace http { -class basic_message_tests : public unit_test::suite +class basic_message_test : public beast::unit_test::suite { public: - void testCreate() + std::pair + request (std::string const& text) { - pass(); + basic_message m; + basic_message::parser p (m, true); + auto result (p.write (boost::asio::buffer(text))); + auto result2 (p.eof()); + return std::make_pair (std::move(m), result.first); } - void run() + void + run() { - testCreate(); + auto const result = request ( + "GET / HTTP/1.1\r\n" + //"Connection: Upgrade\r\n" + //"Upgrade: Ripple\r\n" + "Field: \t Value \t \r\n" + "Blib: Continu\r\n" + " ation\r\n" + "Field: Hey\r\n" + "Content-Length: 1\r\n" + "\r\n" + "x" + ); + + log << "|" << result.first.headers["Field"] << "|"; + + pass(); } }; BEAST_DEFINE_TESTSUITE(basic_message,http,beast); -} -} + +} // http +} // beast diff --git a/beast/http/tests/rfc2616.test.cpp b/beast/http/tests/rfc2616.test.cpp new file mode 100644 index 0000000000..83de441d5a --- /dev/null +++ b/beast/http/tests/rfc2616.test.cpp @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +/* + This file is part of Beast: https://github.com/vinniefalco/Beast + Copyright 2013, Vinnie Falco + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#if BEAST_INCLUDE_BEASTCONFIG +#include "../../BeastConfig.h" +#endif + +#include +#include +#include +#include + +namespace beast { +namespace http { +namespace rfc2616 { + +class rfc2616_test : public beast::unit_test::suite +{ +public: + void + check (std::string const& value, + std::vector const& expected) + { + std::vector parsed; + for_each_element (value.begin(), value.end(), + [&](std::string const& element) + { + parsed.push_back (element); + }); + expect (parsed == expected); + } + + void + run() + { + check ("", {}); + check (" ", {}); + check (" ", {}); + check ("\t", {}); + check (" \t ", {}); + + check (",", {}); + check (",,", {}); + check (" ,", {}); + check (" , ,", {}); + + check ("x", {"x"}); + check (" x", {"x"}); + check (" \t x", {"x"}); + check ("x ", {"x"}); + check ("x \t", {"x"}); + check (" \t x \t ", {"x"}); + + check ("\"\"", {}); + check (" \"\"", {}); + check ("\"\" ", {}); + + check ("\"x\"", {"x"}); + check ("\" \"", {" "}); + check ("\" x\"", {" x"}); + check ("\"x \"", {"x "}); + check ("\" x \"", {" x "}); + check ("\"\tx \"", {"\tx "}); + + check ("x,y", { "x", "y" }); + check ("x ,\ty ", { "x", "y" }); + + check ("x, y, z", {"x","y","z"}); + check ("x, \"y\", z", {"x","y","z"}); + } +}; + +BEAST_DEFINE_TESTSUITE(rfc2616,http,beast); + +} +} +} diff --git a/beast/utility/ci_char_traits.h b/beast/utility/ci_char_traits.h index d319fe059d..3b34932245 100644 --- a/beast/utility/ci_char_traits.h +++ b/beast/utility/ci_char_traits.h @@ -20,11 +20,54 @@ #ifndef BEAST_UTILITY_CI_CHAR_TRAITS_H_INCLUDED #define BEAST_UTILITY_CI_CHAR_TRAITS_H_INCLUDED +#include // #include #include namespace beast { +/** Case-insensitive function object for performing less than comparisons. */ +struct ci_less +{ + static bool const is_transparent = true; + + template + bool + operator() (String const& lhs, String const& rhs) const + { + typedef typename String::value_type char_type; + return std::lexicographical_compare (std::begin(lhs), std::end(lhs), + std::begin(rhs), std::end(rhs), + [] (char_type lhs, char_type rhs) + { + return std::tolower(lhs) < std::tolower(rhs); + } + ); + } +}; + +/** Case-insensitive function object for performing equal to comparisons. */ +struct ci_equal_to +{ + static bool const is_transparent = true; + + template + bool + operator() (String const& lhs, String const& rhs) const + { + typedef typename String::value_type char_type; + return std::equal (lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), + [] (char_type lhs, char_type rhs) + { + return std::tolower(lhs) == std::tolower(rhs); + } + ); + } +}; + +// DEPRECATED VFALCO This causes far more problems than it solves! +// +/** Case insensitive character traits. */ struct ci_char_traits : std::char_traits { static