mirror of
https://github.com/matrix-construct/construct
synced 2024-11-29 10:12:39 +01:00
ircd: Reflow the request handlers to give the resource more control over content.
This commit is contained in:
parent
1f7ed51f3b
commit
04fa556c58
3 changed files with 231 additions and 121 deletions
|
@ -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;
|
||||
};
|
||||
|
|
160
ircd/client.cc
160
ircd/client.cc
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
160
ircd/resource.cc
160
ircd/resource.cc
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue