/*! * restd.c * * Copyright (c) 2015-2020, NADAL Jean-Baptiste. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA * * @Author: NADAL Jean-Baptiste * @Date: 07/02/2020 * */ /*------------------------------- INCLUDES ----------------------------------*/ #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #else #include #endif #include "macro.h" #include "restd.h" struct mimetype_s { const char *extn; const char *mime; }; /* * Local variables. */ static bool initialized = false; /* * Global variables. */ int _restd_log_level = RESTD_LOG_WARN; /*--------------------------------------------------------------------------*/ static const struct mimetype_s g_mime_types[] = { {"txt", "text/plain"}, {"log", "text/plain"}, {"js", "text/javascript"}, {"css", "text/css"}, {"htm", "text/html"}, {"html", "text/html"}, {"diff", "text/x-patch"}, {"patch", "text/x-patch"}, {"c", "text/x-csrc"}, {"h", "text/x-chdr"}, {"o", "text/x-object"}, {"ko", "text/x-object"}, {"bmp", "image/bmp"}, {"gif", "image/gif"}, {"png", "image/png"}, {"jpg", "image/jpeg"}, {"jpeg", "image/jpeg"}, {"svg", "image/svg+xml"}, {"json", "application/json"}, {"jsonp", "application/javascript"}, {"zip", "application/zip"}, {"pdf", "application/pdf"}, {"xml", "application/xml"}, {"xsl", "application/xml"}, {"doc", "application/msword"}, {"ppt", "application/vnd.ms-powerpoint"}, {"xls", "application/vnd.ms-excel"}, {"odt", "application/vnd.oasis.opendocument.text"}, {"odp", "application/vnd.oasis.opendocument.presentation"}, {"pl", "application/x-perl"}, {"sh", "application/x-shellscript"}, {"php", "application/x-php"}, {"deb", "application/x-deb"}, {"iso", "application/x-cd-image"}, {"tar.gz", "application/x-compressed-tar"}, {"tgz", "application/x-compressed-tar"}, {"gz", "application/x-gzip"}, {"tar.bz2", "application/x-bzip-compressed-tar"}, {"tbz", "application/x-bzip-compressed-tar"}, {"bz2", "application/x-bzip"}, {"tar", "application/x-tar"}, {"rar", "application/x-rar-compressed"}, {"mp3", "audio/mpeg"}, {"ogg", "audio/x-vorbis+ogg"}, {"wav", "audio/x-wav"}, {"mpg", "video/mpeg"}, {"mpeg", "video/mpeg"}, {"avi", "video/x-msvideo"}, {"README", "text/plain"}, {"log", "text/plain"}, {"cfg", "text/plain"}, {"conf", "text/plain"}, {"pac", "application/x-ns-proxy-autoconfig"}, {"wpad.dat", "application/x-ns-proxy-autoconfig"}, {"appcache", "text/cache-manifest"}, {"manifest", "text/cache-manifest"}, {NULL, NULL}}; /*--------------------------------------------------------------------------*/ /** * Server option names and default values. */ #define RESTD_SERVER_OPTIONS { \ {"server.port", "8888"}, \ \ {"server.root_path", ""}, \ \ /* Addr format IPv4="1.2.3.4", IPv6="1:2:3:4:5:6", Unix="/path" */ \ {"server.addr", "0.0.0.0"}, \ \ {"server.backlog", "128"}, \ \ /* Set read timeout seconds. 0 means no timeout. */ \ {"server.timeout", "0"}, \ \ /* Enable or disable request pipelining, this change AD_DONE's behavior */ \ {"server.request_pipelining", "1"}, \ \ /* Run server in a separate thread */ \ {"server.thread", "0"}, \ \ /* Collect resources after stop */ \ {"server.free_on_stop", "1"}, \ \ /* End of array marker. Do not remove */ \ {"", "_END_"}}; /*------------------------------- FUNCTIONS ----------------------------------*/ static int set_undefined_options(restd_server_t *server); char *restd_server_get_option(restd_server_t *server, const char *key); int restd_server_get_option_int(restd_server_t *server, const char *key); static void close_server(restd_server_t *server); static void *server_loop(void *instance); static void libevent_log_cb(int severity, const char *msg); static int notify_loopexit(restd_server_t *server); static void notify_cb(struct bufferevent *buffer, void *userdata); void rest_request_cb(struct evhttp_request *req, void *arg); void print_request_info(struct evhttp_request *req); void restd_http_response_from_file(struct evhttp_request *req, int code, int fd, const char *content_type); bool manage_hook(restd_hook_t *hook, restd_resp_t *response, const char *request_path); /*--------------------------- PUBLIC FUNCTIONS -------------------------------*/ /*-------------------------------------------------------------------------- * * Set debug output level. * * @param debug_level debug output level. 0 to disable. * * @return previous debug level. * * @note * debug_level: * REST_LOG_DISABLE * REST_LOG_ERROR * REST_LOG_WARN (default) * REST_LOG_INFO * REST_LOG_DEBUG * REST_LOG_DEBUG2 */ enum restd_log_e restd_log_level(enum restd_log_e log_level) { int prev = _restd_log_level; _restd_log_level = log_level; return prev; } /*-------------------------------------------------------------------------- * * Create a server object. */ restd_server_t *restd_server_new(void) { if (initialized) { initialized = true; } restd_server_t *server = NEW_OBJECT(restd_server_t); if (server == NULL) { return NULL; } // Initialize instance. server->options = qhashtbl(0, 0); server->stats = qhashtbl(100, QHASHTBL_THREADSAFE); server->hooks = qlist(0); //server->call_hooks = call_hooks; if (server->options == NULL || server->stats == NULL || server->hooks == NULL) { restd_server_free(server); return NULL; } DEBUG("Created a server object."); return server; } /*-------------------------------------------------------------------------- * * Release server object and all the resources. */ void restd_server_free(restd_server_t *server) { if (server == NULL) return; int thread = restd_server_get_option_int(server, "server.thread"); if (thread && server->thread) { notify_loopexit(server); sleep(1); close_server(server); } if (server->evhttp) { evhttp_free(server->evhttp); } if (server->evbase) { event_base_free(server->evbase); } if (server->options) { server->options->free(server->options); } if (server->stats) { server->stats->free(server->stats); } if (server->hooks) { qlist_t *tbl = server->hooks; restd_hook_t *hook; while ((hook = qlist_popfirst(tbl, NULL))) { restd_hook_free(hook); } qlist_free(server->hooks); } free(server); DEBUG("Server terminated."); } /*-------------------------------------------------------------------------- * * Start server. * * @return 0 if successful, otherwise -1. */ int restd_server_start(restd_server_t *server) { int ret; DEBUG("Starting a server."); if (server == NULL) return -1; // Set default options that were not set by user.. set_undefined_options(server); // Hookup libevent's log message. if (_restd_log_level >= RESTD_LOG_DEBUG) { event_set_log_callback(libevent_log_cb); if (_restd_log_level >= RESTD_LOG_DEBUG2) { event_enable_debug_mode(); } } // Parse addr int port = restd_server_get_option_int(server, "server.port"); char *addr = restd_server_get_option(server, "server.addr"); struct sockaddr *sockaddr = NULL; size_t sockaddr_len = 0; if (addr[0] == '/') { // Unix socket. struct sockaddr_un unixaddr; bzero((void *)&unixaddr, sizeof(struct sockaddr_un)); if (strlen(addr) >= sizeof(unixaddr.sun_path)) { errno = EINVAL; DEBUG("Too long unix socket name. '%s'", addr); return -1; } unixaddr.sun_family = AF_UNIX; strcpy(unixaddr.sun_path, addr); // no need of strncpy() sockaddr = (struct sockaddr *)&unixaddr; sockaddr_len = sizeof(unixaddr); } else if (strstr(addr, ":")) { // IPv6 struct sockaddr_in6 ipv6addr; bzero((void *)&ipv6addr, sizeof(struct sockaddr_in6)); ipv6addr.sin6_family = AF_INET6; ipv6addr.sin6_port = htons(port); evutil_inet_pton(AF_INET6, addr, &ipv6addr.sin6_addr); sockaddr = (struct sockaddr *)&ipv6addr; sockaddr_len = sizeof(ipv6addr); } else { // IPv4 struct sockaddr_in ipv4addr; bzero((void *)&ipv4addr, sizeof(struct sockaddr_in)); ipv4addr.sin_family = AF_INET; ipv4addr.sin_port = htons(port); ipv4addr.sin_addr.s_addr = (IS_EMPTY_STR(addr)) ? INADDR_ANY : inet_addr(addr); sockaddr = (struct sockaddr *)&ipv4addr; sockaddr_len = sizeof(ipv4addr); } // Bind if (!server->evbase) { server->evbase = event_base_new(); if (!server->evbase) { ERROR("Failed to create a new event base."); return -1; } } server->evhttp = evhttp_new(server->evbase); if (!server->evhttp) { ERROR("Event http initialize failed!\n"); return -2; } evhttp_set_gencb(server->evhttp, rest_request_cb, server); ret = evhttp_bind_socket(server->evhttp, addr, port); if (ret != 0) { ERROR("Http bind server addr:%s & port:%d failed!\n", addr, port); return -3; } // Listen INFO("Listening on %s:%d", addr, port); // Create a eventfd for notification channel. #ifdef __linux__ int notifyfd = eventfd(0, 0); #else int notifyfd = kqueue(); #endif server->notify_buffer = bufferevent_socket_new(server->evbase, notifyfd, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(server->notify_buffer, NULL, notify_cb, NULL, server); int exitstatus = 0; if (restd_server_get_option_int(server, "server.thread")) { DEBUG("Launching server as a thread.\n"); server->thread = NEW_OBJECT(pthread_t); pthread_create(server->thread, NULL, &server_loop, (void *)server); //pthread_detach(server->thread); } else { int *retval = server_loop(server); exitstatus = *retval; free(retval); close_server(server); if (restd_server_get_option_int(server, "server.free_on_stop")) { restd_server_free(server); } } return exitstatus; } /*--------------------------------------------------------------------------*/ int restd_server_attach_event_loop(restd_server_t *server, struct event_base *ev_base) { if (server == NULL) return -1; server->evbase = ev_base; return 0; } /*--------------------------------------------------------------------------*/ void restd_server_set_option(restd_server_t *server, const char *key, const char *value) { server->options->putstr(server->options, key, value); } /*--------------------------------------------------------------------------*/ void restd_server_register_hook_on_path(restd_server_t *server, enum evhttp_cmd_type method, const char *path, restd_callback cb, void *userdata) { restd_hook_t *hook; char *fragment; // Init Hook. hook = restd_hook_new(); hook->method = method; hook->path = (path) ? strdup(path) : NULL; hook->cb = cb; hook->userdata = userdata; hook->path_fragments = qstrtokenizer(path, "/"); // Split URI and detect parameter and action. while ((fragment = qlist_popfirst(hook->path_fragments, NULL)) != NULL) { char *param; param = strchr(fragment, ':'); if (param != NULL) { hook->has_param = true; hook->param_name = strdup(param + 1); } if (hook->has_param == true) { hook->action_name = strdup(fragment); } free(fragment); } server->hooks->addlast(server->hooks, (void *)hook, sizeof(restd_hook_t)); free(hook); } /*--------------------------------------------------------------------------*/ void restd_http_response(restd_resp_t *response, int code, const char *contenttype, const char *data) { struct evbuffer *resp_buf; struct evkeyvalq *resp_headers; resp_buf = evhttp_request_get_output_buffer(response->request); resp_headers = evhttp_request_get_output_headers(response->request); if (data != NULL) { evbuffer_add(resp_buf, data, strlen(data)); } evhttp_add_header(resp_headers, "Content-Type", contenttype); evhttp_send_reply(response->request, code, NULL, resp_buf); } /*--------------------------------------------------------------------------*/ char *restd_http_get_body(restd_resp_t *response) { char *body = NULL; struct evbuffer *buf; buf = evhttp_request_get_input_buffer(response->request); size_t len = evbuffer_get_length(buf); body = malloc(len + 1); int ret = evbuffer_copyout(buf, body, len); body[len] = '\0'; return body; } /*--------------------------------------------------------------------------*/ restd_hook_t *restd_hook_new(void) { restd_hook_t *hook = NEW_OBJECT(restd_hook_t); if (hook == NULL) { return NULL; } bzero((void *)hook, sizeof(restd_hook_t)); hook->has_param = false; return hook; } /*--------------------------------------------------------------------------*/ void restd_hook_free(restd_hook_t *hook) { if (hook == NULL) return; qlist_free(hook->path_fragments); if (hook->path) free(hook->path); if (hook->param_name) free(hook->param_name); if (hook->action_name) free(hook->action_name); free(hook); } /*--------------------------------------------------------------------------*/ restd_resp_t *restd_resp_new(void) { restd_resp_t *resp = NEW_OBJECT(restd_resp_t); if (resp == NULL) { return NULL; } bzero((void *)resp, sizeof(restd_resp_t)); return resp; } /*--------------------------------------------------------------------------*/ void restd_resp_free(restd_resp_t *response) { if (response == NULL) { return; } if (response->parameter) { free(response->parameter); } if (response->action) { free(response->action); } free(response); } /*--------------------------- LOCAL FUNCTIONS -------------------------------*/ /*--------------------------------------------------------------------------*/ // Set default options that were not set by user.. static int set_undefined_options(restd_server_t *server) { int newentries = 0; char *default_options[][2] = RESTD_SERVER_OPTIONS; for (int i = 0; !IS_EMPTY_STR(default_options[i][0]); i++) { if (!restd_server_get_option(server, default_options[i][0])) { restd_server_set_option(server, default_options[i][0], default_options[i][1]); newentries++; } DEBUG("%s=%s", default_options[i][0], restd_server_get_option(server, default_options[i][0])); } return newentries; } /*-------------------------------------------------------------------------- * * Retrieve server option. */ char *restd_server_get_option(restd_server_t *server, const char *key) { return server->options->getstr(server->options, key, false); } /*-------------------------------------------------------------------------- * * Retrieve server option in integer format. */ int restd_server_get_option_int(restd_server_t *server, const char *key) { char *value = restd_server_get_option(server, key); return (value) ? atoi(value) : 0; } /*--------------------------------------------------------------------------*/ static void close_server(restd_server_t *server) { DEBUG("Closing server."); if (server->notify_buffer) { bufferevent_free(server->notify_buffer); server->notify_buffer = NULL; } if (server->listener) { evconnlistener_free(server->listener); server->listener = NULL; } if (server->thread) { void *retval = NULL; DEBUG("Waiting server's last loop to finish."); pthread_join(*(server->thread), &retval); free(retval); free(server->thread); server->thread = NULL; } INFO("Server closed."); } /*--------------------------------------------------------------------------*/ static void *server_loop(void *instance) { restd_server_t *server = (restd_server_t *)instance; int *retval = NEW_OBJECT(int); DEBUG("Loop start\n"); event_base_loop(server->evbase, 0); DEBUG("Loop finished\n"); *retval = (event_base_got_break(server->evbase)) ? -1 : 0; return retval; } /*--------------------------------------------------------------------------*/ static void libevent_log_cb(int severity, const char *msg) { switch (severity) { case _EVENT_LOG_MSG: { INFO("%s", msg); break; } case _EVENT_LOG_WARN: { WARN("%s", msg); break; } case _EVENT_LOG_ERR: { ERROR("%s", msg); break; } default: { DEBUG("%s", msg); break; } } } /*-------------------------------------------------------------------------- * * If there's no event, loopbreak or loopexit call won't work until one more * event arrived. So we use eventfd as a internal notification channel to let * server get out of the loop without waiting for an event. */ static int notify_loopexit(restd_server_t *server) { uint64_t x = 0; return bufferevent_write(server->notify_buffer, &x, sizeof(uint64_t)); } /*--------------------------------------------------------------------------*/ static void notify_cb(struct bufferevent *buffer, void *userdata) { restd_server_t *server = (restd_server_t *)userdata; event_base_loopexit(server->evbase, NULL); DEBUG("Existing loop."); } /*--------------------------------------------------------------------------*/ static const char *file_mime_lookup(const char *path) { const struct mimetype_s *m = &g_mime_types[0]; const char *e; while (m->extn) { e = &path[strlen(path) - 1]; while (e >= path) { if ((*e == '.' || *e == '/') && !strcasecmp(&e[1], m->extn)) return m->mime; e--; } m++; } return "application/octet-stream"; } /*--------------------------------------------------------------------------*/ static bool contain(const char *src, const char *dest, int len) { int len_src, len_dest; int i = 0; len_src = strlen(src); if (len_src < len) return false; len_dest = strlen(dest); if (len_dest < len) return false; while (i < len) { if (src[i] != dest[i]) return false; i++; } return true; } /*--------------------------------------------------------------------------*/ void rest_request_cb(struct evhttp_request *req, void *arg) { restd_server_t *server = (restd_server_t *)arg; char *root_path; restd_resp_t *response; response = restd_resp_new(); response->request = req; #if 0 print_request_info(req); #endif const char *request_path = evhttp_request_get_uri(req); qlist_t *hooks = server->hooks; //int reason = RESTD_ERROR_PATH_NOT_FOUND; qlist_obj_t obj; bzero((void *)&obj, sizeof(qlist_obj_t)); while (hooks->getnext(hooks, &obj, false) == true) { restd_hook_t *hook = (restd_hook_t *)obj.data; if (hook->cb) { //printf("==== call_hooks: method: %d - %d \n", hook->method, evhttp_request_get_command(req)); //printf("==== call_hooks: path: %s - %s\n", hook->path, request_path); //printf("==== HOOK FOUND !!!!\n"); if (hook->method != evhttp_request_get_command(req)) { //printf("==== Hook found but method failed -> next.\n"); //reason = RESTD_ERROR_METHOD_NOT_ALLOWED; continue; } if (manage_hook(hook, response, request_path) == true) { restd_resp_free(response); return; } } } restd_resp_free(response); // No Hook Found check if it's a real file into document root. root_path = restd_server_get_option(server, "server.root_path"); if ((root_path != NULL) && (strlen(root_path) != 0)) { int fd; char buf[1024] = ""; qstrcatf(buf, "%s%s", root_path, request_path); fd = open(buf, 0); if (fd != -1) { restd_http_response_from_file(req, 200, fd, file_mime_lookup(buf)); return; } else { // TODO 404 } } #if 0 // TODO if (conn->server->error_handler != NULL) { return conn->server->error_handler(reason, conn, NULL); } else #endif { evhttp_send_reply(req, 500, "Internal Error", NULL); } } /*--------------------------------------------------------------------------*/ void print_request_info(struct evhttp_request *req) { const char *cmdtype; struct evkeyvalq *headers; struct evkeyval *header; printf("request.\n"); switch (evhttp_request_get_command(req)) { case EVHTTP_REQ_GET: cmdtype = "GET"; break; case EVHTTP_REQ_POST: cmdtype = "POST"; break; case EVHTTP_REQ_HEAD: cmdtype = "HEAD"; break; case EVHTTP_REQ_PUT: cmdtype = "PUT"; break; case EVHTTP_REQ_DELETE: cmdtype = "DELETE"; break; case EVHTTP_REQ_OPTIONS: cmdtype = "OPTIONS"; break; case EVHTTP_REQ_TRACE: cmdtype = "TRACE"; break; case EVHTTP_REQ_CONNECT: cmdtype = "CONNECT"; break; case EVHTTP_REQ_PATCH: cmdtype = "PATCH"; break; default: cmdtype = "unknown"; break; } printf("Received a %s request for %s\nHeaders:\n", cmdtype, evhttp_request_get_uri(req)); headers = evhttp_request_get_input_headers(req); for (header = headers->tqh_first; header; header = header->next.tqe_next) { printf(" %s: %s\n", header->key, header->value); } } /*--------------------------------------------------------------------------*/ void restd_http_response_from_file(struct evhttp_request *req, int code, int fd, const char *content_type) { struct evbuffer *resp_buf; struct evkeyvalq *resp_headers; ev_off_t len; resp_buf = evhttp_request_get_output_buffer(req); resp_headers = evhttp_request_get_output_headers(req); len = lseek(fd, 0, SEEK_END); evbuffer_add_file(resp_buf, fd, 0, len); evhttp_add_header(resp_headers, "Content-Type", content_type); evhttp_send_reply(req, code, NULL, resp_buf); } /*--------------------------------------------------------------------------*/ bool manage_hook(restd_hook_t *hook, restd_resp_t *response, const char *request_path) { if ((hook->path != NULL) && (request_path != NULL)) { int i = 0; int pos = -1; while (hook->path[i]) { if (hook->path[i] == ':') pos = i; i++; } if (pos != -1 && contain(hook->path, request_path, pos)) { const char *buffer = &request_path[pos]; // printf("buffer: <%s>\n", buffer); // TODO conn->id = atoi(buffer); hook->cb(response, hook->userdata); return true; } else { int rett = strcmp(hook->path, request_path); if (rett == 0) { hook->cb(response, hook->userdata); return true; } } } return false; }