1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00

api: rewrite API to v2

This commit is contained in:
Steffen Vogel 2020-08-17 17:03:54 +02:00
parent c191b15809
commit b58573f123
28 changed files with 913 additions and 1358 deletions

View file

@ -30,19 +30,49 @@
#include <thread>
#include <list>
#include <libwebsockets.h>
#include <villas/log.hpp>
#include <villas/common.hpp>
#include <villas/api/server.hpp>
#include <villas/queue_signalled.hpp>
#include <villas/exceptions.hpp>
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<bool> running; /**< Atomic flag for signalizing thread termination. */
SuperNode *super_node;
api::Server server;
void run();
void worker();

View file

@ -1,87 +0,0 @@
/** REST-API-releated functions.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#pragma once
#include <jansson.h>
#include <villas/log.hpp>
#include <villas/plugin.hpp>
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<typename T, const char *name, const char *desc>
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 */

View file

@ -1,81 +0,0 @@
/** Socket API endpoint.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#pragma once
#include <string>
#include <vector>
#include <map>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>
#include <villas/common.hpp>
#include <villas/log.hpp>
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<pollfd> pfds;
std::map<int, sessions::Socket *> 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 */

View file

@ -23,6 +23,8 @@
#pragma once
#include <atomic>
#include <jansson.h>
#include <villas/queue.h>
@ -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<json_t *> queue;
} request, response;
lws *wsi;
Web *web;
Api *api;
Logger logger;
JsonBuffer requestBuffer;
JsonBuffer responseBuffer;
std::atomic<Request *> request;
std::atomic<Response *> 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 */

View file

@ -1,61 +0,0 @@
/** HTTP Api session.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#pragma once
#include <villas/api/sessions/wsi.hpp>
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 */

View file

@ -1,55 +0,0 @@
/** WebSockets API session.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#pragma once
#include <villas/api/sessions/wsi.hpp>
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 */

View file

@ -21,7 +21,9 @@
*********************************************************************************/
#include <villas/api.hpp>
#include <villas/web.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/utils.hpp>
#include <villas/node/config.h>
#include <villas/memory.h>
@ -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");

View file

@ -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})

View file

@ -1,4 +1,4 @@
/** LWS Api session.
/** API Request.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
@ -21,43 +21,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
#pragma once
#include <villas/plugin.hpp>
#include <villas/api.hpp>
#include <villas/api/request.hpp>
#include <string>
using namespace villas;
using namespace villas::node::api;
#include <villas/api/session.hpp>
#include <villas/web.hpp>
Request * RequestFactory::make(Session *s, const std::string &uri, int meth)
{
for (auto *rf : plugin::Registry::lookup<RequestFactory>()) {
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");
}

View file

@ -21,19 +21,20 @@
*********************************************************************************/
#include <villas/config.h>
#include <villas/api/action.hpp>
#include <villas/hook.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<ActionFactory>()) {
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<RequestFactory>()) {
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<CapabilitiesAction, n, d> p;
static RequestPlugin<CapabilitiesRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -20,35 +20,43 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <villas/api/action.hpp>
#include <villas/api/session.hpp>
#include <villas/super_node.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<ConfigAction, n, d> p;
static RequestPlugin<ConfigRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

77
lib/api/requests/file.cpp Normal file
View file

@ -0,0 +1,77 @@
/** The "file" API ressource.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <villas/super_node.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
#include <villas/nodes/file.hpp>
#include <villas/io.h>
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<FileRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */
} /* namespace villas */

View file

@ -26,63 +26,69 @@
#include <villas/node.h>
#include <villas/super_node.hpp>
#include <villas/utils.hpp>
#include <villas/api/session.hpp>
#include <villas/api/action.hpp>
#include <villas/api.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
namespace villas {
namespace node {
namespace api {
template<int (*A)(struct node *)>
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<NodeAction<node_start>, n1, d1> p1;
static RequestPlugin<NodeActionRequest<node_start>, n1, r1, d1> p1;
char n2[] = "node.stop";
char n2[] = "node/stop";
char r2[] = "/node/([^/]+)/stop";
char d2[] = "stop a node";
static ActionPlugin<NodeAction<node_stop>, n2, d2> p2;
static RequestPlugin<NodeActionRequest<node_stop>, n2, r2, d2> p2;
char n3[] = "node.pause";
char n3[] = "node/pause";
char r3[] = "/node/([^/]+)/pause";
char d3[] = "pause a node";
static ActionPlugin<NodeAction<node_pause>, n3, d3> p3;
static RequestPlugin<NodeActionRequest<node_pause>, n3, r3, d3> p3;
char n4[] = "node.resume";
char n4[] = "node/resume";
char r4[] = "/node/([^/]+)/resume";
char d4[] = "resume a node";
static ActionPlugin<NodeAction<node_resume>, n4, d4> p4;
static RequestPlugin<NodeActionRequest<node_resume>, n4, r4, d4> p4;
char n5[] = "node.restart";
char n5[] = "node/restart";
char r5[] = "/node/([^/]+)/restart";
char d5[] = "restart a node";
static ActionPlugin<NodeAction<node_restart>, n5, d5> p5;
static RequestPlugin<NodeActionRequest<node_restart>, n5, r5, d5> p5;
} /* namespace api */

View file

@ -27,20 +27,27 @@
#include <villas/node.h>
#include <villas/utils.hpp>
#include <villas/stats.hpp>
#include <villas/api/action.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<NodesAction, n, d> p;
static RequestPlugin<NodesRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -0,0 +1,88 @@
/** The API ressource for start/stop/pause/resume paths.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <jansson.h>
#include <villas/plugin.h>
#include <villas/node.h>
#include <villas/super_node.hpp>
#include <villas/utils.hpp>
#include <villas/api.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
namespace villas {
namespace node {
namespace api {
template<int (*A)(struct node *)>
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<PathActionRequest<path_start>, n1, r1, d1> p1;
char n2[] = "path/stop";
char r2[] = "/path/([^/]+)/stop";
char d2[] = "stop a path";
static RequestPlugin<PathActionRequest<path_stop>, n2, r2, d2> p2;
} /* namespace api */
} /* namespace node */
} /* namespace villas */

123
lib/api/requests/paths.cpp Normal file
View file

@ -0,0 +1,123 @@
/** The "paths" API ressource.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <jansson.h>
#include <villas/super_node.hpp>
#include <villas/path.h>
#include <villas/path_source.h>
#include <villas/path_destination.h>
#include <villas/hook.hpp>
#include <villas/utils.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<PathsRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */
} /* namespace villas */

View file

@ -1,4 +1,4 @@
/** The "restart" API action.
/** The "restart" API request.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC
@ -21,17 +21,18 @@
*********************************************************************************/
#include <villas/super_node.hpp>
#include <villas/api/action.hpp>
#include <villas/api/session.hpp>
#include <villas/log.hpp>
#include <villas/node/exceptions.hpp>
#include <villas/utils.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
#include <villas/api/session.hpp>
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<RestartAction, n, d> p;
static RequestPlugin<RestartRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -1,4 +1,4 @@
/** The "shutdown" API action.
/** The "shutdown" API request.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC
@ -23,29 +23,37 @@
#include <signal.h>
#include <villas/utils.hpp>
#include <villas/api/action.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<ShutdownAction, n, d> p;
static RequestPlugin<ShutdownRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -1,4 +1,4 @@
/** The "paths" API ressource.
/** The API ressource for querying statistics.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC
@ -22,51 +22,58 @@
#include <jansson.h>
#include <villas/log.h>
#include <villas/node.h>
#include <villas/stats.hpp>
#include <villas/super_node.hpp>
#include <villas/path.h>
#include <villas/utils.hpp>
#include <villas/api/action.hpp>
#include <villas/api.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<PathsAction, n, d> p;
/* Register API requests */
static char n[] = "stats";
static char r[] = "/node/([^/]+)/stats";
static char d[] = "get internal statistics counters";
static RequestPlugin<StatsRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -1,4 +1,4 @@
/** The API ressource for getting and resetting statistics.
/** The API ressource for resetting statistics.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC
@ -27,54 +27,56 @@
#include <villas/stats.hpp>
#include <villas/super_node.hpp>
#include <villas/utils.hpp>
#include <villas/api/session.hpp>
#include <villas/api/action.hpp>
#include <villas/api.hpp>
#include <villas/api/session.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<StatsAction, n, d> p;
/* Register API requests */
static char n[] = "stats/reset";
static char r[] = "/stats/reset";
static char d[] = "reset internal statistics counters";
static RequestPlugin<StatsRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -1,4 +1,4 @@
/** The "stats" API action.
/** The "stats" API request.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @copyright 2014-2020, Institute for Automation of Complex Power Systems, EONERC
@ -24,37 +24,45 @@
#include <jansson.h>
#include <villas/api/action.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<StatusAction, n, d> p;
static RequestPlugin<StatusRequest, n, r, d> p;
} /* namespace api */
} /* namespace node */

View file

@ -1,4 +1,4 @@
/** Socket Api session.
/** API Response.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
@ -21,37 +21,28 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************************/
#pragma once
#include <villas/api/response.hpp>
#include <villas/api/request.hpp>
#include <villas/api/session.hpp>
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()
);
}

View file

@ -1,211 +0,0 @@
/** Socket API endpoint.
*
* @file
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <unistd.h>
#include <sys/types.h>
#include <exception>
#include <algorithm>
#if __GNUC__ <= 7 && !defined(__clang__)
#include <experimental/filesystem>
#else
#include <filesystem>
#endif
#include <villas/config.h>
#include <villas/node/exceptions.hpp>
#include <villas/utils.hpp>
#include <villas/super_node.hpp>
#include <villas/api/server.hpp>
#include <villas/api/sessions/socket.hpp>
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<sessions::Socket *> 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;
}

View file

@ -29,109 +29,334 @@
#include <villas/memory.h>
#include <villas/api/session.hpp>
#include <villas/api/action.hpp>
#include <villas/api/request.hpp>
#include <villas/api/response.hpp>
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<Web *>(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<char *>(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<const char *, const char *> 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<const unsigned char *>(hdr.first),
reinterpret_cast<const unsigned char *>(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<ActionFactory>(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<Session *>(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";
}
}

View file

@ -1,212 +0,0 @@
/** HTTP Api session.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <sstream>
#include <libwebsockets.h>
#include <villas/node/exceptions.hpp>
#include <villas/web.hpp>
#include <villas/log.hpp>
#include <villas/config.h>
#include <villas/api/sessions/http.hpp>
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<Web *>(user_ctx);
Http *s = static_cast<Http *>(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;
}

View file

@ -1,85 +0,0 @@
/** Unix domain socket Api session.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <sstream>
#include <villas/compat.hpp>
#include <villas/log.hpp>
#include <villas/api/sessions/socket.hpp>
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;
}

View file

@ -1,155 +0,0 @@
/** WebSockets Api session.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <sstream>
#include <libwebsockets.h>
#include <villas/web.hpp>
#include <villas/node/exceptions.hpp>
#include <villas/log.hpp>
#include <villas/api/sessions/websocket.hpp>
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<Web *>(user_ctx);
WebSocket *s = static_cast<WebSocket *>(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;
}

View file

@ -1,74 +0,0 @@
/** LWS Api session.
*
* @author Steffen Vogel <stvogel@eonerc.rwth-aachen.de>
* @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 <http://www.gnu.org/licenses/>.
*********************************************************************************/
#include <sstream>
#include <villas/api/sessions/wsi.hpp>
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<Web*>(ctx);
}