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:
parent
2172b39f9e
commit
30fb1d59d7
2 changed files with 232 additions and 88 deletions
|
@ -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
|
||||||
|
|
228
ircd/resource.cc
228
ircd/resource.cc
|
@ -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)}
|
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue