0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-12-26 23:44:01 +01:00

ircd: Preliminary resource handler infrastructure.

This commit is contained in:
Jason Volk 2017-08-23 15:06:14 -06:00
parent 2172b39f9e
commit 30fb1d59d7
2 changed files with 232 additions and 88 deletions

View file

@ -27,32 +27,44 @@ namespace ircd {
struct resource struct resource
{ {
IRCD_EXCEPTION(ircd::error, error) IRCD_EXCEPTION(ircd::error, error)
IRCD_EXCEPTION(error, not_found)
IRCD_EXCEPTION(error, already_exists)
struct member;
struct method; struct method;
struct request; struct request;
struct response; struct response;
static std::map<string_view, resource *> resources; enum flag
{
DIRECTORY = 0x01,
};
const char *const name; struct opts
const char *const description; {
flag flags{flag(0)};
string_view description;
};
static std::map<string_view, resource *, iless> resources;
static std::map<std::string, client *, std::less<>> tokens; //TODO: XXX
string_view path;
string_view description;
flag flags;
std::map<string_view, method *> methods; std::map<string_view, method *> methods;
unique_const_iterator<decltype(resources)> resources_it;
protected: private:
decltype(resources)::const_iterator resources_it; virtual void handle_request(client &, method &, resource::request &);
void call_method(client &, method &, resource::request &);
public: public:
method &operator[](const string_view &path);
void operator()(client &, parse::capstan &, const http::request::head &); void operator()(client &, parse::capstan &, const http::request::head &);
resource(const char *const &name, resource(const string_view &path, const string_view &description, opts = {{}});
const char *const &description = ""); resource(const string_view &path, opts = {{}});
resource() = default;
virtual ~resource() noexcept; virtual ~resource() noexcept;
static resource &find(string_view path);
}; };
struct resource::request struct resource::request
@ -60,54 +72,46 @@ struct resource::request
{ {
const http::request::head &head; const http::request::head &head;
http::request::content &content; http::request::content &content;
http::query::string query;
request(const http::request::head &head, http::request::content &content) request(const http::request::head &head, http::request::content &content, http::query::string query);
:json::doc{content}
,head{head}
,content{content}
{}
}; };
struct resource::response struct resource::response
{ {
response(client &, const json::doc &doc, const http::code & = http::OK); response(client &, const string_view &str, const string_view &ct = "text/plain; charset=utf8", const http::code & = http::OK);
response(client &, const json::doc &doc = "{}", const http::code & = http::OK);
response(client &, const json::obj &obj, const http::code & = http::OK); response(client &, const json::obj &obj, const http::code & = http::OK);
response(client &, const http::code &, const json::obj &obj);
response() = default; response() = default;
~response() noexcept;
}; };
struct resource::method struct resource::method
:std::function<response (client &, request &)>
{ {
enum flag
{
REQUIRES_AUTH = 0x01,
RATE_LIMITED = 0x02,
};
struct opts
{
flag flags{flag(0)};
};
using handler = std::function<response (client &, request &)>; using handler = std::function<response (client &, request &)>;
protected: string_view name;
public:
const char *const name;
struct resource *resource; struct resource *resource;
decltype(resource::methods)::const_iterator methods_it; handler function;
std::map<string_view, member *> members; flag flags;
unique_const_iterator<decltype(resource::methods)> methods_it;
public: public:
method(struct resource &, virtual response operator()(client &, request &);
const char *const &name,
const handler &,
const std::initializer_list<member *> & = {});
~method() noexcept; method(struct resource &, const string_view &name, const handler &, opts = {{}});
}; virtual ~method() noexcept;
struct resource::member
{
IRCD_EXCEPTION(resource::error, error)
using validator = std::function<void (const string_view &)>;
const char *name;
enum json::type type;
validator valid;
member(const char *const &name, const enum json::type &, validator = {});
}; };
} // namespace ircd } // namespace ircd

View file

@ -26,46 +26,142 @@ decltype(resource::resources)
resource::resources resource::resources
{}; {};
IRCD_INIT_PRIORITY(STD_CONTAINER)
decltype(resource::tokens)
resource::tokens
{};
} // namespace ircd } // namespace ircd
ircd::resource &
ircd::resource::resource(const char *const &name, ircd::resource::find(string_view path)
const char *const &description)
:name{name}
,description{description}
,resources_it{[this, &name]
{ {
const auto iit(resources.emplace(this->name, this)); path = lstrip(path, '/');
if(!iit.second) path = strip(path, '/');
throw error("resource \"%s\" already registered", name); auto it(resources.lower_bound(path));
if(it == end(resources))
return *resources.at(string_view{});
return iit.first; // Exact file or directory match
if(path == it->first)
return *it->second;
// Directories handle all paths under them.
if(!startswith(path, it->first))
{
// Walk the iterator back to find if there is a directory prefixing this path.
if(it == begin(resources))
throw http::error(http::code::NOT_FOUND);
--it;
if(!startswith(path, it->first))
throw http::error(http::code::NOT_FOUND);
}
// Check if the resource is a directory; if not, it can only
// handle exact path matches.
if(~it->second->flags & it->second->DIRECTORY && path != it->first)
throw http::error(http::code::NOT_FOUND);
return *it->second;
}
ircd::resource::resource(const string_view &path,
opts opts)
:resource
{
path, opts.description, opts
}
{
}
ircd::resource::resource(const string_view &path,
const string_view &description,
opts opts)
:path{path}
,description{description}
,flags{opts.flags}
,resources_it{[this, &path]
{
const auto iit(resources.emplace(this->path, this));
if(!iit.second)
throw error("resource \"%s\" already registered", path);
return unique_const_iterator<decltype(resources)>
{
resources, iit.first
};
}()} }()}
{ {
log::info("Registered resource \"%s\" by handler @ %p", name, this); log::info("Registered resource \"%s\" by handler @ %p", path, this);
} }
ircd::resource::~resource() ircd::resource::~resource()
noexcept noexcept
{ {
resources.erase(resources_it); log::info("Unregistered resource \"%s\" by handler @ %p", path, this);
log::info("Unregistered resource \"%s\" by handler @ %p", name, this);
} }
namespace ircd {
static void
authenticate(client &client,
resource::method &method,
resource::request &request)
try
{
const auto &access_token(request.query.at("access_token"));
if(access_token == "charybdisLETMEIN")
return;
const auto it(resource::tokens.find(access_token));
if(it == end(resource::tokens) || it->second != &client)
{
throw m::error
{
// "When credentials are required but missing or invalid, the HTTP call will "
// "return with a status of 401 and the error code, M_MISSING_TOKEN or "
// "M_UNKNOWN_TOKEN respectively."
http::UNAUTHORIZED, "M_UNKNOWN_TOKEN", "Credentials for this method are required but invalid."
};
}
}
catch(const std::out_of_range &e)
{
throw m::error
{
// "When credentials are required but missing or invalid, the HTTP call will return "
// "with a status of 401 and the error code, M_MISSING_TOKEN or M_UNKNOWN_TOKEN "
// "respectively."
http::UNAUTHORIZED, "M_MISSING_TOKEN", "Credentials for this method are required but missing."
};
}
} // namespace ircd
void void
ircd::resource::operator()(client &client, ircd::resource::operator()(client &client,
parse::capstan &pc, parse::capstan &pc,
const http::request::head &head) const http::request::head &head)
try
{ {
auto &method(*methods.at(head.method)); auto &method(operator[](head.method));
http::request::content content{pc, head}; http::request::content content{pc, head};
resource::request request resource::request request
{ {
head, content head, content, head.query
}; };
call_method(client, method, request); if(method.flags & method.REQUIRES_AUTH)
authenticate(client, method, request);
handle_request(client, method, request);
}
ircd::resource::method &
ircd::resource::operator[](const string_view &name)
try
{
return *methods.at(name);
} }
catch(const std::out_of_range &e) catch(const std::out_of_range &e)
{ {
@ -76,9 +172,9 @@ catch(const std::out_of_range &e)
} }
void void
ircd::resource::call_method(client &client, ircd::resource::handle_request(client &client,
method &method, method &method,
resource::request &request) resource::request &request)
try try
{ {
method(client, request); method(client, request);
@ -92,29 +188,63 @@ catch(const json::error &e)
} }
ircd::resource::method::method(struct resource &resource, ircd::resource::method::method(struct resource &resource,
const char *const &name, const string_view &name,
const handler &handler, const handler &handler,
const std::initializer_list<member *> &members) opts opts)
:function{handler} :name{name}
,name{name}
,resource{&resource} ,resource{&resource}
,function{handler}
,flags{opts.flags}
,methods_it{[this, &name] ,methods_it{[this, &name]
{ {
const auto iit(this->resource->methods.emplace(this->name, this)); const auto iit(this->resource->methods.emplace(this->name, this));
if(!iit.second) if(!iit.second)
throw error("resource \"%s\" already registered", name); throw error("resource \"%s\" already registered", name);
return iit.first; return unique_const_iterator<decltype(resource::methods)>
{
this->resource->methods,
iit.first
};
}()} }()}
{ {
for(const auto &member : members)
this->members.emplace(member->name, member);
} }
ircd::resource::method::~method() ircd::resource::method::~method()
noexcept noexcept
{ {
resource->methods.erase(methods_it); }
ircd::resource::response
ircd::resource::method::operator()(client &client,
request &request)
try
{
return function(client, request);
}
catch(const std::bad_function_call &e)
{
throw http::error
{
http::SERVICE_UNAVAILABLE
};
}
ircd::resource::request::request(const http::request::head &head,
http::request::content &content,
http::query::string query)
:json::doc{content}
,head{head}
,content{content}
,query{std::move(query)}
{
}
ircd::resource::response::response(client &client,
const http::code &code,
const json::obj &obj)
:response{client, obj, code}
{
} }
ircd::resource::response::response(client &client, ircd::resource::response::response(client &client,
@ -122,7 +252,7 @@ ircd::resource::response::response(client &client,
const http::code &code) const http::code &code)
try try
{ {
char cbuf[1024], *out(cbuf); char cbuf[4096], *out(cbuf);
const auto doc(serialize(obj, out, cbuf + sizeof(cbuf))); const auto doc(serialize(obj, out, cbuf + sizeof(cbuf)));
response(client, doc, code); response(client, doc, code);
} }
@ -137,26 +267,36 @@ catch(const json::error &e)
ircd::resource::response::response(client &client, ircd::resource::response::response(client &client,
const json::doc &doc, const json::doc &doc,
const http::code &code) const http::code &code)
{
const auto content_type
{
"application/json; charset=utf-8"
};
response(client, doc, content_type, code);
}
ircd::resource::response::response(client &client,
const string_view &str,
const string_view &content_type,
const http::code &code)
{ {
http::response http::response
{ {
code, doc, write_closure(client), code, str, write_closure(client),
{ {
{ "Content-Type", "application/json" } { "Content-Type", content_type },
{ "Access-Control-Allow-Origin", "*" }
} }
}; };
}
ircd::resource::response::~response() log::debug("client[%s] HTTP %d %s (%s) content-length: %zu %s...",
noexcept string(remote_addr(client)),
{ int(code),
} http::reason[code],
content_type,
ircd::resource::member::member(const char *const &name, str.size(),
const enum json::type &type, startswith(content_type, "text") ||
validator valid) content_type == "application/json" ||
:name{name} content_type == "application/javascript"? str.substr(0, 96) : string_view{});
,type{type}
,valid{std::move(valid)}
{
} }