summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam <Adam@anope.org>2016-10-30 18:58:11 -0400
committerAdam <Adam@anope.org>2016-10-30 18:58:11 -0400
commitbe4083106b245053992e2b15c4c75aa74f5d39de (patch)
treef2257da53d82139a7f09a2211abb49ffa1a46f01
parentc06cc40d27e5cb83424d89cb19571eb42392abe6 (diff)
Add new RESTful JSON API and documentation
-rw-r--r--data/modules.example.conf9
-rw-r--r--docs/REST.md245
-rw-r--r--include/modules/httpd.h20
-rw-r--r--include/serialize.h25
-rw-r--r--modules/database/sql.cpp30
-rw-r--r--modules/extra/rest.cpp426
-rw-r--r--modules/httpd.cpp46
-rw-r--r--src/serialize.cpp4
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;