From b58573f1235a4c90f32d179c35ec59261453a6f1 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Mon, 17 Aug 2020 17:03:54 +0200 Subject: [PATCH] api: rewrite API to v2 --- include/villas/api.hpp | 33 +- include/villas/api/action.hpp | 87 ----- include/villas/api/server.hpp | 81 ---- include/villas/api/session.hpp | 66 ++-- include/villas/api/sessions/http.hpp | 61 --- include/villas/api/sessions/websocket.hpp | 55 --- lib/api.cpp | 48 ++- lib/api/CMakeLists.txt | 33 +- .../sessions/wsi.hpp => lib/api/request.cpp | 56 ++- .../{actions => requests}/capabiltities.cpp | 26 +- lib/api/{actions => requests}/config.cpp | 26 +- lib/api/requests/file.cpp | 77 ++++ .../node.cpp => requests/node_action.cpp} | 60 +-- lib/api/{actions => requests}/nodes.cpp | 24 +- lib/api/requests/path_action.cpp | 88 +++++ lib/api/requests/paths.cpp | 123 ++++++ lib/api/{actions => requests}/restart.cpp | 35 +- lib/api/{actions => requests}/shutdown.cpp | 24 +- .../{actions/paths.cpp => requests/stats.cpp} | 55 +-- .../stats.cpp => requests/stats_reset.cpp} | 46 +-- lib/api/{actions => requests}/status.cpp | 28 +- .../socket.hpp => lib/api/response.cpp | 53 ++- lib/api/server.cpp | 211 ----------- lib/api/session.cpp | 349 ++++++++++++++---- lib/api/sessions/http.cpp | 212 ----------- lib/api/sessions/socket.cpp | 85 ----- lib/api/sessions/websocket.cpp | 155 -------- lib/api/sessions/wsi.cpp | 74 ---- 28 files changed, 913 insertions(+), 1358 deletions(-) delete mode 100644 include/villas/api/action.hpp delete mode 100644 include/villas/api/server.hpp delete mode 100644 include/villas/api/sessions/http.hpp delete mode 100644 include/villas/api/sessions/websocket.hpp rename include/villas/api/sessions/wsi.hpp => lib/api/request.cpp (62%) rename lib/api/{actions => requests}/capabiltities.cpp (77%) rename lib/api/{actions => requests}/config.cpp (73%) create mode 100644 lib/api/requests/file.cpp rename lib/api/{actions/node.cpp => requests/node_action.cpp} (56%) rename lib/api/{actions => requests}/nodes.cpp (84%) create mode 100644 lib/api/requests/path_action.cpp create mode 100644 lib/api/requests/paths.cpp rename lib/api/{actions => requests}/restart.cpp (79%) rename lib/api/{actions => requests}/shutdown.cpp (71%) rename lib/api/{actions/paths.cpp => requests/stats.cpp} (52%) rename lib/api/{actions/stats.cpp => requests/stats_reset.cpp} (68%) rename lib/api/{actions => requests}/status.cpp (71%) rename include/villas/api/sessions/socket.hpp => lib/api/response.cpp (66%) delete mode 100644 lib/api/server.cpp delete mode 100644 lib/api/sessions/http.cpp delete mode 100644 lib/api/sessions/socket.cpp delete mode 100644 lib/api/sessions/websocket.cpp delete mode 100644 lib/api/sessions/wsi.cpp diff --git a/include/villas/api.hpp b/include/villas/api.hpp index 4da171e3b..935a5f211 100644 --- a/include/villas/api.hpp +++ b/include/villas/api.hpp @@ -30,19 +30,49 @@ #include #include +#include + #include #include #include #include +#include namespace villas { namespace node { namespace api { -const int version = 1; +const int version = 2; /* Forward declarations */ class Session; +class Response; +class Request; + +class Error : public RuntimeError { + +public: + Error(int c = HTTP_STATUS_INTERNAL_SERVER_ERROR, const std::string &msg = "Invalid API request") : + RuntimeError(msg), + code(c) + { } + + int code; +}; + +class BadRequest : public Error { + +public: + BadRequest(const std::string &msg = "Bad API request") : + Error(HTTP_STATUS_BAD_REQUEST, msg) + { } +}; + +class InvalidMethod : public BadRequest { + +public: + InvalidMethod(Request *req); +}; } /* namespace api */ @@ -60,7 +90,6 @@ protected: std::atomic running; /**< Atomic flag for signalizing thread termination. */ SuperNode *super_node; - api::Server server; void run(); void worker(); diff --git a/include/villas/api/action.hpp b/include/villas/api/action.hpp deleted file mode 100644 index 3206a0072..000000000 --- a/include/villas/api/action.hpp +++ /dev/null @@ -1,87 +0,0 @@ -/** REST-API-releated functions. - * - * @file - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#pragma once - -#include - -#include -#include - -namespace villas { -namespace node { -namespace api { - -/* Forward declarations */ -class Session; - -/** API action descriptor */ -class Action { - -protected: - Session *session; - - Logger logger; - -public: - Action(Session *s) : - session(s) - { - logger = logging.get("api:action"); - } - - virtual int execute(json_t *args, json_t **resp) = 0; -}; - -class ActionFactory : public plugin::Plugin { - -public: - using plugin::Plugin::Plugin; - - virtual Action * make(Session *s) = 0; -}; - -template -class ActionPlugin : public ActionFactory { - -public: - using ActionFactory::ActionFactory; - - virtual Action * make(Session *s) { - return new T(s); - } - - // Get plugin name - virtual std::string - getName() const - { return name; } - - // Get plugin description - virtual std::string - getDescription() const - { return desc; } -}; - -} /* namespace api */ -} /* namespace node */ -} /* namespace villas */ diff --git a/include/villas/api/server.hpp b/include/villas/api/server.hpp deleted file mode 100644 index 341c6f4a2..000000000 --- a/include/villas/api/server.hpp +++ /dev/null @@ -1,81 +0,0 @@ -/** Socket API endpoint. - * - * @file - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include -#include - -namespace villas { -namespace node { - -/* Forward declarations */ -class Api; - -namespace api { -namespace sessions { - -/* Forward declarations */ -class Socket; - -} /* namespace sessions */ - -class Server { - -protected: - enum State state; - - Api *api; - Logger logger; - - int sd; - - std::vector pfds; - std::map sessions; - - void acceptNewSession(); - void closeSession(sessions::Socket *s); - - struct sockaddr_un getSocketAddress(); - -public: - Server(Api *a); - ~Server(); - - void start(); - void stop(); - - void run(int timeout = 100); -}; - -} /* namespace api */ -} /* namespace node */ -} /* namespace villas */ diff --git a/include/villas/api/session.hpp b/include/villas/api/session.hpp index 608a54a57..4d73d0693 100644 --- a/include/villas/api/session.hpp +++ b/include/villas/api/session.hpp @@ -23,6 +23,8 @@ #pragma once +#include + #include #include @@ -35,10 +37,15 @@ namespace node { /* Forward declarations */ class SuperNode; class Api; +class Web; namespace api { -/** A connection via HTTP REST or WebSockets to issue API actions. */ +/* Forward declarations */ +class Request; +class Response; + +/** A connection via HTTP REST or WebSockets to issue API requests. */ class Session { public: @@ -48,47 +55,54 @@ public: }; enum Version { - UNKOWN = 0, - VERSION_1 = 1 + UNKNOWN_VERSION = 0, + VERSION_1 = 1, + VERSION_2 = 2 }; protected: enum State state; enum Version version; - Logger logger; - - int runs; - - struct { - JsonBuffer buffer; - Queue queue; - } request, response; + lws *wsi; + Web *web; Api *api; + Logger logger; + + JsonBuffer requestBuffer; + JsonBuffer responseBuffer; + + std::atomic request; + std::atomic response; + + bool headersSent; + public: - Session(Api *a); + Session(struct lws *w); + ~Session(); - virtual ~Session(); + std::string getName() const; - int runAction(json_t *req, json_t **resp); - virtual void runPendingActions(); - - virtual std::string getName(); - - int getRuns() - { - return runs; - } - - SuperNode * getSuperNode() + SuperNode * getSuperNode() const { return api->getSuperNode(); } - virtual void shutdown() - { } + void open(void *in, size_t len); + int writeable(); + void body(void *in, size_t len); + void bodyComplete(); + void execute(); + void shutdown(); + + static int protocolCallback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len); + + int getRequestMethod(struct lws *wsi); + + static std::string methodToString(int meth); + }; } /* namespace api */ diff --git a/include/villas/api/sessions/http.hpp b/include/villas/api/sessions/http.hpp deleted file mode 100644 index 0d7a41c6d..000000000 --- a/include/villas/api/sessions/http.hpp +++ /dev/null @@ -1,61 +0,0 @@ -/** HTTP Api session. - * - * @file - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#pragma once - -#include - -extern "C" int api_http_protocol_cb(lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len); - -namespace villas { -namespace node { - -/* Forward declarations */ -class SuperNode; -class Api; - -namespace api { -namespace sessions { - -class Http : public Wsi { - -protected: - bool headersSent; - -public: - Http(Api *s, lws *w); - virtual ~Http() { }; - - void read(void *in, size_t len); - - int complete(); - - int write(); - - virtual std::string getName(); -}; - -} /* namespace sessions */ -} /* namespace api */ -} /* namespace node */ -} /* namespace villas */ diff --git a/include/villas/api/sessions/websocket.hpp b/include/villas/api/sessions/websocket.hpp deleted file mode 100644 index 0bfe62a49..000000000 --- a/include/villas/api/sessions/websocket.hpp +++ /dev/null @@ -1,55 +0,0 @@ -/** WebSockets API session. - * - * @file - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#pragma once - -#include - -extern "C" int api_ws_protocol_cb(lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len); - -namespace villas { -namespace node { - -/* Forward declarations */ -class Api; - -namespace api { -namespace sessions { - -class WebSocket : public Wsi { - -public: - WebSocket(Api *a, lws *w); - virtual ~WebSocket() { }; - - virtual std::string getName(); - - int read(void *in, size_t len); - - int write(); -}; - -} /* namespace sessions */ -} /* namespace api */ -} /* namespace node */ -} /* namespace villas */ diff --git a/lib/api.cpp b/lib/api.cpp index 24748e686..0fc8cac9f 100644 --- a/lib/api.cpp +++ b/lib/api.cpp @@ -21,7 +21,9 @@ *********************************************************************************/ #include +#include #include +#include #include #include #include @@ -31,10 +33,17 @@ using namespace villas; using namespace villas::node; using namespace villas::node::api; +InvalidMethod::InvalidMethod(Request *req) : + BadRequest( + fmt::format("The '{}' API endpoint does not support {} requests", + req->factory->getName(), Session::methodToString(req->method) + ) + ) +{ } + Api::Api(SuperNode *sn) : state(State::INITIALIZED), - super_node(sn), - server(this) + super_node(sn) { logger = logging.get("api"); } @@ -50,8 +59,6 @@ void Api::start() logger->info("Starting sub-system"); - server.start(); - running = true; thread = std::thread(&Api::worker, this); @@ -76,37 +83,24 @@ void Api::stop() pending.push(nullptr); /* unblock thread */ thread.join(); - server.stop(); - state = State::STOPPED; } -void Api::run() -{ - if (pending.empty()) - return; - - /* Process pending actions */ - while (!pending.empty()) { - Session *s = pending.pop(); - if (s) { - /* Check that the session is still alive */ - auto it = std::find(sessions.begin(), sessions.end(), s); - if (it == sessions.end()) - return; - - s->runPendingActions(); - } - } -} - void Api::worker() { logger->info("Started worker"); while (running) { - run(); - server.run(); + /* Process pending requests */ + while (!pending.empty()) { + Session *s = pending.pop(); + if (s) { + /* Check that the session is still alive */ + auto it = std::find(sessions.begin(), sessions.end(), s); + if (it != sessions.end()) + s->execute(); + } + } } logger->info("Stopped worker"); diff --git a/lib/api/CMakeLists.txt b/lib/api/CMakeLists.txt index dcc6a29df..8d51675f1 100644 --- a/lib/api/CMakeLists.txt +++ b/lib/api/CMakeLists.txt @@ -23,30 +23,21 @@ set(API_SRC session.cpp - server.cpp + request.cpp + response.cpp - sessions/socket.cpp - - actions/capabiltities.cpp - actions/shutdown.cpp - actions/status.cpp - actions/config.cpp - actions/nodes.cpp - actions/paths.cpp - actions/restart.cpp - actions/node.cpp - actions/stats.cpp + requests/capabiltities.cpp + requests/shutdown.cpp + requests/status.cpp + requests/config.cpp + requests/nodes.cpp + requests/paths.cpp + requests/restart.cpp + requests/node_action.cpp + requests/file.cpp + requests/stats_reset.cpp ) -if(WITH_WEB) - list(APPEND API_SRC - sessions/wsi.cpp - sessions/http.cpp - sessions/websocket.cpp - ) -endif() - add_library(api STATIC ${API_SRC}) target_include_directories(api PUBLIC ${INCLUDE_DIRS}) target_link_libraries(api PUBLIC ${LIBRARIES}) - diff --git a/include/villas/api/sessions/wsi.hpp b/lib/api/request.cpp similarity index 62% rename from include/villas/api/sessions/wsi.hpp rename to lib/api/request.cpp index 9d19e80a2..f673ee8e6 100644 --- a/include/villas/api/sessions/wsi.hpp +++ b/lib/api/request.cpp @@ -1,4 +1,4 @@ -/** LWS Api session. +/** API Request. * * @file * @author Steffen Vogel @@ -21,43 +21,29 @@ * along with this program. If not, see . *********************************************************************************/ -#pragma once +#include +#include +#include -#include +using namespace villas; +using namespace villas::node::api; -#include -#include +Request * RequestFactory::make(Session *s, const std::string &uri, int meth) +{ + for (auto *rf : plugin::Registry::lookup()) { + std::smatch mr; + if (not rf->match(uri, mr)) + continue; -/* Forward declarations */ -struct lws; + auto *r = rf->make(s); -namespace villas { -namespace node { + r->uri = uri; + r->method = meth; + r->matches = mr; + r->factory = rf; -/* Forward declarations */ -class Api; + return r; + } -namespace api { -namespace sessions { - -class Wsi : public Session { - -protected: - lws *wsi; - - Web *web; - -public: - Wsi(Api *a, lws *w); - - virtual void runPendingActions(); - - virtual std::string getName(); - - virtual void shutdown(); -}; - -} /* namespace sessions */ -} /* namespace api */ -} /* namespace node */ -} /* namespace villas */ + throw BadRequest("Unknown API request"); +} diff --git a/lib/api/actions/capabiltities.cpp b/lib/api/requests/capabiltities.cpp similarity index 77% rename from lib/api/actions/capabiltities.cpp rename to lib/api/requests/capabiltities.cpp index 77ffdfd87..d7945445e 100644 --- a/lib/api/actions/capabiltities.cpp +++ b/lib/api/requests/capabiltities.cpp @@ -21,19 +21,20 @@ *********************************************************************************/ #include -#include #include +#include +#include namespace villas { namespace node { namespace api { -class CapabilitiesAction : public Action { +class CapabilitiesRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { json_t *json_hooks = json_array(); json_t *json_apis = json_array(); @@ -41,7 +42,13 @@ public: json_t *json_formats = json_array(); json_t *json_name; - for (auto f : plugin::Registry::lookup()) { + if (method != Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Capabilities endpoint does not accept any body data"); + + for (auto f : plugin::Registry::lookup()) { json_name = json_string(f->getName().c_str()); json_array_append_new(json_apis, json_name); @@ -67,21 +74,22 @@ public: } #endif - *resp = json_pack("{ s: s, s: o, s: o, s: o, s: o }", + auto *json_capabilities = json_pack("{ s: s, s: o, s: o, s: o, s: o }", "build", PROJECT_BUILD_ID, "hooks", json_hooks, "node-types", json_nodes, "apis", json_apis, "formats", json_formats); - return 0; + return new Response(session, json_capabilities); } }; -/* Register action */ +/* Register API request */ static char n[] = "capabilities"; +static char r[] = "/capabilities"; static char d[] = "get capabiltities and details about this VILLASnode instance"; -static ActionPlugin p; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/actions/config.cpp b/lib/api/requests/config.cpp similarity index 73% rename from lib/api/actions/config.cpp rename to lib/api/requests/config.cpp index 3c2af57da..9ca921655 100644 --- a/lib/api/actions/config.cpp +++ b/lib/api/requests/config.cpp @@ -20,35 +20,43 @@ * along with this program. If not, see . *********************************************************************************/ -#include -#include #include +#include +#include +#include namespace villas { namespace node { namespace api { -class ConfigAction : public Action { +class ConfigRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { json_t *cfg = session->getSuperNode()->getConfig(); - *resp = cfg + if (method != Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Config endpoint does not accept any body data"); + + auto *json_config = cfg ? json_incref(cfg) : json_object(); - return 0; + return new Response(session, json_config); } }; -/* Register action */ +/* Register API request */ static char n[] = "config"; +static char r[] = "/config"; static char d[] = "get configuration of this VILLASnode instance"; -static ActionPlugin p; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/requests/file.cpp b/lib/api/requests/file.cpp new file mode 100644 index 000000000..5fddd0926 --- /dev/null +++ b/lib/api/requests/file.cpp @@ -0,0 +1,77 @@ +/** The "file" API ressource. + * + * @author Steffen Vogel + * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program 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, either version 3 of the License, or + * any later version. + * + * 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 . + *********************************************************************************/ + +#include +#include +#include +#include + +#include +#include + +namespace villas { +namespace node { +namespace api { + +class FileRequest : public Request { + +public: + using Request::Request; + + virtual Response * execute() + { + if (method != Method::GET && method != Method::POST) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("File endpoint does not accept any body data"); + + const auto &nodeName = matches[1].str(); + + struct vlist *nodes = session->getSuperNode()->getNodes(); + struct node *n = (struct node *) vlist_lookup(nodes, nodeName.c_str()); + if (!n) + throw BadRequest("Invalid node"); + + struct node_type *vt = node_type_lookup("file"); + + if (n->_vt != vt) + throw BadRequest("This node is not a file node"); + + struct file *f = (struct file *) n->_vd; + + if (matches[1].str() == "rewind") + io_rewind(&f->io); + + return new Response(session); + } +}; + +/* Register API request */ +static char n[] = "file"; +static char r[] = "/node/([^/]+)/file(?:/([^/]+))?"; +static char d[] = "control instances of 'file' node-type"; +static RequestPlugin p; + +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/api/actions/node.cpp b/lib/api/requests/node_action.cpp similarity index 56% rename from lib/api/actions/node.cpp rename to lib/api/requests/node_action.cpp index 9bdbec3dd..77c18fbb5 100644 --- a/lib/api/actions/node.cpp +++ b/lib/api/requests/node_action.cpp @@ -26,63 +26,69 @@ #include #include #include -#include -#include #include +#include +#include +#include namespace villas { namespace node { namespace api { template -class NodeAction : public Action { +class NodeActionRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { - int ret; - json_error_t err; - const char *node_str; + if (method != Method::POST) + throw InvalidMethod(this); - ret = json_unpack_ex(args, &err, 0, "{ s: s }", - "node", &node_str - ); - if (ret < 0) - return ret; + if (body != nullptr) + throw BadRequest("Node endpoints do not accept any body data"); + + const auto &nodeName = matches[1].str(); struct vlist *nodes = session->getSuperNode()->getNodes(); - struct node *n = (struct node *) vlist_lookup(nodes, node_str); + struct node *n = (struct node *) vlist_lookup(nodes, nodeName.c_str()); if (!n) - return -1; + throw Error(HTTP_STATUS_NOT_FOUND, "Node not found"); - return A(n); + A(n); + + return new Response(session); } }; -/* Register actions */ -char n1[] = "node.start"; +/* Register API requests */ +char n1[] = "node/start"; +char r1[] = "/node/([^/]+)/start"; char d1[] = "start a node"; -static ActionPlugin, n1, d1> p1; +static RequestPlugin, n1, r1, d1> p1; -char n2[] = "node.stop"; +char n2[] = "node/stop"; +char r2[] = "/node/([^/]+)/stop"; char d2[] = "stop a node"; -static ActionPlugin, n2, d2> p2; +static RequestPlugin, n2, r2, d2> p2; -char n3[] = "node.pause"; +char n3[] = "node/pause"; +char r3[] = "/node/([^/]+)/pause"; char d3[] = "pause a node"; -static ActionPlugin, n3, d3> p3; +static RequestPlugin, n3, r3, d3> p3; -char n4[] = "node.resume"; +char n4[] = "node/resume"; +char r4[] = "/node/([^/]+)/resume"; char d4[] = "resume a node"; -static ActionPlugin, n4, d4> p4; +static RequestPlugin, n4, r4, d4> p4; -char n5[] = "node.restart"; +char n5[] = "node/restart"; +char r5[] = "/node/([^/]+)/restart"; char d5[] = "restart a node"; -static ActionPlugin, n5, d5> p5; +static RequestPlugin, n5, r5, d5> p5; } /* namespace api */ diff --git a/lib/api/actions/nodes.cpp b/lib/api/requests/nodes.cpp similarity index 84% rename from lib/api/actions/nodes.cpp rename to lib/api/requests/nodes.cpp index 5ebc1d050..88eb4fdd5 100644 --- a/lib/api/actions/nodes.cpp +++ b/lib/api/requests/nodes.cpp @@ -27,20 +27,27 @@ #include #include #include -#include #include +#include +#include namespace villas { namespace node { namespace api { -class NodesAction : public Action { +class NodesRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { + if (method != Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Nodes endpoint does not accept any body data"); + json_t *json_nodes = json_array(); struct vlist *nodes = session->getSuperNode()->getNodes(); @@ -86,16 +93,15 @@ public: json_array_append_new(json_nodes, json_node); } - *resp = json_nodes; - - return 0; + return new Response(session, json_nodes); } }; -/* Register action */ +/* Register API request */ static char n[] = "nodes"; +static char r[] = "/nodes"; static char d[] = "retrieve list of all known nodes"; -static ActionPlugin p; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/requests/path_action.cpp b/lib/api/requests/path_action.cpp new file mode 100644 index 000000000..41aa06b01 --- /dev/null +++ b/lib/api/requests/path_action.cpp @@ -0,0 +1,88 @@ +/** The API ressource for start/stop/pause/resume paths. + * + * @author Steffen Vogel + * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program 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, either version 3 of the License, or + * any later version. + * + * 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 . + *********************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace villas { +namespace node { +namespace api { + +template +class PathActionRequest : public Request { + +public: + using Request::Request; + + virtual Response * execute() + { + if (method != Method::POST) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Path endpoints do not accept any body data"); + + unsigned long pathIndex; + const auto &pathIndexStr = matches[1].str(); + + try { + pathIndex = std::atoul(pathIndexStr); + } catch (const std::invalid_argument &e) { + throw BadRequest("Invalid argument"); + } + + struct vlist *paths = session->getSuperNode()->getPaths(); + struct vpath *p = (struct path *) vlist_at_safe(pathIndex); + + if (!p) + throw Error(HTTP_STATUS_NOT_FOUND, "Node not found"); + + A(p); + + return new Response(session); + } + +}; + +/* Register API requests */ +char n1[] = "path/start"; +char r1[] = "/path/([^/]+)/start"; +char d1[] = "start a path"; +static RequestPlugin, n1, r1, d1> p1; + +char n2[] = "path/stop"; +char r2[] = "/path/([^/]+)/stop"; +char d2[] = "stop a path"; +static RequestPlugin, n2, r2, d2> p2; + + +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/api/requests/paths.cpp b/lib/api/requests/paths.cpp new file mode 100644 index 000000000..4364755c1 --- /dev/null +++ b/lib/api/requests/paths.cpp @@ -0,0 +1,123 @@ +/** The "paths" API ressource. + * + * @author Steffen Vogel + * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program 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, either version 3 of the License, or + * any later version. + * + * 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 . + *********************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace villas { +namespace node { +namespace api { + +class PathsRequest : public Request { + +public: + using Request::Request; + + virtual Response * execute() + { + if (method != Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Paths endpoint does not accept any body data"); + + json_t *json_paths = json_array(); + + struct vlist *paths = session->getSuperNode()->getPaths(); + + for (size_t i = 0; i < vlist_length(paths); i++) { + struct vpath *p = (struct vpath *) vlist_at(paths, i); + + char uuid[37]; + uuid_unparse(p->uuid, uuid); + + json_t *json_signals = json_array(); + json_t *json_hooks = json_array(); + json_t *json_sources = json_array(); + json_t *json_destinations = json_array(); + + for (size_t i = 0; i < vlist_length(&p->signals); i++) { + struct signal *sig = (struct signal *) vlist_at_safe(&p->signals, i); + + json_array_append(json_signals, signal_to_json(sig)); + } + + for (size_t i = 0; i < vlist_length(&p->hooks); i++) { + Hook *h = (Hook *) vlist_at_safe(&p->hooks, i); + + json_array_append(json_hooks, h->getConfig()); + } + + for (size_t i = 0; i < vlist_length(&p->sources); i++) { + struct vpath_source *pd = (struct vpath_source *) vlist_at_safe(&p->sources, i); + + json_array_append_new(json_sources, json_string(node_name_short(pd->node))); + } + + for (size_t i = 0; i < vlist_length(&p->destinations); i++) { + struct vpath_destination *pd = (struct vpath_destination *) vlist_at_safe(&p->destinations, i); + + json_array_append_new(json_destinations, json_string(node_name_short(pd->node))); + } + + json_t *json_path = json_pack("{ s: s, s: s, s: s, s: b, s: b s: b, s: b, s: b, s: b s: i, s: o, s: o, s: o, s: o }", + "uuid", uuid, + "state", state_print(p->state), + "mode", p->mode == PathMode::ANY ? "any" : "all", + "enabled", p->enabled, + "builtin", p->builtin, + "reverse", p->reverse, + "original_sequence_no", p->original_sequence_no, + "last_sequence", p->last_sequence, + "poll", p->poll, + "queuelen", p->queuelen, + "signals", json_signals, + "hooks", json_hooks, + "in", json_sources, + "out", json_destinations + ); + + json_array_append_new(json_paths, json_path); + } + + return new Response(session, json_paths); + } +}; + +/* Register API request */ +static char n[] = "paths"; +static char r[] = "/paths"; +static char d[] = "retrieve list of all paths with details"; +static RequestPlugin p; + +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/api/actions/restart.cpp b/lib/api/requests/restart.cpp similarity index 79% rename from lib/api/actions/restart.cpp rename to lib/api/requests/restart.cpp index f6ec0296d..3cd55afa0 100644 --- a/lib/api/actions/restart.cpp +++ b/lib/api/requests/restart.cpp @@ -1,4 +1,4 @@ -/** The "restart" API action. +/** The "restart" API request. * * @author Steffen Vogel * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC @@ -21,17 +21,18 @@ *********************************************************************************/ #include -#include -#include #include #include #include +#include +#include +#include namespace villas { namespace node { namespace api { -class RestartAction : public Action { +class RestartRequest : public Request { protected: static std::string configUri; @@ -55,19 +56,22 @@ protected: } public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { int ret; json_error_t err; + if (method != Method::POST) + throw InvalidMethod(this); + const char *cfg = nullptr; - if (args) { - ret = json_unpack_ex(args, &err, 0, "{ s?: s }", "config", &cfg); + if (body) { + ret = json_unpack_ex(body, &err, 0, "{ s?: s }", "config", &cfg); if (ret < 0) - return ret; + throw Error(); } /* If no config is provided via request, we will use the previous one */ @@ -86,7 +90,7 @@ public: /* We pass some env variables to the new process */ setenv("VILLAS_API_RESTART_COUNT", buf, 1); - *resp = json_pack("{ s: i, s: o }", + auto *json_response = json_pack("{ s: i, s: o }", "restarts", cnt, "config", configUri.empty() ? json_null() @@ -96,21 +100,22 @@ public: /* Register exit handler */ ret = atexit(handler); if (ret) - return ret; + throw Error(HTTP_STATUS_INTERNAL_SERVER_ERROR, "Failed to restart VILLASnode instance"); /* Properly terminate current instance */ utils::killme(SIGTERM); - return 0; + return new Response(session, json_response); } }; -std::string RestartAction::configUri; +std::string RestartRequest::configUri; -/* Register action */ +/* Register API request */ static char n[] = "restart"; +static char r[] = "/restart"; static char d[] = "restart VILLASnode with new configuration"; -static ActionPlugin p; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/actions/shutdown.cpp b/lib/api/requests/shutdown.cpp similarity index 71% rename from lib/api/actions/shutdown.cpp rename to lib/api/requests/shutdown.cpp index 73bd2e672..560756b42 100644 --- a/lib/api/actions/shutdown.cpp +++ b/lib/api/requests/shutdown.cpp @@ -1,4 +1,4 @@ -/** The "shutdown" API action. +/** The "shutdown" API request. * * @author Steffen Vogel * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC @@ -23,29 +23,37 @@ #include #include -#include +#include +#include namespace villas { namespace node { namespace api { -class ShutdownAction : public Action { +class ShutdownRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { + if (method != Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Shutdown endpoint does not accept any body data"); + utils::killme(SIGTERM); - return 0; + return new Response(session); } }; -/* Register action */ +/* Register API request */ static char n[] = "shutdown"; +static char r[] = "/shutdown"; static char d[] = "quit VILLASnode"; -static ActionPlugin p; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/actions/paths.cpp b/lib/api/requests/stats.cpp similarity index 52% rename from lib/api/actions/paths.cpp rename to lib/api/requests/stats.cpp index 703ade636..d2c8e08d5 100644 --- a/lib/api/actions/paths.cpp +++ b/lib/api/requests/stats.cpp @@ -1,4 +1,4 @@ -/** The "paths" API ressource. +/** The API ressource for querying statistics. * * @author Steffen Vogel * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC @@ -22,51 +22,58 @@ #include +#include +#include +#include #include -#include #include -#include +#include #include +#include +#include namespace villas { namespace node { namespace api { -class PathsAction : public Action { +class StatsRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { - json_t *json_paths = json_array(); + int ret; + json_error_t err; - struct vlist *paths = session->getSuperNode()->getPaths(); + if (method != Method::GET) + throw InvalidMethod(this); - for (size_t i = 0; i < vlist_length(paths); i++) { - struct vpath *p = (struct vpath *) vlist_at(paths, i); + if (body != nullptr) + throw BadRequest("Stats endpoint does not accept any body data"); - json_t *json_path = json_pack("{ s: s }", - "state", state_print(p->state) - ); + json_t *json_stats = json_array(); - /* Add all additional fields of node here. - * This can be used for metadata */ - json_object_update(json_path, p->cfg); + struct vlist *nodes = session->getSuperNode()->getNodes(); + for (size_t i = 0; i < vlist_length(nodes); i++) { + struct node *n = (struct node *) vlist_at(nodes, i); - json_array_append_new(json_paths, json_path); + if (node && strcmp(node, node_name(n))) + continue; + + if (n->stats) + json_array_append(json_stats, n->stats->toJson()); } - *resp = json_paths; - - return 0; + return new Response(session, json_stats); } }; -/* Register action */ -static char n[] = "paths"; -static char d[] = "retrieve list of all paths with details"; -static ActionPlugin p; +/* Register API requests */ +static char n[] = "stats"; +static char r[] = "/node/([^/]+)/stats"; +static char d[] = "get internal statistics counters"; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/actions/stats.cpp b/lib/api/requests/stats_reset.cpp similarity index 68% rename from lib/api/actions/stats.cpp rename to lib/api/requests/stats_reset.cpp index bb80a03a6..b73b4fbfd 100644 --- a/lib/api/actions/stats.cpp +++ b/lib/api/requests/stats_reset.cpp @@ -1,4 +1,4 @@ -/** The API ressource for getting and resetting statistics. +/** The API ressource for resetting statistics. * * @author Steffen Vogel * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC @@ -27,54 +27,56 @@ #include #include #include -#include -#include #include +#include +#include +#include namespace villas { namespace node { namespace api { -class StatsAction : public Action { +class StatsRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { - int ret, reset = 0; + int ret; json_error_t err; - ret = json_unpack_ex(args, &err, 0, "{ s?: b }", - "reset", &reset + char *node = nullptr; + + ret = json_unpack_ex(body, &err, 0, "{ s?: s }", + "node", &node ); if (ret < 0) - return ret; + throw Error(); struct vlist *nodes = session->getSuperNode()->getNodes(); for (size_t i = 0; i < vlist_length(nodes); i++) { struct node *n = (struct node *) vlist_at(nodes, i); + if (node && strcmp(node, node_name(n))) + continue; + if (n->stats) { - if (reset) { - n->stats->reset(); - info("Stats resetted for node %s", node_name(n)); - } + n->stats->reset(); + info("Stats resetted for node %s", node_name(n)); } } - *resp = json_object(); - - return 0; + return new Response(session); } - }; -/* Register actions */ -static char n[] = "stats"; -static char d[] = "get or reset internal statistics counters"; -static ActionPlugin p; +/* Register API requests */ +static char n[] = "stats/reset"; +static char r[] = "/stats/reset"; +static char d[] = "reset internal statistics counters"; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/lib/api/actions/status.cpp b/lib/api/requests/status.cpp similarity index 71% rename from lib/api/actions/status.cpp rename to lib/api/requests/status.cpp index 7270d097c..97ec92c16 100644 --- a/lib/api/actions/status.cpp +++ b/lib/api/requests/status.cpp @@ -1,4 +1,4 @@ -/** The "stats" API action. +/** The "stats" API request. * * @author Steffen Vogel * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC @@ -24,37 +24,45 @@ #include -#include +#include +#include namespace villas { namespace node { namespace api { -class StatusAction : public Action { +class StatusRequest : public Request { public: - using Action::Action; + using Request::Request; - virtual int execute(json_t *args, json_t **resp) + virtual Response * execute() { int ret; struct lws_context *ctx = lws_get_context(s->wsi); char buf[4096]; + if (method != Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("Status endpoint does not accept any body data"); + ret = lws_json_dump_context(ctx, buf, sizeof(buf), 0); if (ret) - return ret; + throw Error(); - *resp = json_loads(buf, 0, nullptr); + auto *json_status = json_loads(buf, 0, nullptr); - return 0; + return new Response(session, json_status); } }; -/* Register action */ +/* Register API request */ static char n[] = "status"; +static char r[] = "/status"; static char d[] = "get status and statistics of web server"; -static ActionPlugin p; +static RequestPlugin p; } /* namespace api */ } /* namespace node */ diff --git a/include/villas/api/sessions/socket.hpp b/lib/api/response.cpp similarity index 66% rename from include/villas/api/sessions/socket.hpp rename to lib/api/response.cpp index eacda603a..fc86c7a97 100644 --- a/include/villas/api/sessions/socket.hpp +++ b/lib/api/response.cpp @@ -1,4 +1,4 @@ -/** Socket Api session. +/** API Response. * * @file * @author Steffen Vogel @@ -21,37 +21,28 @@ * along with this program. If not, see . *********************************************************************************/ -#pragma once +#include +#include -#include +using namespace villas::node::api; -namespace villas { -namespace node { +Response::Response(Session *s, json_t *resp) : + session(s), + response(resp), + code(HTTP_STATUS_OK) +{ + logger = logging.get("api:response"); +} -/* Forward declarations */ -class Api; +Response::~Response() +{ + if (response) + json_decref(response); +} -namespace api { -namespace sessions { - -class Socket : public Session { - -protected: - int sd; - -public: - Socket(Api *a, int s); - ~Socket(); - - int read(); - int write(); - - virtual std::string getName(); - - int getSd() const { return sd; } -}; - -} /* namespace sessions */ -} /* namespace api */ -} /* namespace node */ -} /* namespace villas */ +json_t * ErrorResponse::toJson() +{ + return json_pack("{ s: s }", + "error", error.c_str() + ); +} diff --git a/lib/api/server.cpp b/lib/api/server.cpp deleted file mode 100644 index 1cb8b4d46..000000000 --- a/lib/api/server.cpp +++ /dev/null @@ -1,211 +0,0 @@ -/** Socket API endpoint. - * - * @file - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#include -#include - -#include -#include - -#if __GNUC__ <= 7 && !defined(__clang__) - #include -#else - #include -#endif - -#include -#include -#include -#include -#include -#include - -using namespace villas; -using namespace villas::node::api; - -#if __GNUC__ <= 7 && !defined(__clang__) - namespace fs = std::experimental::filesystem; -#else - namespace fs = std::filesystem; -#endif - -Server::Server(Api *a) : - state(State::INITIALIZED), - api(a) -{ - logger = logging.get("api:server"); -} - -Server::~Server() -{ - assert(state != State::STARTED); -} - -void Server::start() -{ - int ret; - - assert(state != State::STARTED); - - sd = socket(AF_UNIX, SOCK_STREAM, 0); - if (sd < 0) - throw SystemError("Failed to create Api socket"); - - pollfd pfd = { - .fd = sd, - .events = POLLIN - }; - - pfds.push_back(pfd); - - struct sockaddr_un sun = getSocketAddress(); - - ret = bind(sd, (struct sockaddr *) &sun, sizeof(struct sockaddr_un)); - if (ret) - throw SystemError("Failed to bind API socket"); - - ret = listen(sd, 5); - if (ret) - throw SystemError("Failed to listen on API socket"); - - logger->info("Listening on UNIX socket: {}", sun.sun_path); - - state = State::STARTED; -} - -struct sockaddr_un Server::getSocketAddress() -{ - struct sockaddr_un sun = { .sun_family = AF_UNIX }; - - fs::path socketPath; - - if (getuid() == 0) { - socketPath = PREFIX "/var/lib/villas"; - } - else { - std::string homeDir = getenv("HOME"); - - socketPath = homeDir + "/.villas"; - } - - if (!fs::exists(socketPath)) { - logging.get("api")->info("Creating directory for API socket: {}", socketPath); - fs::create_directories(socketPath); - } - - socketPath += "/node-" + api->getSuperNode()->getName() + ".sock"; - - if (fs::exists(socketPath)) { - logging.get("api")->info("Removing existing socket: {}", socketPath); - fs::remove(socketPath); - } - - strncpy(sun.sun_path, socketPath.c_str(), sizeof(sun.sun_path) - 1); - - return sun; -} - -void Server::stop() -{ - int ret; - - assert(state == State::STARTED); - - ret = close(sd); - if (ret) - throw SystemError("Failed to close API socket"); - - state = State::STOPPED; -} - -void Server::run(int timeout) -{ - int ret; - - assert(state == State::STARTED); - - auto len = pfds.size(); - - ret = poll(pfds.data(), len, timeout); - if (ret < 0) - throw SystemError("Failed to poll on API socket"); - - std::vector closing; - - for (unsigned i = 1; i < len; i++) { - auto &pfd = pfds[i]; - - /* pfds[0] is the server socket */ - auto s = sessions[pfd.fd]; - - if (pfd.revents & POLLIN) { - ret = s->read(); - if (ret < 0) - closing.push_back(s); - } - - if (pfd.revents & POLLOUT) { - s->write(); - } - } - - /* Destroy closed sessions */ - for (auto *s : closing) - closeSession(s); - - /* Accept new connections */ - if (pfds[0].revents & POLLIN) - acceptNewSession(); -} - -void Server::acceptNewSession() { - int fd = ::accept(sd, nullptr, nullptr); - - auto s = new sessions::Socket(api, fd); - if (!s) - throw MemoryAllocationError(); - - pollfd pfd = { - .fd = fd, - .events = POLLIN | POLLOUT - }; - - pfds.push_back(pfd); - sessions[fd] = s; - - api->sessions.push_back(s); -} - -void Server::closeSession(sessions::Socket *s) -{ - int sd = s->getSd(); - - sessions.erase(sd); - api->sessions.remove(s); - - pfds.erase(std::remove_if(pfds.begin(), pfds.end(), - [sd](const pollfd &p){ return p.fd == sd; }) - ); - - delete s; -} diff --git a/lib/api/session.cpp b/lib/api/session.cpp index fa1b3be66..7afb4e021 100644 --- a/lib/api/session.cpp +++ b/lib/api/session.cpp @@ -29,109 +29,334 @@ #include #include -#include +#include +#include using namespace villas; +using namespace villas::node; using namespace villas::node::api; -Session::Session(Api *a) : - runs(0), - api(a) +Session::Session(lws *w) : + wsi(w) { logger = logging.get("api:session"); + lws_context *ctx = lws_get_context(wsi); + void *user_ctx = lws_context_user(ctx); + + web = static_cast(user_ctx); + api = web->getApi(); + + if (!api) + throw RuntimeError("API is disabled"); + + api->sessions.push_back(this); + logger->debug("Initiated API session: {}", getName()); + + state = Session::State::ESTABLISHED; } Session::~Session() { + api->sessions.remove(this); + + if (request) + delete request; + + if (response) + delete response; + logger->debug("Destroyed API session: {}", getName()); } - -void Session::runPendingActions() +void Session::execute() { - json_t *req, *resp; + Request *req = request.exchange(nullptr); - while (!request.queue.empty()) { - req = request.queue.pop(); + logger->debug("Running API request: uri={}, method={}", req->uri, req->method); - runAction(req, &resp); + try { + response = req->execute(); - json_decref(req); + logger->debug("Completed API request: request={}", req->uri, req->method); + } catch (const Error &e) { + response = new ErrorResponse(this, e); - response.queue.push(resp); + logger->warn("API request failed: uri={}, method={}, code={}: {}", req->uri, req->method, e.code, e.what()); + } catch (const RuntimeError &e) { + response = new ErrorResponse(this, e); + + logger->warn("API request failed: uri={}, method={}: {}", req->uri, req->method, e.what()); + } + + logger->debug("Ran pending API requests. Triggering on_writeable callback: wsi={}", (void *) wsi); + + web->callbackOnWritable(wsi); +} + +std::string Session::getName() const +{ + std::stringstream ss; + + ss << "version=" << version; + + if (wsi) { + char name[128]; + char ip[128]; + + lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), name, sizeof(name), ip, sizeof(ip)); + + ss << ", remote.name=" << name << ", remote.ip=" << ip; + } + + return ss.str(); +} + +void Session::shutdown() +{ + state = State::SHUTDOWN; + + web->callbackOnWritable(wsi); +} + +void Session::open(void *in, size_t len) +{ + int ret; + char buf[32]; + unsigned long contentLength; + + int meth; + std::string uri = reinterpret_cast(in); + + try { + meth = getRequestMethod(wsi); + if (meth == Request::Method::UNKNOWN_METHOD) + throw RuntimeError("Invalid request method"); + + request = RequestFactory::make(this, uri, meth); + + ret = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_HTTP_CONTENT_LENGTH); + if (ret < 0) + throw RuntimeError("Failed to get content length"); + + try { + contentLength = std::stoull(buf); + } catch (const std::invalid_argument &) { + contentLength = 0; + } + } catch (const Error &e) { + response = new ErrorResponse(this, e); + lws_callback_on_writable(wsi); + } catch (const RuntimeError &e) { + response = new ErrorResponse(this, e); + lws_callback_on_writable(wsi); + } + + /* This is an OPTIONS request. + * + * We immediatly send headers and close the connection + * without waiting for a POST body */ + if (meth == Request::Method::OPTIONS) + lws_callback_on_writable(wsi); + /* This request has no body. + * We can reply immediatly */ + else if (contentLength == 0) + api->pending.push(this); + else { + /* This request has a HTTP body. We wait for more data to arrive */ } } -int Session::runAction(json_t *json_in, json_t **json_out) +void Session::body(void *in, size_t len) +{ + requestBuffer.append((const char *) in, len); +} + +void Session::bodyComplete() +{ + try { + auto *j = requestBuffer.decode(); + if (!j) + throw BadRequest("Failed to decode request payload"); + + requestBuffer.clear(); + + (*request).setBody(j); + + api->pending.push(this); + } catch (const Error &e) { + response = new ErrorResponse(this, e); + + logger->warn("Failed to decode API request: {}", e.what()); + } +} + +int Session::writeable() { int ret; - const char *action; - char *id; - json_error_t err; - json_t *json_args = nullptr; - json_t *json_resp = nullptr; + if (!headersSent) { + int code = HTTP_STATUS_OK; + const char *contentType = "application/json"; - ret = json_unpack_ex(json_in, &err, 0, "{ s: s, s: s, s?: o }", - "action", &action, - "id", &id, - "request", &json_args); - if (ret) { - ret = -100; - *json_out = json_pack("{ s: s, s: i }", - "error", "invalid request", - "code", ret); + uint8_t headers[2048], *p = headers, *end = &headers[sizeof(headers) - 1]; + + responseBuffer.clear(); + if (response) { + Response *resp = response; + + json_t *json_response = resp->toJson(); + + responseBuffer.encode(json_response); + + code = resp->getCode(); + } + + if (lws_add_http_common_headers(wsi, code, contentType, + responseBuffer.size(), /* no content len */ + &p, end)) + return 1; + + std::map constantHeaders = { + { "Server:", USER_AGENT }, + { "Access-Control-Allow-Origin:", "*" }, + { "Access-Control-Allow-Methods:", "GET, POST, OPTIONS" }, + { "Access-Control-Allow-Headers:", "Content-Type" }, + { "Access-Control-Max-Age:", "86400" } + }; + + for (auto &hdr : constantHeaders) { + if (lws_add_http_header_by_name (wsi, + reinterpret_cast(hdr.first), + reinterpret_cast(hdr.second), + strlen(hdr.second), &p, end)) + return -1; + } + + if (lws_finalize_write_http_header(wsi, headers, &p, end)) + return 1; + + /* No wait, until we can send the body */ + headersSent = true; + + /* Do we have a body to send */ + if (responseBuffer.size() > 0) + lws_callback_on_writable(wsi); - logger->debug("Completed API request: action={}, id={}, code={}", action, id, ret); return 0; } + else { + ret = lws_write(wsi, (unsigned char *) responseBuffer.data(), responseBuffer.size(), LWS_WRITE_HTTP_FINAL); + if (ret < 0) + return -1; - auto acf = plugin::Registry::lookup(action); - if (!acf) { - ret = -101; - *json_out = json_pack("{ s: s, s: s, s: i, s: s }", - "action", action, - "id", id, - "code", ret, - "error", "action not found"); - - logger->debug("Completed API request: action={}, id={}, code={}", action, id, ret); - return 0; + return 1; } +} - logger->debug("Running API request: action={}, id={}", action, id); +int Session::protocolCallback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) +{ + int ret; + Session *s = reinterpret_cast(user); - Action *act = acf->make(this); + switch (reason) { + case LWS_CALLBACK_HTTP_BIND_PROTOCOL: + try { + new (s) Session(wsi); + } catch (const RuntimeError &e) { + return -1; + } - ret = act->execute(json_args, &json_resp); - if (ret) - *json_out = json_pack("{ s: s, s: s, s: i, s: s }", - "action", action, - "id", id, - "code", ret, - "error", "action failed"); - else - *json_out = json_pack("{ s: s, s: s }", - "action", action, - "id", id); + break; - if (json_resp) - json_object_set_new(*json_out, "response", json_resp); + case LWS_CALLBACK_HTTP_DROP_PROTOCOL: + if (s == nullptr) + return -1; - logger->debug("Completed API request: action={}, id={}, code={}", action, id, ret); + s->~Session(); - runs++; + break; + + case LWS_CALLBACK_HTTP: + s->open(in, len); + + break; + + case LWS_CALLBACK_HTTP_BODY: + s->body(in, len); + + break; + + case LWS_CALLBACK_HTTP_BODY_COMPLETION: + s->bodyComplete(); + + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: + ret = s->writeable(); + + /* + * HTTP/1.0 no keepalive: close network connection + * HTTP/1.1 or HTTP1.0 + KA: wait / process next transaction + * HTTP/2: stream ended, parent connection remains up + */ + if (ret) { + if (lws_http_transaction_completed(wsi)) + return -1; + } + else + lws_callback_on_writable(wsi); + + break; + + default: + break; + } return 0; } -std::string Session::getName() +int Session::getRequestMethod(struct lws *wsi) { - std::stringstream ss; - - ss << "version=" << version << ", runs=" << runs; - - return ss.str(); + if (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) + return Request::Method::GET; + else if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI)) + return Request::Method::POST; +#if defined(LWS_WITH_HTTP_UNCOMMON_HEADERS) || defined(LWS_HTTP_HEADERS_ALL) + else if (lws_hdr_total_length(wsi, WSI_TOKEN_PUT_URI)) + return Request::Method::PUT; + else if (lws_hdr_total_length(wsi, WSI_TOKEN_PATCH_URI)) + return Request::Method::PATCH; + else if (lws_hdr_total_length(wsi, WSI_TOKEN_OPTIONS_URI)) + return Request::Method::OPTIONS; +#endif + else + return Request::Method::UNKNOWN_METHOD; +} + +std::string Session::methodToString(int method) +{ + switch (method) { + case Request::Method::POST: + return "POST"; + + case Request::Method::GET: + return "GET"; + + case Request::Method::DELETE: + return "DELETE"; + + case Request::Method::PUT: + return "PUT"; + + case Request::Method::PATCH: + return "GPATCHET"; + + case Request::Method::OPTIONS: + return "OPTIONS"; + + default: + return "UNKNOWN"; + } } diff --git a/lib/api/sessions/http.cpp b/lib/api/sessions/http.cpp deleted file mode 100644 index f61bb9c92..000000000 --- a/lib/api/sessions/http.cpp +++ /dev/null @@ -1,212 +0,0 @@ -/** HTTP Api session. - * - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#include - -#include - -#include -#include -#include -#include -#include - -using namespace villas; -using namespace villas::node; -using namespace villas::node::api::sessions; - -Http::Http(Api *a, lws *w) : - Wsi(a, w), - headersSent(false) -{ - int ret, hdrlen, options = -1, version; - char *uri; - - if ((hdrlen = lws_hdr_total_length(wsi, WSI_TOKEN_OPTIONS_URI))) - options = 1; - else if ((hdrlen = lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI))) - options = 0; - else - throw RuntimeError("Invalid request"); - - uri = new char[hdrlen + 1]; - if (!uri) - throw MemoryAllocationError(); - - lws_hdr_copy(wsi, uri, hdrlen + 1, options ? WSI_TOKEN_OPTIONS_URI : WSI_TOKEN_POST_URI); - - /* Parse request URI */ - ret = sscanf(uri, "/api/v%d", (int *) &version); - if (ret != 1 || version != api::version) - throw RuntimeError("Unsupported API version: {}", version); - - /* This is an OPTIONS request. - * - * We immediatly send headers and close the connection - * without waiting for a POST body */ - if (options) - lws_callback_on_writable(wsi); - - delete uri; -} - -void Http::read(void *in, size_t len) -{ - request.buffer.append((const char *) in, len); -} - -int Http::complete() -{ - json_t *req; - - req = request.buffer.decode(); - if (!req) - return 0; - - request.buffer.clear(); - request.queue.push(req); - - return 1; -} - -int Http::write() -{ - int ret; - - if (!headersSent) { - std::stringstream headers; - - json_t *resp = response.queue.pop(); - - response.buffer.clear(); - response.buffer.encode(resp); - - json_decref(resp); - - headers << "HTTP/1.1 200 OK\r\n" - << "Content-type: application/json\r\n" - << "User-agent: " USER_AGENT "\r\n" - << "Connection: close\r\n" - << "Content-Length: " << response.buffer.size() << "\r\n" - << "Access-Control-Allow-Origin: *\r\n" - << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" - << "Access-Control-Allow-Headers: Content-Type\r\n" - << "Access-Control-Max-Age: 86400\r\n" - << "\r\n"; - - ret = lws_write(wsi, (unsigned char *) headers.str().data(), headers.str().size(), LWS_WRITE_HTTP_HEADERS); - if (ret < 0) - return -1; - - /* No wait, until we can send the body */ - headersSent = true; - lws_callback_on_writable(wsi); - - return 0; - } - else { - ret = lws_write(wsi, (unsigned char *) response.buffer.data(), response.buffer.size(), LWS_WRITE_HTTP_FINAL); - if (ret < 0) - return -1; - - headersSent = false; - - return 1; - } -} - -std::string Http::getName() -{ - std::stringstream ss; - - ss << Wsi::getName() << ", mode=http"; - - return ss.str(); -} - -int api_http_protocol_cb(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) -{ - int ret; - - lws_context *ctx = lws_get_context(wsi); - void *user_ctx = lws_context_user(ctx); - - Web *w = static_cast(user_ctx); - Http *s = static_cast(user); - Api *a = w->getApi(); - - if (a == nullptr) - return -1; - - switch (reason) { - case LWS_CALLBACK_HTTP_BIND_PROTOCOL: - if (a == nullptr) - return -1; - - try { - new (s) Http(a, wsi); - - a->sessions.push_back(s); - } - catch (RuntimeError &e) { - return -1; - } - - break; - - case LWS_CALLBACK_HTTP_DROP_PROTOCOL: - if (s == nullptr) - return -1; - - a->sessions.remove(s); - - s->~Http(); - - break; - - case LWS_CALLBACK_HTTP_BODY: - s->read(in, len); - - break; - - case LWS_CALLBACK_HTTP_BODY_COMPLETION: - ret = s->complete(); - if (ret) - a->pending.push(s); - - break; - - case LWS_CALLBACK_HTTP_WRITEABLE: - ret = s->write(); - if (ret) { - if (lws_http_transaction_completed(wsi)) - return -1; - } - - return 0; - - default: - break; - } - - return 0; -} diff --git a/lib/api/sessions/socket.cpp b/lib/api/sessions/socket.cpp deleted file mode 100644 index 1f016f4ba..000000000 --- a/lib/api/sessions/socket.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/** Unix domain socket Api session. - * - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#include - -#include -#include -#include - -using namespace villas::node::api::sessions; - -Socket::Socket(Api *a, int s) : - Session(a), - sd(s) -{ - -} - -Socket::~Socket() -{ - close(sd); -} - -std::string Socket::getName() -{ - std::stringstream ss; - - ss << Session::getName() << ", mode=socket"; - - return ss.str(); -} - -int Socket::read() -{ - json_t *j; - json_error_t err; - - j = json_loadfd(sd, JSON_DISABLE_EOF_CHECK, &err); - if (!j) - return -1; - - request.queue.push(j); - - api->pending.push(this); - - return 0; -} - -int Socket::write() -{ - int ret; - json_t *j; - - while (!response.queue.empty()) { - j = response.queue.pop(); - - ret = json_dumpfd(j, sd, 0); - if (ret) - return ret; - - char nl = '\n'; - send(sd, &nl, 1, 0); - } - - return 0; -} diff --git a/lib/api/sessions/websocket.cpp b/lib/api/sessions/websocket.cpp deleted file mode 100644 index c42fcfc7d..000000000 --- a/lib/api/sessions/websocket.cpp +++ /dev/null @@ -1,155 +0,0 @@ -/** WebSockets Api session. - * - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#include - -#include - -#include -#include -#include -#include - -using namespace villas::node; -using namespace villas::node::api::sessions; - -WebSocket::WebSocket(Api *a, lws *w) : - Wsi(a, w) -{ - /* Parse request URI */ - int version; - char uri[64]; - lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_GET_URI); - - sscanf(uri, "/v%d", (int *) &version); - - if (version != api::version) - throw RuntimeError("Unsupported API version: {}", version); -} - -int WebSocket::read(void *in, size_t len) -{ - json_t *req; - - if (lws_is_first_fragment(wsi)) - request.buffer.clear(); - - request.buffer.append((const char *) in, len); - - if (lws_is_final_fragment(wsi)) { - req = request.buffer.decode(); - if (!req) - return 0; - - request.queue.push(req); - - return 1; - } - - return 0; -} - -int WebSocket::write() -{ - json_t *resp; - - if (state == State::SHUTDOWN) - return -1; - - resp = response.queue.pop(); - - char pad[LWS_PRE]; - - response.buffer.clear(); - response.buffer.append(pad, sizeof(pad)); - response.buffer.encode(resp); - - json_decref(resp); - - return lws_write(wsi, (unsigned char *) response.buffer.data() + LWS_PRE, response.buffer.size() - LWS_PRE, LWS_WRITE_TEXT); -} - -std::string WebSocket::getName() -{ - std::stringstream ss; - - ss << Wsi::getName() << ", mode=ws"; - - return ss.str(); -} - -int api_ws_protocol_cb(lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) -{ - int ret; - - lws_context *ctx = lws_get_context(wsi); - void *user_ctx = lws_context_user(ctx); - - Web *w = static_cast(user_ctx); - WebSocket *s = static_cast(user); - Api *a = w->getApi(); - - if (a == nullptr) - return -1; - - switch (reason) { - case LWS_CALLBACK_ESTABLISHED: - if (a == nullptr) { - std::string err = "API disabled"; - lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR, (unsigned char *) err.data(), err.length()); - return -1; - } - - new (s) WebSocket(a, wsi); - - a->sessions.push_back(s); - - break; - - case LWS_CALLBACK_CLOSED: - - s->~WebSocket(); - - a->sessions.remove(s); - - break; - - case LWS_CALLBACK_RECEIVE: - ret = s->read(in, len); - if (ret) - a->pending.push(s); - - break; - - case LWS_CALLBACK_SERVER_WRITEABLE: - ret = s->write(); - if (ret) - return ret; - - break; - - default: - break; - } - - return 0; -} diff --git a/lib/api/sessions/wsi.cpp b/lib/api/sessions/wsi.cpp deleted file mode 100644 index c176e2e51..000000000 --- a/lib/api/sessions/wsi.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/** LWS Api session. - * - * @author Steffen Vogel - * @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC - * @license GNU General Public License (version 3) - * - * VILLASnode - * - * This program 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, either version 3 of the License, or - * any later version. - * - * 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 . - *********************************************************************************/ - -#include - -#include - -using namespace villas::node::api::sessions; - -void Wsi::runPendingActions() -{ - Session::runPendingActions(); - - logger->debug("Ran pending actions. Triggering on_writeable callback: wsi={}", (void *) wsi); - - web->callbackOnWritable(wsi); -} - -void Wsi::shutdown() -{ - state = State::SHUTDOWN; - - web->callbackOnWritable(wsi); -} - -std::string Wsi::getName() -{ - std::stringstream ss; - - ss << Session::getName(); - - if (wsi) { - char name[128]; - char ip[128]; - - lws_get_peer_addresses(wsi, lws_get_socket_fd(wsi), name, sizeof(name), ip, sizeof(ip)); - - ss << ", remote.name=" << name << ", remote.ip=" << ip; - } - - return ss.str(); - -} - -Wsi::Wsi(Api *a, lws *w) : - Session(a), - wsi(w) -{ - state = Session::State::ESTABLISHED; - - lws_context *user_ctx = lws_get_context(wsi); - void *ctx = lws_context_user(user_ctx); - - web = static_cast(ctx); -}