0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-11-29 18:22:50 +01:00

ircd: Reflow the request handlers to give the resource more control over content.

This commit is contained in:
Jason Volk 2018-01-11 18:45:25 -08:00
parent 1f7ed51f3b
commit 04fa556c58
3 changed files with 231 additions and 121 deletions

View file

@ -25,8 +25,6 @@
namespace ircd
{
struct resource;
bool handle_request(client &client, parse::capstan &pc);
}
struct ircd::resource
@ -52,7 +50,11 @@ struct ircd::resource
public:
method &operator[](const string_view &path);
void operator()(client &, parse::capstan &, const http::request::head &);
void operator()(client &,
const http::request::head &,
const string_view &content_partial,
size_t &content_consumed);
resource(const string_view &path, const opts &);
resource(const string_view &path);
@ -96,12 +98,15 @@ struct ircd::resource::request
template<class> struct object;
const http::request::head &head;
http::request::content &content;
string_view content;
http::query::string query;
string_view user_id; //m::user::id::buf user_id; //TODO: bleeding
vector_view<string_view> parv;
request(const http::request::head &head, http::request::content &content, http::query::string query, const vector_view<string_view> &parv);
request(const http::request::head &head,
const string_view &content,
http::query::string query,
const vector_view<string_view> &parv);
};
template<class tuple>
@ -110,7 +115,7 @@ struct ircd::resource::request::object
{
resource::request &r;
const http::request::head &head;
const http::request::content &content;
const string_view &content;
const http::query::string &query;
const decltype(r.user_id) &user_id;
const vector_view<string_view> &parv;
@ -146,6 +151,8 @@ struct ircd::resource::response
struct ircd::resource::method
{
using handler = std::function<response (client &, request &)>;
enum flag
{
REQUIRES_AUTH = 0x01,
@ -155,20 +162,23 @@ struct ircd::resource::method
struct opts
{
flag flags{flag(0)};
};
flag flags {(flag)0};
using handler = std::function<response (client &, request &)>;
/// The maximum size of the Content-Length for this method. Anything
/// larger will be summarily rejected with a 413.
size_t payload_max {128_KiB};
};
string_view name;
struct resource *resource;
handler function;
flag flags;
struct opts opts;
unique_const_iterator<decltype(resource::methods)> methods_it;
public:
virtual response operator()(client &, request &);
method(struct resource &, const string_view &name, const handler &, opts = {{}});
method(struct resource &, const string_view &name, const handler &, const struct opts &);
method(struct resource &, const string_view &name, const handler &);
virtual ~method() noexcept;
};

View file

@ -34,14 +34,14 @@ namespace ircd
// (or idle mode) after which it is disconnected.
const auto async_timeout
{
40s
35s
};
// Time limit for how long a connected client can be in "request mode." This
// should never be hit unless there's an error in the handling code.
const auto request_timeout
{
15s
10s
};
// The pool of request contexts. When a client makes a request it does so by acquiring
@ -358,7 +358,7 @@ ircd::handle_client_request(std::shared_ptr<client> client,
{
if(!client->main())
{
//assert(!client->sock || !connected(*client->sock));
close(*client, net::dc::SSL_NOTIFY).wait();
return;
}
@ -516,6 +516,12 @@ catch(const std::exception &e)
return;
}
namespace ircd
{
bool handle_request(client &client, parse::capstan &pc, const http::request::head &head, size_t &content_consumed);
bool handle_request(client &client, parse::capstan &pc);
}
/// Client main.
///
/// Before main(), the client had been sitting in async mode waiting for
@ -528,14 +534,9 @@ bool
ircd::client::main()
noexcept try
{
const auto header_max{8_KiB};
const auto content_max{8_MiB}; //TODO: XXX
const unique_buffer<const mutable_buffer> buffer
{
header_max + content_max
};
parse::buffer pb{buffer};
const auto header_max{4_KiB};
char header_buffer[header_max];
parse::buffer pb{header_buffer};
return handle(pb);
}
catch(const std::exception &e)
@ -659,3 +660,140 @@ catch(const boost::system::system_error &e)
close(*this, net::dc::RST, net::close_ignore);
return false;
}
/// Handle a single request within the client main() loop.
///
/// This function returns false if the main() loop should exit
/// and thus disconnect the client. It should return true in most
/// cases even for lightly erroneous requests that won't affect
/// the next requests on the tape.
///
/// This function is timed. The timeout will prevent a client from
/// sending a partial request and leave us waiting for the rest.
/// As of right now this timeout extends to our handling of the
/// request too.
bool
ircd::handle_request(client &client,
parse::capstan &pc)
try
{
// This is the first read off the wire. The headers are entirely read and
// the tape is advanced.
const http::request::head head{pc};
// The size of HTTP headers are never initially known, which means
// the above head parse could have read too much off the socket bleeding
// into the content or even the next request entirely. That's ok because
// the state of `pc` will reflect that back to the main() loop for the
// next request, but for this request we have to figure out how much of
// the content was accidentally read so far.
size_t content_consumed
{
std::min(pc.unparsed(), head.content_length)
};
bool ret
{
handle_request(client, pc, head, content_consumed)
};
if(ret && iequals(head.connection, "close"_sv))
ret = false;
return ret;
}
catch(const ircd::error &e)
{
log::error("socket(%p) local[%s] remote[%s] in %ld$us: %s",
client.sock.get(),
string(local(client)),
string(remote(client)),
client.request_timer.at<microseconds>().count(),
e.what());
resource::response
{
client, e.what(), {}, http::INTERNAL_SERVER_ERROR
};
throw;
}
bool
ircd::handle_request(client &client,
parse::capstan &pc,
const http::request::head &head,
size_t &content_consumed)
try
{
// The resource is responsible for reading content at its discretion, if
// at all. If we accidentally read some content it has to be presented.
const string_view content_partial
{
pc.parsed, pc.unparsed()
};
// Advance the tape up to the end of the partial content read. We no
// longer use the capstan after this point because the resource reads
// directly off the socket. The `pc` will be positioned properly for the
// next request so long as any remaining content is read off the socket.
pc.parsed += content_consumed;
assert(pc.parsed <= pc.read);
assert(content_partial.size() == content_consumed);
log::debug("socket(%p) local[%s] remote[%s] HTTP %s `%s' content-length:%zu part:%zu",
client.sock.get(),
string(local(client)),
string(remote(client)),
head.method,
head.path,
head.content_length,
content_consumed);
auto &resource
{
ircd::resource::find(head.path)
};
resource(client, head, content_partial, content_consumed);
return true;
}
catch(const http::error &e)
{
resource::response
{
client, e.content, "text/html; charset=utf8", e.code, e.headers
};
switch(e.code)
{
// These codes are "recoverable" and allow the next HTTP request in
// a pipeline to take place. In order for that to happen, any content
// which wasn't read because of the exception has to be read now.
default:
{
assert(client.sock);
const size_t unconsumed{head.content_length - content_consumed};
log::debug("socket(%p) local[%s] remote[%s] discarding %zu of %zu unconsumed content...",
client.sock.get(),
string(local(client)),
string(remote(client)),
unconsumed,
head.content_length);
net::discard_all(*client.sock, unconsumed);
return true;
}
// These codes are "unrecoverable" errors and no more HTTP can be
// conducted with this tape. The client must be disconnected.
case http::BAD_REQUEST:
case http::PAYLOAD_TOO_LARGE:
case http::REQUEST_TIMEOUT:
close(client).wait();
return false;
// The client must also be disconnected at some point down the stack.
case http::INTERNAL_SERVER_ERROR:
throw;
}
}

View file

@ -21,99 +21,10 @@
#include <ircd/m/m.h>
namespace ircd
{
void handle_request(client &client, parse::capstan &pc, const http::request::head &head);
}
decltype(ircd::resource::resources)
ircd::resource::resources
{};
/// Handle a single request within the client main() loop.
///
/// This function returns false if the main() loop should exit
/// and thus disconnect the client. It should return true in most
/// cases even for lightly erroneous requests that won't affect
/// the next requests on the tape.
///
/// This function is timed. The timeout will prevent a client from
/// sending a partial request and leave us waiting for the rest.
/// As of right now this timeout extends to our handling of the
/// request too.
bool
ircd::handle_request(client &client,
parse::capstan &pc)
try
{
bool ret{true};
http::request
{
pc, nullptr, [&client, &pc, &ret]
(const auto &head)
{
handle_request(client, pc, head);
ret = !iequals(head.connection, "close"s);
}
};
return ret;
}
catch(const http::error &e)
{
resource::response
{
client, e.content, "text/html; charset=utf8", e.code, e.headers
};
switch(e.code)
{
case http::BAD_REQUEST:
case http::REQUEST_TIMEOUT:
close(client).wait();
return false;
case http::INTERNAL_SERVER_ERROR:
throw;
default:
return true;
}
}
catch(const ircd::error &e)
{
log::error("client[%s]: in %ld$us: %s",
string(remote(client)),
client.request_timer.at<microseconds>().count(),
e.what());
resource::response
{
client, e.what(), {}, http::INTERNAL_SERVER_ERROR
};
throw;
}
void
ircd::handle_request(client &client,
parse::capstan &pc,
const http::request::head &head)
{
log::debug("client[%s] HTTP %s `%s' (content-length: %zu)",
string(remote(client)),
head.method,
head.path,
head.content_length);
auto &resource
{
ircd::resource::find(head.path)
};
resource(client, pc, head);
}
ircd::resource &
ircd::resource::find(string_view path)
{
@ -287,12 +198,53 @@ ircd::verify_origin(client &client,
void
ircd::resource::operator()(client &client,
parse::capstan &pc,
const http::request::head &head)
const http::request::head &head,
const string_view &content_partial,
size_t &content_read)
{
auto &method(operator[](head.method));
http::request::content content{pc, head};
assert(pc.unparsed() == 0);
// Find the method or METHOD_NOT_ALLOWED
auto &method
{
operator[](head.method)
};
// Bail out if the method limited the amount of content and it was exceeded.
if(head.content_length > method.opts.payload_max)
throw http::error
{
http::PAYLOAD_TOO_LARGE
};
assert(size(content_partial) <= head.content_length);
assert(size(content_partial) == content_read);
const size_t content_remain
{
head.content_length - content_read
};
unique_buffer<mutable_buffer> content_buffer;
string_view content{content_partial};
if(content_remain)
{
// Copy any partial content to the final contiguous allocated buffer;
content_buffer = unique_buffer<mutable_buffer>{head.content_length};
memcpy(data(content_buffer), data(content_partial), size(content_partial));
// Setup a window inside the buffer for the remaining socket read.
const mutable_buffer content_remain_buffer
{
data(content_buffer) + content_read, content_remain
};
//TODO: more discretion from the method.
// Read the remaining content off the socket.
content_read += read_all(*client.sock, content_remain_buffer);
assert(content_read == head.content_length);
content = string_view
{
data(content_buffer), head.content_length
};
}
const auto pathparm
{
@ -310,10 +262,10 @@ ircd::resource::operator()(client &client,
head, content, head.query, parv
};
if(method.flags & method.REQUIRES_AUTH)
if(method.opts.flags & method.REQUIRES_AUTH)
authenticate(client, method, request);
if(method.flags & method.VERIFY_ORIGIN)
if(method.opts.flags & method.VERIFY_ORIGIN)
verify_origin(client, method, request);
handle_request(client, method, request);
@ -389,14 +341,24 @@ catch(const std::out_of_range &e)
};
}
ircd::resource::method::method(struct resource &resource,
const string_view &name,
const handler &handler)
:method
{
resource, name, handler, {}
}
{
}
ircd::resource::method::method(struct resource &resource,
const string_view &name,
const handler &handler,
opts opts)
const struct opts &opts)
:name{name}
,resource{&resource}
,function{handler}
,flags{opts.flags}
,opts{opts}
,methods_it{[this, &name]
{
const auto iit(this->resource->methods.emplace(this->name, this));
@ -433,7 +395,7 @@ catch(const std::bad_function_call &e)
}
ircd::resource::request::request(const http::request::head &head,
http::request::content &content,
const string_view &content,
http::query::string query,
const vector_view<string_view> &parv)
:json::object{content}