Skip to content

Commit

Permalink
HTTPServer
Browse files Browse the repository at this point in the history
Access handlers using a HTTP server. The HTTP Server is using the good old userlevel sockets, it is not handled inside Click's data path. This is exactly like ControlSocket, except it works in HTTP.

GET /element/handler will issue a read on element's handler
PUT /element/handler will issue a write passing the body as the handler string. Parameters like ?id=7 append to the request will be lost
POST /element/handler?id=7 will issue a write passing the "id=7\n<body>" as data where <body> is the request data
DELETE and other requests will translate as read or a write according to the rfc but with the lowercase request as prefix. Eg :
    DELETE /element/handler?id=7 will do a write on the "delete_handler" passing data id=7, body will be lost
Actually other than DELETE are lost but I'll welcome pull requests

GET / or element class like /myclass/ will list the sub-elements
GET /element/ will list the handlers

This is very convenient with the JSON elements to make Click RESTful. We have some internal elements that respond to their handlers as JSON, allowing to use Click with an API.

It's working but corner cases are unsure. Good enough for research...

Userlevel only. Requires libmicrohttpd (available in all good OS packagers)
  • Loading branch information
Tom Barbette committed Oct 10, 2017
1 parent a1d5ae4 commit 7dc2031
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 0 deletions.
14 changes: 14 additions & 0 deletions configure.in
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ else
LIBS="$save_libs"
fi


AC_SEARCH_LIBS([MHD_start_daemon], [microhttpd], [ac_have_libmicrohttpd=yes], [ac_have_libmicrohttpd=no])
AC_CHECK_HEADERS([microhttpd.h], [ac_have_microhttpd_h=yes], [ac_have_microhttpd_h=no])
#if test "$ac_have_libpthread$ac_have_pthread_h" = yesyes; then
if test "$ac_have_libmicrohttpd$ac_have_microhttpd_h" = yesyes; then
AC_DEFINE([HAVE_MICROHTTPD])
fi


AC_ARG_ENABLE([select],
[AS_HELP_STRING([ --enable-select=[[select|poll|kqueue]]], [set file descriptor wait mechanism])
AS_HELP_STRING([ --disable-select], [do not use select()])],
Expand Down Expand Up @@ -2227,6 +2236,11 @@ if test "$ac_have_libmbdevio$ac_have_mbgdevio_h" = yesyes; then
provisions="$provisions mbglib"
fi

dnl add 'microhttpd' if libmicrohttpd support is available
if test "$ac_have_libmicrohttpd$ac_have_microhttpd_h" = yesyes; then
provisions="$provisions microhttpd"
fi

dnl add 'netmap' if netmap support is available
if test "x$use_netmap" != xno; then
provisions="$provisions netmap"
Expand Down
297 changes: 297 additions & 0 deletions elements/userlevel/httpserver.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
#include <click/config.h>


#include <microhttpd.h>
#include "httpserver.hh"

#include <click/args.hh>
#include <click/standard/scheduleinfo.hh>
#include <click/error.hh>
#include <click/router.hh>
#include <sys/select.h>

CLICK_DECLS


static int ahc_policy(void *cls, const struct sockaddr *addr, socklen_t addrlen) {
(void)cls;
(void)addr;
(void)addrlen;
return MHD_YES;
}

HTTPServer::HTTPServer() : _verbose(false), _port(80), _daemon(0) {
}

HTTPServer::~HTTPServer() {

}


void HTTPServer::selected(int fd, int mask) {
remove_select(fd, mask);
MHD_run(_daemon);
update_fd_set();
}

void HTTPServer::update_fd_set() {
int max_fd = 0;
fd_set _read_fd_set,_write_fd_set,_except_fd_set;

FD_ZERO(&_read_fd_set);
FD_ZERO(&_write_fd_set);
FD_ZERO(&_except_fd_set);

if (MHD_get_fdset(_daemon, &_read_fd_set, &_write_fd_set, &_except_fd_set, &max_fd) != MHD_YES) {
click_chatter("Could not get fd set");
return;
}
for (int i = 0; i <= max_fd; i++) {
if (FD_ISSET(i, &_read_fd_set)) {
add_select(i,SELECT_READ);
}
if (FD_ISSET(i, &_write_fd_set)) {
add_select(i,SELECT_WRITE);
}
}
}

int HTTPServer::configure(Vector<String> &conf, ErrorHandler *errh) {
if (Args(conf, this, errh)
.read_p("PORT", _port) //TODO AUTH
.read("VERBOSE", _verbose)
.complete() < 0)
return -1;

return 0;
}


int HTTPServer::initialize(ErrorHandler *errh) {
_daemon = MHD_start_daemon(MHD_USE_DEBUG,
_port,
&ahc_policy,
NULL,
&ahc_echo,
(void*)this,
MHD_OPTION_END);
if (_daemon == NULL)
return 1;

update_fd_set();

return 0;
}


int HTTPServer::ahc_echo(
void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **con_cls) {
HTTPServer *server = reinterpret_cast<HTTPServer *>(cls);

int ret = MHD_YES;

if (NULL == *con_cls)
{*con_cls = new String("");

return MHD_YES;
}

if (server->_verbose)
click_chatter("[%s] %s",method,url);


//Processing request
String body;
int status;
struct MHD_Response *response;
const Handler *h;

String path = String(&url[1]);
if (path[0] == '/')
path = path.substring(1);

Element *e = 0;
String ename;
String fullpath = "";
do {
int pos = path.find_left('/');
String en;
if (pos == -1) {
en = path;
} else {
en = path.substring(0,pos);
}

Element* et = server->router()->find(fullpath + en);
if (!et) {
break;
}
e = et;
ename = en;
fullpath = en + "/";
if (pos == -1) {
path = "";
break;
} else {
path = path.substring(pos + 1);
}
} while (1);


String hname;
String param;
int isNotPost = strcmp (method, "POST");
int isNotPut = strcmp (method, "PUT");
int pos = path.find_left('/');
if (pos == -1) {
hname = path;
param = "";
} else {
hname = path.substring(0,pos);
param = path.substring(pos + 1);
}
click_chatter("Element '%s', handler '%s', param '%s'",ename.c_str(), hname.c_str(),param.c_str());

if (!e) {
if (hname == "" || ename == "") {
e = server->router()->root_element();
} else {
body = "No element named '" + ename + "'";
status = 404;
goto send;
}
}


if ((hname == "") && strcmp("GET",method) == 0) {
/*Json jelements = Json::make_array();
for (int i = 0; i < server->router()->nelements(); i++) {
String ename = server->router()->element(i)->name();
Json je = Json::make_object();
je.set("name",ename);
jelements.push_back(je);
}
body = jelements.unparse();
status = MHD_HTTP_OK;
goto send;*/
if (ename.length() > 0) {
hname = "handlers";
} else {
hname = "list";
}
}

// Then find handler.
if (strcmp("GET",method) == 0) {
h = Router::handler(e, hname);
if (h && h->visible()) {
if (h->readable()) {
if (h->flags() & Handler::f_read_param) {
body = h->call_read(e, param, ErrorHandler::default_handler());
} else {
body = h->call_read(e, ErrorHandler::default_handler());
}
status = MHD_HTTP_OK;
} else {
body = "This request is not readable";
status = MHD_HTTP_BAD_REQUEST;
}
goto send;
} else {
goto bad_handler;
}
} else if (0 == isNotPost or 0 == isNotPut) {
if (isNotPost) {
hname = "put_" + hname;
}
h = Router::handler(e, hname);
if (h && h->visible()) {
if (*upload_data_size != 0) {
static_cast<String*>(*con_cls)->append(String(upload_data, *upload_data_size));
*upload_data_size = 0;

return MHD_YES;
} else {
String data = *static_cast<String*>(*con_cls);
click_chatter("Last call with data %s",data.c_str());
if (h->writable()) {
int ret;
if (isNotPost)
ret = h->call_write(param + "\n" + data, e, ErrorHandler::default_handler());
else
ret = h->call_write(data, e, ErrorHandler::default_handler());
if (ret == 0) {
body = "success";
} else {
body = "error";
}
status = MHD_HTTP_OK;
} else {
body = "This request is not writable";
status = MHD_HTTP_BAD_REQUEST;
}
delete static_cast<String*>(*con_cls);
}
goto send;
} else {
goto bad_handler;
}
} else if (strcmp("DELETE",method) == 0) {
hname = "delete_" + hname;
h = Router::handler(e, hname);
if (h && h->visible()) {
int ret = h->call_write(param, e, ErrorHandler::default_handler());
if (ret == 0) {
body = "success";
} else {
body = "error";
}
status = MHD_HTTP_OK;
goto send;
} else {
goto bad_handler;
}
} else {
body = "Unsupported method";
status = MHD_HTTP_METHOD_NOT_ALLOWED;
goto send;
}
assert(false);

bad_handler:
body = "Invalid path '" + String(url) + "' or no " + hname + " in " + ename;
status = 404;
goto send;

send:
response = MHD_create_response_from_buffer (body.length(),
(void*)body.c_str(),
MHD_RESPMEM_MUST_COPY);
if (NULL == response) {
click_chatter("Could not create response");
return MHD_NO;
}
ret = MHD_queue_response(connection,
status,
response);
MHD_destroy_response(response);

return ret;
}

void HTTPServer::cleanup(CleanupStage) {
if (_daemon != NULL)
MHD_stop_daemon(_daemon);
}

CLICK_ENDDECLS
ELEMENT_REQUIRES(userlevel microhttpd)
EXPORT_ELEMENT(HTTPServer)
ELEMENT_MT_SAFE(HTTPServer)
63 changes: 63 additions & 0 deletions elements/userlevel/httpserver.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// -*- c-basic-offset: 4 -*-
#ifndef CLICK_HTTPSERVER_HH
#define CLICK_HTTPSERVER_HH
#include <click/element.hh>
#include <click/etheraddress.hh>
#include <click/task.hh>
#include <click/notifier.hh>
#include <click/multithread.hh>
#include <click/ring.hh>
CLICK_DECLS

/*
=c
HTTPServer */

class HTTPServer : public Element { public:

HTTPServer() CLICK_COLD;
~HTTPServer() CLICK_COLD;

const char *class_name() const { return "HTTPServer"; }
const char *port_count() const { return PORTS_0_0; }


int configure(Vector<String> &, ErrorHandler *) CLICK_COLD;
int initialize(ErrorHandler *) CLICK_COLD;
void cleanup(CleanupStage) CLICK_COLD;

void selected(int fd, int mask);
void update_fd_set();

static int ahc_echo(
void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **ptr
);
private:
int _port;
bool _verbose;
struct MHD_Daemon * _daemon;


class Request {
public:
struct MHD_Connection * connection;

Request() : connection(0) {

}

};
MPMCRing<Request*,32> _requests;

};

CLICK_ENDDECLS
#endif

0 comments on commit 7dc2031

Please sign in to comment.