implement HTTP request body parsing
This commit is contained in:
@@ -480,6 +480,62 @@ BOOST_AUTO_TEST_CASE( basic_request ) {
|
||||
BOOST_CHECK( r.get_header("Host") == "www.example.com" );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( basic_request_with_body ) {
|
||||
websocketpp::http::parser::request r;
|
||||
|
||||
std::string raw = "GET / HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 5\r\n\r\nabcdef";
|
||||
|
||||
bool exception = false;
|
||||
size_t pos = 0;
|
||||
|
||||
try {
|
||||
pos = r.consume(raw.c_str(),raw.size());
|
||||
} catch (std::exception &e) {
|
||||
exception = true;
|
||||
std::cout << e.what() << std::endl;
|
||||
}
|
||||
|
||||
BOOST_CHECK( exception == false );
|
||||
BOOST_CHECK_EQUAL( pos, 65 );
|
||||
BOOST_CHECK( r.ready() == true );
|
||||
BOOST_CHECK_EQUAL( r.get_version(), "HTTP/1.1" );
|
||||
BOOST_CHECK_EQUAL( r.get_method(), "GET" );
|
||||
BOOST_CHECK_EQUAL( r.get_uri(), "/" );
|
||||
BOOST_CHECK_EQUAL( r.get_header("Host"), "www.example.com" );
|
||||
BOOST_CHECK_EQUAL( r.get_header("Content-Length"), "5" );
|
||||
BOOST_CHECK_EQUAL( r.get_body(), "abcde" );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( basic_request_with_body_split ) {
|
||||
websocketpp::http::parser::request r;
|
||||
|
||||
std::string raw = "GET / HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 6\r\n\r\nabc";
|
||||
std::string raw2 = "def";
|
||||
|
||||
bool exception = false;
|
||||
size_t pos = 0;
|
||||
|
||||
try {
|
||||
pos += r.consume(raw.c_str(),raw.size());
|
||||
pos += r.consume(raw2.c_str(),raw2.size());
|
||||
} catch (std::exception &e) {
|
||||
exception = true;
|
||||
std::cout << e.what() << std::endl;
|
||||
}
|
||||
|
||||
BOOST_CHECK( exception == false );
|
||||
BOOST_CHECK_EQUAL( pos, 66 );
|
||||
BOOST_CHECK( r.ready() == true );
|
||||
BOOST_CHECK_EQUAL( r.get_version(), "HTTP/1.1" );
|
||||
BOOST_CHECK_EQUAL( r.get_method(), "GET" );
|
||||
BOOST_CHECK_EQUAL( r.get_uri(), "/" );
|
||||
BOOST_CHECK_EQUAL( r.get_header("Host"), "www.example.com" );
|
||||
BOOST_CHECK_EQUAL( r.get_header("Content-Length"), "6" );
|
||||
BOOST_CHECK_EQUAL( r.get_body(), "abcdef" );
|
||||
}
|
||||
|
||||
|
||||
|
||||
BOOST_AUTO_TEST_CASE( trailing_body_characters ) {
|
||||
websocketpp::http::parser::request r;
|
||||
|
||||
@@ -595,6 +651,27 @@ BOOST_AUTO_TEST_CASE( max_header_len_split ) {
|
||||
BOOST_CHECK( exception == true );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( max_body_len ) {
|
||||
websocketpp::http::parser::request r;
|
||||
|
||||
r.set_max_body_size(5);
|
||||
|
||||
std::string raw = "GET / HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 6\r\n\r\nabcdef";
|
||||
|
||||
bool exception = false;
|
||||
size_t pos = 0;
|
||||
|
||||
try {
|
||||
pos += r.consume(raw.c_str(),raw.size());
|
||||
} catch (websocketpp::http::exception const & e) {
|
||||
exception = true;
|
||||
BOOST_CHECK_EQUAL(e.m_error_code,websocketpp::http::status_code::request_entity_too_large);
|
||||
}
|
||||
|
||||
BOOST_CHECK_EQUAL(r.get_max_body_size(),5);
|
||||
BOOST_CHECK( exception == true );
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE( firefox_full_request ) {
|
||||
websocketpp::http::parser::request r;
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ namespace http {
|
||||
|
||||
/// Maximum size in bytes before rejecting an HTTP header as too big.
|
||||
size_t const max_header_size = 16000;
|
||||
|
||||
/// Default Maximum size in bytes for HTTP message bodies.
|
||||
size_t const max_body_size = 32000000;
|
||||
|
||||
/// Number of bytes to use for temporary istream read buffers
|
||||
size_t const istream_buffer = 512;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#define HTTP_PARSER_IMPL_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <istream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -94,6 +95,9 @@ inline void parser::set_body(std::string const & value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: should this method respect the max size? If so how should errors
|
||||
// be indicated?
|
||||
|
||||
std::stringstream len;
|
||||
len << value.size();
|
||||
replace_header("Content-Length", len.str());
|
||||
@@ -112,6 +116,48 @@ inline bool parser::parse_parameter_list(std::string const & in,
|
||||
return (it == in.begin());
|
||||
}
|
||||
|
||||
inline bool parser::prepare_body() {
|
||||
if (get_header("Content-Length") != "") {
|
||||
std::string const & cl_header = get_header("Content-Length");
|
||||
char * end;
|
||||
|
||||
// TODO: not 100% sure what the compatibility of this method is. Also,
|
||||
// I believe this will only work up to 32bit sizes. Is there a need for
|
||||
// > 4GiB HTTP payloads?
|
||||
m_body_bytes_needed = std::strtoul(cl_header.c_str(),&end,10);
|
||||
|
||||
if (m_body_bytes_needed > m_body_bytes_max) {
|
||||
throw exception("HTTP message body too large",
|
||||
status_code::request_entity_too_large);
|
||||
}
|
||||
|
||||
m_body_encoding = body_encoding::plain;
|
||||
return true;
|
||||
} else if (get_header("Transfer-Encoding") == "chunked") {
|
||||
// TODO
|
||||
//m_body_encoding = body_encoding::chunked;
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline size_t parser::process_body(char const * buf, size_t len) {
|
||||
if (m_body_encoding == body_encoding::plain) {
|
||||
size_t processed = (std::min)(m_body_bytes_needed,len);
|
||||
m_body.append(buf,processed);
|
||||
m_body_bytes_needed -= processed;
|
||||
return processed;
|
||||
} else if (m_body_encoding == body_encoding::chunked) {
|
||||
// TODO:
|
||||
throw exception("Unexpected body encoding",
|
||||
status_code::internal_server_error);
|
||||
} else {
|
||||
throw exception("Unexpected body encoding",
|
||||
status_code::internal_server_error);
|
||||
}
|
||||
}
|
||||
|
||||
inline void parser::process_header(std::string::iterator begin,
|
||||
std::string::iterator end)
|
||||
{
|
||||
|
||||
@@ -39,7 +39,17 @@ namespace http {
|
||||
namespace parser {
|
||||
|
||||
inline size_t request::consume(char const * buf, size_t len) {
|
||||
size_t bytes_processed;
|
||||
|
||||
if (m_ready) {return 0;}
|
||||
|
||||
if (m_body_bytes_needed > 0) {
|
||||
bytes_processed = process_body(buf,len);
|
||||
if (body_ready()) {
|
||||
m_ready = true;
|
||||
}
|
||||
return bytes_processed;
|
||||
}
|
||||
|
||||
if (m_buf->size() + len > max_header_size) {
|
||||
// exceeded max header size
|
||||
@@ -81,9 +91,8 @@ inline size_t request::consume(char const * buf, size_t len) {
|
||||
if (m_method.empty() || get_header("Host") == "") {
|
||||
throw exception("Incomplete Request",status_code::bad_request);
|
||||
}
|
||||
m_ready = true;
|
||||
|
||||
size_t bytes_processed = (
|
||||
bytes_processed = (
|
||||
len - static_cast<std::string::size_type>(m_buf->end()-end)
|
||||
+ sizeof(header_delimiter) - 1
|
||||
);
|
||||
@@ -91,8 +100,22 @@ inline size_t request::consume(char const * buf, size_t len) {
|
||||
// frees memory used temporarily during request parsing
|
||||
m_buf.reset();
|
||||
|
||||
// return number of bytes processed (starting bytes - bytes left)
|
||||
return bytes_processed;
|
||||
// if this was not an upgrade request and has a content length
|
||||
// continue capturing content-length bytes and expose them as a
|
||||
// request body.
|
||||
|
||||
if (prepare_body()) {
|
||||
bytes_processed += process_body(buf+bytes_processed,len-bytes_processed);
|
||||
if (body_ready()) {
|
||||
m_ready = true;
|
||||
}
|
||||
return bytes_processed;
|
||||
} else {
|
||||
m_ready = true;
|
||||
|
||||
// return number of bytes processed (starting bytes - bytes left)
|
||||
return bytes_processed;
|
||||
}
|
||||
} else {
|
||||
if (m_method.empty()) {
|
||||
this->process(begin,end);
|
||||
|
||||
@@ -49,6 +49,14 @@ namespace state {
|
||||
};
|
||||
}
|
||||
|
||||
namespace body_encoding {
|
||||
enum value {
|
||||
unknown,
|
||||
plain,
|
||||
chunked
|
||||
};
|
||||
}
|
||||
|
||||
typedef std::map<std::string, std::string, utility::ci_less > header_list;
|
||||
|
||||
/// Read and return the next token in the stream
|
||||
@@ -385,6 +393,11 @@ inline std::string strip_lws(std::string const & input) {
|
||||
*/
|
||||
class parser {
|
||||
public:
|
||||
parser()
|
||||
: m_body_bytes_needed(0)
|
||||
, m_body_bytes_max(max_body_size)
|
||||
, m_body_encoding(body_encoding::unknown) {}
|
||||
|
||||
/// Get the HTTP version string
|
||||
/**
|
||||
* @return The version string for this parser
|
||||
@@ -468,12 +481,11 @@ public:
|
||||
*/
|
||||
void remove_header(std::string const & key);
|
||||
|
||||
/// Set HTTP body
|
||||
/// Get HTTP body
|
||||
/**
|
||||
* Sets the body of the HTTP object and fills in the appropriate content
|
||||
* length header.
|
||||
* Gets the body of the HTTP object
|
||||
*
|
||||
* @param [in] value The value to set the body to.
|
||||
* @return The body of the HTTP message.
|
||||
*/
|
||||
std::string const & get_body() const {
|
||||
return m_body;
|
||||
@@ -490,6 +502,32 @@ public:
|
||||
*/
|
||||
void set_body(std::string const & value);
|
||||
|
||||
/// Get body size limit
|
||||
/**
|
||||
* Retrieves the maximum number of bytes to parse & buffer before canceling
|
||||
* a request.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return The maximum length of a message body.
|
||||
*/
|
||||
size_t get_max_body_size() const {
|
||||
return m_body_bytes_max;
|
||||
}
|
||||
|
||||
/// Set body size limit
|
||||
/**
|
||||
* Set the maximum number of bytes to parse and buffer before canceling a
|
||||
* request.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param value The size to set the max body length to.
|
||||
*/
|
||||
void set_max_body_size(size_t value) {
|
||||
m_body_bytes_max = value;
|
||||
}
|
||||
|
||||
/// Extract an HTTP parameter list from a string.
|
||||
/**
|
||||
* @param [in] in The input string.
|
||||
@@ -508,6 +546,45 @@ protected:
|
||||
*/
|
||||
void process_header(std::string::iterator begin, std::string::iterator end);
|
||||
|
||||
/// Prepare the parser to begin parsing body data
|
||||
/**
|
||||
* Inspects headers to determine if the message has a body that needs to be
|
||||
* read. If so, sets up the necessary state, otherwise returns false. If
|
||||
* this method returns true and loading the message body is desired call
|
||||
* `process_body` until it returns zero bytes or an error.
|
||||
*
|
||||
* Must not be called until after all headers have been processed.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return True if more bytes are needed to load the body, false otherwise.
|
||||
*/
|
||||
bool prepare_body();
|
||||
|
||||
/// Process body data
|
||||
/**
|
||||
* Parses body data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param [in] begin An iterator to the beginning of the sequence.
|
||||
* @param [in] end An iterator to the end of the sequence.
|
||||
* @return The number of bytes processed
|
||||
*/
|
||||
size_t process_body(char const * buf, size_t len);
|
||||
|
||||
/// Check if the parser is done parsing the body
|
||||
/**
|
||||
* Behavior before a call to `prepare_body` is undefined.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return True if the message body has been completed loaded.
|
||||
*/
|
||||
bool body_ready() const {
|
||||
return (m_body_bytes_needed == 0);
|
||||
}
|
||||
|
||||
/// Generate and return the HTTP headers as a string
|
||||
/**
|
||||
* Each headers will be followed by the \r\n sequence including the last one.
|
||||
@@ -519,7 +596,11 @@ protected:
|
||||
|
||||
std::string m_version;
|
||||
header_list m_headers;
|
||||
std::string m_body;
|
||||
|
||||
std::string m_body;
|
||||
size_t m_body_bytes_needed;
|
||||
size_t m_body_bytes_max;
|
||||
body_encoding::value m_body_encoding;
|
||||
};
|
||||
|
||||
} // namespace parser
|
||||
|
||||
Reference in New Issue
Block a user