diff options
| author | Adam <Adam@anope.org> | 2016-10-30 18:58:11 -0400 |
|---|---|---|
| committer | Adam <Adam@anope.org> | 2016-10-30 18:58:11 -0400 |
| commit | be4083106b245053992e2b15c4c75aa74f5d39de (patch) | |
| tree | f2257da53d82139a7f09a2211abb49ffa1a46f01 | |
| parent | c06cc40d27e5cb83424d89cb19571eb42392abe6 (diff) | |
Add new RESTful JSON API and documentation
| -rw-r--r-- | data/modules.example.conf | 9 | ||||
| -rw-r--r-- | docs/REST.md | 245 | ||||
| -rw-r--r-- | include/modules/httpd.h | 20 | ||||
| -rw-r--r-- | include/serialize.h | 25 | ||||
| -rw-r--r-- | modules/database/sql.cpp | 30 | ||||
| -rw-r--r-- | modules/extra/rest.cpp | 426 | ||||
| -rw-r--r-- | modules/httpd.cpp | 46 | ||||
| -rw-r--r-- | src/serialize.cpp | 4 |
8 files changed, 779 insertions, 26 deletions
diff --git a/data/modules.example.conf b/data/modules.example.conf index a409bc0fb..b75fcd550 100644 --- a/data/modules.example.conf +++ b/data/modules.example.conf @@ -382,6 +382,7 @@ module { name = "help" } port = 3306 } } + /* * redis * @@ -408,6 +409,14 @@ module { name = "help" } } /* + * rest [EXTRA] + * + * This module exposes a RESTful API using JSON via the httpd module. + * See docs/REST.md for details. + */ +#module { name = "rest" } + +/* * rewrite * * Allows rewriting commands sent to/from clients. diff --git a/docs/REST.md b/docs/REST.md new file mode 100644 index 000000000..f8e6adf08 --- /dev/null +++ b/docs/REST.md @@ -0,0 +1,245 @@ +# Anope REST API + +Anope provides a RESTful HTTP API through the rest and httpd modules which +expose basic CRUD operations on all of Anope's internal data structures. + +The type names and data structures are the same as is used in the SQL/RDBMS +databaes support, but it otherwise currently not documented. + +The RESTful API documented here is relatively low level and allows you to +construct things that aren't normally possible in Anope, such as creating +nickless accounts or having an accounts display not matched a grouped nicks +name. This would produce weird behavior and not work as expected. It is +your responsibility to not do this. + +As an alternative you may execute commands remotely using the xmlrpc_main +module, which uses Anope's normal command system and is less subject to misuse. + +# Example usage + +## Getting available object types + +``` +$ curl localhost:8080/rest/types +{ + "types": [ + "botinfo", + "xline", + "oper", + "badword", + "channel", + "level", + "mlockmode", + "accesschanaccess", + "akick", + "entrymsg", + "flagschanaccess", + "logsetting", + "modelock", + "csmiscdata", + "cssuspendinfo", + "xopchanaccess", + "vhost", + "hostrequest", + "memoinfo", + "memo", + "memoignore", + "nick", + "account", + "mode", + "nsaccess", + "autojoin", + "nsmiscdata", + "nssuspendinfo", + "forbid", + "ignore", + "operinfo", + "newsitem", + "exception", + "stats" + ] +} +``` + +The types are subject to change, as several are dynamically added via modules, +including third party modules. + +## Listing objects of a type + +Send a GET to /rest/{object type} to return a list of all objects of that type. +For example, to fetch a list of all accounts. + +``` +$ curl localhost:8080/rest/account +{ + "account": [ + { + "display": "Adam", + "pass": "sha256:55ad6396a287df38967de578cbf92788faea8c94cd7d0e4f55eb45257921d75b:3df6978927389fb71a560a232572e5871bd072752eca27f159dc1fdc2462411c", + "email": "adam@sigterm.info", + "id": "10" + }, + { + "display": "Adam2", + "pass": "sha256:4cbb6ec03a8e7401342ee8832f5e4bd2abeb8fc7a007e66e2ac02e0c14f694f0:041167d04d70381e4218781e06b2b57b08165a987aced63f0a8ca8d25ac8ee51", + "email": "adam@anope.org", + "id": "95" + } + ] +} +``` + +## Searching for objects + +You may search for objects by the value of one of their fields, if it is unique. +This is most useful for looking up nicks, accounts, and channels. + +If the field you are searching for is not something commonly looked up by Anope, +which is mostly anything other than nick, account, and channel names, it will +likely cause the SQL database backend to on-the-fly index the field. + +Send a GET request to /rest/{object type}/{field name}/{search} + +``` +$ curl localhost:8080/rest/account/display/Adam +{ + "display": "Adam", + "pass": "sha256:55ad6396a287df38967de578cbf92788faea8c94cd7d0e4f55eb45257921d75b:3df6978927389fb71a560a232572e5871bd072752eca27f159dc1fdc2462411c", + "email": "adam@sigterm.info", + "id": "10" +} +``` + +Which will return the entire object. If it is not found, a non-200 status code is returned, +along with an error message. Error messages are never JSON formatted, but are simple strings. + +## Getting objects + +If you have an object ID, you can also fetch the object by id. + +Send a GET request to /rest/{object type}/{id} + +``` +$ curl localhost:8080/rest/account/10 +{ + "display": "Adam", + "pass": "sha256:55ad6396a287df38967de578cbf92788faea8c94cd7d0e4f55eb45257921d75b:3df6978927389fb71a560a232572e5871bd072752eca27f159dc1fdc2462411c", + "email": "adam@sigterm.info", + "id": "10" +} +``` + +## Updating the value of a field of an existing object + +You may PUT to /rest/{object type}/{id}/{field} data to update the value of a field. + +``` +$ curl -D - -X PUT localhost:8080/rest/account/10/email -d 'adam@anope.org' +HTTP/1.1 200 OK +Date: Sun, 30 Oct 2016 17:17:05 EDT +Server: Anope-2.1 +Content-Type: application/json +Content-Length: 0 +Connection: Close + +``` + +Again, a 200 status code is okay, anything else is an error, and a description is provided in the reply content. +You can now fetch the object back and notice the changed email address. + +## Creating objects + +You can create new objects by POSTing to /rest/{object type} + +``` +$ curl -D - -X POST localhost:8080/rest/account -d '{"display": "Adam3", "email": "Adam@anope.org"}' +HTTP/1.1 201 Created +Date: Sun, 30 Oct 2016 17:39:56 EDT +Server: Anope-2.1 +Content-Type: application/json +Content-Length: 3 +Connection: Close + +126 +``` + +This will return a 201 Ceated, with the body being the object's new id. + +## Deleting objects + +Similarly you can delete objects by sending DELETE to /rest/{object type}/{id}. + +``` +$ curl -D - -X DELETE localhost:8080/rest/account/126 +HTTP/1.1 200 OK +Date: Sun, 30 Oct 2016 17:42:40 EDT +Server: Anope-2.1 +Content-Type: application/json +Content-Length: 0 +Connection: Close +``` + +Due to the way Anope stores its data, deleting any object at any time should be safe, and will not leak +other (perhaps now unreachable) objects. + +# Object References and Relationships + +Object references are represented in the REST API by {type}:{id} + +``` +$ curl localhost:8080/rest/nsaccess/13 +{ + "account": "account:10", + "mask": "Adam@*.gvd.168.192.IP", + "id": "13" +} +``` + +Such as the value of "account" here for this nickserv access entry. Even though object ids within Anope +are global and not specific to the type. As such, if you submit a POST or PUT operation which creates a +field that is a reference, it is expected you supply both the type and id, in this format. + +It is possible to search for object references of a specific type to a given object. This is how all +one-to-many relationships are looked up within Anope. Objects never contain lists of other objects. +There are no join tables, and the objects within the C++ API do not contain collections of other +objects. + +For example: + +``` +$ curl localhost:8080/rest/account/10?refs=nsaccess +{ + "account": [ + { + "account": "account:10", + "mask": "Adam@*.gvd.168.192.IP", + "id": "13" + }, + { + "account": "account:10", + "mask": "adam@anope.org", + "id": "126" + } + ] +} +``` + +This fetches all objects of type nsaccess which have a reference to an account with id 10. Because +the type nsaccess only has one field that is a reference, the result is effectively the account's +NickServ access list. + +Because searching by refs searches object references and not at a field level, in cases of searching +for objects with mutltiple references, you would have to inspect the resulting objects to see if it +is what you want. For example, if you wanted to find all channels owned by a user, you may perform +something like GET /rest/account/10?refs=channel. The channels returned have a reference to the +given account, whether that is the channels founder or something else, like successor, or any other +field possible (that may have even been added by a 3rd party module), is unknown. + +Relationships defined within Anope, that is, the fields on objects which are object references, can +optionally depend on the existance of the object to which they reference. These fields can be seen +within the SQL schema as having foreign keys with ON CASCADE DELETE. When an object is deleted, all +references to it automatically get either NULLed, or the objects which reference it get recursively +deleted too. This is why it is generally safe to delete any object at any time. + +The field "account" on "nsaccess" is one that will cascade delete, which is why if you delete an +account, the access entries for it are automatically also cleaned up. diff --git a/include/modules/httpd.h b/include/modules/httpd.h index ce28e0622..9cbef9b78 100644 --- a/include/modules/httpd.h +++ b/include/modules/httpd.h @@ -22,12 +22,31 @@ enum HTTPError { HTTP_ERROR_OK = 200, + HTTP_CREATED = 201, HTTP_FOUND = 302, HTTP_BAD_REQUEST = 400, HTTP_PAGE_NOT_FOUND = 404, + HTTP_INTERNAL_SERVER_ERROR = 500, HTTP_NOT_SUPPORTED = 505 }; +namespace httpd +{ + +enum class Method +{ + NONE, + GET, + POST, + PUT, + DELETE, + OPTIONS, + HEAD +}; + +} // namespace httpd + + /* A message to someone */ struct HTTPReply { @@ -93,6 +112,7 @@ struct HTTPReply /* A message from soneone */ struct HTTPMessage { + httpd::Method method = httpd::Method::NONE; std::map<Anope::string, Anope::string> headers; std::map<Anope::string, Anope::string> cookies; std::map<Anope::string, Anope::string> get_data; diff --git a/include/serialize.h b/include/serialize.h index 8f35e61c6..f10563507 100644 --- a/include/serialize.h +++ b/include/serialize.h @@ -95,7 +95,7 @@ class CoreExport Serialize::Object : public Extensible, public virtual Base std::map<TypeBase *, std::vector<Edge>> edges; - std::vector<Edge> GetRefs(TypeBase *); + std::vector<Edge> GetEdges(TypeBase *); public: Object(TypeBase *type); @@ -128,6 +128,8 @@ class CoreExport Serialize::Object : public Extensible, public virtual Base template<typename T> std::vector<T> GetRefs(); + std::vector<Object *> GetRefs(Serialize::TypeBase *); + /** * Get the value of a field on this object. */ @@ -867,6 +869,25 @@ T Serialize::New() return static_cast<T>(type->Create()); } +inline std::vector<Serialize::Object *> Serialize::Object::GetRefs(Serialize::TypeBase *type) +{ + std::vector<Serialize::TypeBase *> types = GetTypes(type->GetName()); + std::vector<Object *> objs; + + if (types.empty()) + { + Log(LOG_DEBUG) << "GetRefs for unknown type on #" << this->id << " type " << s_type->GetName() << " named " << type->GetName(); + return objs; + } + + for (Serialize::TypeBase *t : types) + for (const Serialize::Edge &edge : GetEdges(t)) + if (!edge.direction) + objs.push_back(edge.other); + + return objs; +} + template<typename T> std::vector<T> Serialize::Object::GetRefs() { @@ -881,7 +902,7 @@ std::vector<T> Serialize::Object::GetRefs() } for (Serialize::TypeBase *t : types) - for (const Serialize::Edge &edge : GetRefs(t)) + for (const Serialize::Edge &edge : GetEdges(t)) if (!edge.direction) objs.push_back(anope_dynamic_static_cast<T>(edge.other)); diff --git a/modules/database/sql.cpp b/modules/database/sql.cpp index f88fc35d7..50447c005 100644 --- a/modules/database/sql.cpp +++ b/modules/database/sql.cpp @@ -344,16 +344,32 @@ class DBSQL : public Module, public Pipe EventReturn OnSerializeHasField(Serialize::Object *object, Serialize::FieldBase *field) override { - SQL::Result::Value v; + if (field->object) + { + Anope::string type; + Serialize::ID id; - if (!GetValue(object, field, v)) - return EVENT_CONTINUE; + EventReturn er = OnSerializeGetSerializable(object, field, type, id); - if (v.null) - return EVENT_CONTINUE; + if (er != EVENT_ALLOW) + return EVENT_CONTINUE; - field->UnserializeFromString(object, v.value); - return EVENT_STOP; + field->UnserializeFromString(object, type + ":" + stringify(id)); + return EVENT_STOP; + } + else + { + SQL::Result::Value v; + + if (!GetValue(object, field, v)) + return EVENT_CONTINUE; + + if (v.null) + return EVENT_CONTINUE; + + field->UnserializeFromString(object, v.value); + return EVENT_STOP; + } } EventReturn OnSerializableGetId(Serialize::ID &id) override diff --git a/modules/extra/rest.cpp b/modules/extra/rest.cpp new file mode 100644 index 000000000..187b2147f --- /dev/null +++ b/modules/extra/rest.cpp @@ -0,0 +1,426 @@ +/* + * Anope IRC Services + * + * Copyright (C) 2016 Anope Team <team@anope.org> + * + * This file is part of Anope. Anope is free software; you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software + * Foundation, version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see see <http://www.gnu.org/licenses/>. + */ + +#include "module.h" +#include "modules/httpd.h" + +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/json_parser.hpp> + +class RESTService : public HTTPPage +{ + public: + RESTService(Module *creator, const Anope::string &sname) : HTTPPage("/rest", "application/json") + { + } + + bool OnRequest(HTTPProvider *provider, const Anope::string &page_name, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) override + { + sepstream sep(page_name, '/'); + Anope::string token; + + sep.GetToken(token); // /rest + sep.GetToken(token); // type + + if (token == "types") + { + GetTypes(provider, client, message, reply); + return true; + } + + Serialize::TypeBase *type = Serialize::TypeBase::Find(token); + if (type == nullptr) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Unknown type"); + return true; + } + + if (message.method == httpd::Method::POST) + { + Post(provider, client, message, reply, type, sep); + } + else if (message.method == httpd::Method::GET) + { + Get(provider, client, message, reply, type, sep); + } + else if (message.method == httpd::Method::PUT) + { + Put(provider, client, message, reply, type, sep); + } + else if (message.method == httpd::Method::DELETE) + { + Delete(provider, client, message, reply, type, sep); + } + else + { + reply.error = HTTP_NOT_SUPPORTED; + reply.Write("Not Supported"); + } + + return true; + } + + private: + /** Get all object type names + */ + void GetTypes(HTTPProvider *provider, HTTPClient *client, HTTPMessage &message, HTTPReply &reply) + { + boost::property_tree::ptree tree, children; + + for (Serialize::TypeBase *stype : Serialize::TypeBase::GetTypes()) + { + boost::property_tree::ptree child; + child.put("", stype->GetName().str()); + children.push_back(std::make_pair("", child)); + } + + tree.add_child("types", children); + + std::stringstream ss; + boost::property_tree::write_json(ss, tree); + + reply.Write(ss.str()); + } + + void Post(HTTPProvider *provider, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, Serialize::TypeBase *type, sepstream &sep) + { + boost::property_tree::ptree tree; + std::stringstream ss(message.content.str()); + + try + { + boost::property_tree::read_json(ss, tree); + } + catch (const boost::property_tree::json_parser::json_parser_error &) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("Unable to parse JSON"); + return; + } + + for (auto &p : tree) + { + const std::string &key = p.first; + + Serialize::FieldBase *field = type->GetField(key); + if (field == nullptr) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("No such field " + field->serialize_name); + return; + } + } + + Serialize::Object *object = type->Create(); + if (object == nullptr) + { + reply.error = HTTP_INTERNAL_SERVER_ERROR; + reply.Write("Unable to create new object"); + return; + } + + for (auto &p : tree) + { + const std::string &key = p.first; + const boost::property_tree::ptree &pt = p.second; + + Serialize::FieldBase *field = type->GetField(key); + if (field == nullptr) + { + reply.error = HTTP_INTERNAL_SERVER_ERROR; + reply.Write("Missing field"); + return; + } + + field->UnserializeFromString(object, pt.get_value<std::string>()); + } + + reply.error = HTTP_CREATED; + reply.Write(stringify(object->id)); + } + + void Get(HTTPProvider *provider, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, Serialize::TypeBase *type, sepstream &sep) + { + Anope::string token; + sep.GetToken(token); + + if (token.empty()) + { + GetList(reply, type); + return; + } + + if (token.is_pos_number_only()) + { + GetID(message, reply, type, token); + return; + } + + GetField(reply, type, token, sep); + } + + boost::property_tree::ptree ToJson(Serialize::Object *object) + { + boost::property_tree::ptree tree; + + for (Serialize::FieldBase *field : object->GetSerializableType()->GetFields()) + { + if (field->HasFieldS(object)) // for ext + tree.put(field->serialize_name.str(), field->SerializeToString(object).str()); + } + + tree.put("id", object->id); + return tree; + } + + /** Get a list of all objects of a type + */ + void GetList(HTTPReply &reply, Serialize::TypeBase *type) + { + boost::property_tree::ptree tree, children; + + for (Serialize::Object *object : Serialize::GetObjects_<Serialize::Object *>(type)) + { + boost::property_tree::ptree child = ToJson(object);; + children.push_back(std::make_pair("", child)); + } + + tree.add_child(type->GetName().str(), children); + + std::stringstream ss; + boost::property_tree::write_json(ss, tree); + + reply.Write(ss.str()); + } + + /** Get object by id + */ + void GetID(HTTPMessage &message, HTTPReply &reply, Serialize::TypeBase *type, const Anope::string &token) + { + Serialize::ID id; + try + { + id = convertTo<Serialize::ID>(token); + } + catch (const ConvertException &ex) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("Invalid id"); + return; + } + + if (message.get_data.count("refs") > 0) + { + Serialize::TypeBase *ref_type = Serialize::TypeBase::Find(message.get_data["refs"]); + + if (ref_type == nullptr) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Unknown reference type"); + return; + } + + GetRefs(reply, type, id, ref_type); + return; + } + + GetID(reply, type, id); + } + + void GetID(HTTPReply &reply, Serialize::TypeBase *type, Serialize::ID id) + { + Serialize::Object *object = type->Require(id); + if (object == nullptr) + { + /* This has to be a cached object with type/id mismatch */ + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Type/id mismatch"); + return; + } + + boost::property_tree::ptree tree = ToJson(object); + + std::stringstream ss; + boost::property_tree::write_json(ss, tree); + + reply.Write(ss.str()); + } + + /** Get object by field and value + */ + void GetField(HTTPReply &reply, Serialize::TypeBase *type, const Anope::string &token, sepstream &sep) + { + Serialize::FieldBase *field = type->GetField(token); + if (field == nullptr) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("No such field"); + return; + } + + Anope::string value; + sep.GetToken(value); + + Serialize::ID id; + EventReturn result = EventManager::Get()->Dispatch(&Event::SerializeEvents::OnSerializeFind, type, field, value, id); + if (result != EVENT_ALLOW) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Not found"); + return; + } + + GetID(reply, type, id); + } + + /** Find refs of type ref_type to object id of type type + */ + void GetRefs(HTTPReply &reply, Serialize::TypeBase *type, Serialize::ID id, Serialize::TypeBase *ref_type) + { + Serialize::Object *object = type->Require(id); + if (object == nullptr) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Type/id mismatch"); + return; + } + + boost::property_tree::ptree tree, children; + + for (Serialize::Object *ref : object->GetRefs(ref_type)) + { + boost::property_tree::ptree child = ToJson(ref); + children.push_back(std::make_pair("", child)); + } + + tree.add_child(type->GetName().str(), children); + + std::stringstream ss; + boost::property_tree::write_json(ss, tree); + + reply.Write(ss.str()); + } + + void Put(HTTPProvider *provider, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, Serialize::TypeBase *type, sepstream &sep) + { + Anope::string token; + Anope::string fieldname; + + if (!sep.GetToken(token) || !sep.GetToken(fieldname)) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("No id and/or field"); + return; + } + + Serialize::ID id; + try + { + id = convertTo<Serialize::ID>(token); + } + catch (const ConvertException &ex) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("Invalid id"); + return; + } + + Serialize::Object *object = type->Require(id); + if (object == nullptr) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Type/id mismatch"); + return; + } + + Serialize::FieldBase *field = type->GetField(fieldname); + if (field == nullptr) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("No such field"); + return; + } + + field->UnserializeFromString(object, message.content); + } + + void Delete(HTTPProvider *provider, HTTPClient *client, HTTPMessage &message, HTTPReply &reply, Serialize::TypeBase *type, sepstream &sep) + { + Anope::string token; + sep.GetToken(token); + + Serialize::ID id; + try + { + id = convertTo<Serialize::ID>(token); + } + catch (const ConvertException &ex) + { + reply.error = HTTP_BAD_REQUEST; + reply.Write("Invalid id"); + return; + } + + Serialize::Object *object = type->Require(id); + if (object == nullptr) + { + reply.error = HTTP_PAGE_NOT_FOUND; + reply.Write("Type/id mismatch"); + return; + } + + object->Delete(); + } +}; + +class ModuleREST : public Module +{ + ServiceReference<HTTPProvider> httpref; + + public: + RESTService restinterface; + + ModuleREST(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR) + , restinterface(this, "rest") + { + + } + + ~ModuleREST() + { + if (httpref) + httpref->UnregisterPage(&restinterface); + } + + void OnReload(Configuration::Conf *conf) override + { + if (httpref) + httpref->UnregisterPage(&restinterface); + + this->httpref = ServiceReference<HTTPProvider>(conf->GetModule(this)->Get<Anope::string>("server", "httpd/main")); + + if (!httpref) + throw ConfigException("Unable to find http reference, is m_httpd loaded?"); + + httpref->RegisterPage(&restinterface); + } +}; + +MODULE_INIT(ModuleREST) diff --git a/modules/httpd.cpp b/modules/httpd.cpp index 11f8318e7..41cd4a4ec 100644 --- a/modules/httpd.cpp +++ b/modules/httpd.cpp @@ -35,12 +35,16 @@ static Anope::string GetStatusFromCode(HTTPError err) { case HTTP_ERROR_OK: return "200 OK"; + case HTTP_CREATED: + return "201 Created"; case HTTP_FOUND: return "302 Found"; case HTTP_BAD_REQUEST: return "400 Bad Request"; case HTTP_PAGE_NOT_FOUND: return "404 Not Found"; + case HTTP_INTERNAL_SERVER_ERROR: + return "500 Internal Server Error"; case HTTP_NOT_SUPPORTED: return "505 HTTP Version Not Supported"; } @@ -59,13 +63,6 @@ class MyHTTPClient : public HTTPClient unsigned content_length; - enum - { - ACTION_NONE, - ACTION_GET, - ACTION_POST - } action; - void Serve() { if (this->served) @@ -105,7 +102,7 @@ class MyHTTPClient : public HTTPClient public: time_t created; - MyHTTPClient(HTTPProvider *l, int f, const sockaddrs &a) : Socket(f, l->IsIPv6()), HTTPClient(l, f, a), provider(l), header_done(false), served(false), ip(a.addr()), content_length(0), action(ACTION_NONE), created(Anope::CurTime) + MyHTTPClient(HTTPProvider *l, int f, const sockaddrs &a) : Socket(f, l->IsIPv6()), HTTPClient(l, f, a), provider(l), header_done(false), served(false), ip(a.addr()), content_length(0), created(Anope::CurTime) { Log(LOG_DEBUG, "httpd") << "Accepted connection " << f << " from " << a.addr(); } @@ -168,12 +165,13 @@ class MyHTTPClient : public HTTPClient { Log(LOG_DEBUG_2) << "HTTP from " << this->clientaddr.addr() << ": " << buf; - if (this->action == ACTION_NONE) + if (message.method == httpd::Method::NONE) { std::vector<Anope::string> params; spacesepstream(buf).GetTokens(params); - if (params.empty() || (params[0] != "GET" && params[0] != "POST")) + if (params.empty() || (params[0] != "GET" && params[0] != "POST" && params[0] != "PUT" + && params[0] != "DELETE" && params[0] != "OPTIONS" && params[0] != "HEAD")) { this->SendError(HTTP_BAD_REQUEST, "Unknown operation"); return true; @@ -186,9 +184,17 @@ class MyHTTPClient : public HTTPClient } if (params[0] == "GET") - this->action = ACTION_GET; + message.method = httpd::Method::GET; else if (params[0] == "POST") - this->action = ACTION_POST; + message.method = httpd::Method::POST; + else if (params[0] == "PUT") + message.method = httpd::Method::PUT; + else if (params[0] == "DELETE") + message.method = httpd::Method::DELETE; + else if (params[0] == "OPTIONS") + message.method = httpd::Method::OPTIONS; + else if (params[0] == "HEAD") + message.method = httpd::Method::HEAD; Anope::string targ = params[1]; size_t q = targ.find('?'); @@ -339,9 +345,19 @@ class MyHTTPProvider : public HTTPProvider, public Timer HTTPPage* FindPage(const Anope::string &pname) override { - if (this->pages.count(pname) == 0) - return NULL; - return this->pages[pname]; + Anope::string resource = pname; + + while (this->pages.count(resource) == 0) + { + size_t s = resource.find_last_of('/'); + + if (s == Anope::string::npos) + return nullptr; + + resource = resource.substr(0, s); + } + + return this->pages[resource]; } }; diff --git a/src/serialize.cpp b/src/serialize.cpp index 20c05e502..8f9b91d75 100644 --- a/src/serialize.cpp +++ b/src/serialize.cpp @@ -62,7 +62,7 @@ void Serialize::Unregister(Module *m) field->Unregister(); } -std::vector<Edge> Object::GetRefs(TypeBase *type) +std::vector<Edge> Object::GetEdges(TypeBase *type) { std::vector<Edge> refs; EventReturn result = EventManager::Get()->Dispatch(&Event::SerializeEvents::OnSerializeGetRefs, this, type, refs); @@ -151,7 +151,7 @@ void Object::Delete() Log(LOG_DEBUG_2) << "Deleting object id #" << id << " type " << s_type->GetName(); /* Delete dependant objects */ - for (const Edge &edge : GetRefs(nullptr)) + for (const Edge &edge : GetEdges(nullptr)) { Object *other = edge.other; FieldBase *field = edge.field; |
