/* * Copyright (C) 2014 * Olivier Gayot * Bartosz Golaszewski * Olivier Gayot * * 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 2 of the License, or * (at your option) 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This software is based on Andy Green's test-server available here * http://git.libwebsockets.org/cgi-bin/cgit/libwebsockets/ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "misc.h" #include "log.h" #define DEFAULT_PORT 8080 #define DEFAULT_PAGES_DIR "/srv/http/cao/" #define DEFAULT_HOMEPAGE "index.html" #define DEFAULT_CONFIG_FILE "/etc/caod/config.cfg" #define DEFAULT_SAMPLING_RATE 1 /* one ms */ #define DEFAULT_CLIENT_REFRESH_RATE 100 /* in ms */ #define USER_DATA_BUFSIZE 1048576 struct per_session_data_cao_prot { bool handshake_ok: 1; /* * libwebsockets requires a padding before and after the buffer. * Since we want to be able to use sizeof(buffer) anyway, we will * make a packed structure containing the buffer and its padding. */ struct { unsigned char _pre_padding[LWS_SEND_BUFFER_PRE_PADDING]; unsigned char buffer[USER_DATA_BUFSIZE]; unsigned char _post_padding[LWS_SEND_BUFFER_POST_PADDING]; }; unsigned short len; struct { unsigned short sps; /* snaps per second */ } params; }; struct mime_type { char *ext; size_t ext_len; char *type; }; struct pws_event { bool set; int probe_id; int state; }; static bool daemonize_g = false; static int verbosity_g = 0; static volatile bool asked_to_quit_g = false; static const char *pages_dir_g = DEFAULT_PAGES_DIR; static const char *homepage_g = DEFAULT_HOMEPAGE; static const char *config_file_g = DEFAULT_CONFIG_FILE; static struct lws_context *context_g; #define MIME_EXT_ARGS(_ext) _ext, (sizeof(_ext) - 1) static struct mime_type mimes_g[] = { { MIME_EXT_ARGS(".ico"), "image/x-icon", }, { MIME_EXT_ARGS(".png"), "image/png" }, { MIME_EXT_ARGS(".html"), "text/html" }, { MIME_EXT_ARGS(".css"), "text/css" }, { MIME_EXT_ARGS(".js"), "application/javascript" }, #define SHORTEST_MIME_EXT 3 /* size of the shortest extension (incl. the dot) */ }; #undef MIME_EXT_ARGS /* return a string describing the mime type if supported, otherwise NULL */ static const char *get_mimetype(const char *filename) { int n, i; n = strlen(filename); if (n < SHORTEST_MIME_EXT + 1) return NULL; for (i = 0; i < countof(mimes_g); ++i) { const struct mime_type *mimep = &mimes_g[i]; if (!strcmp(&filename[n - mimep->ext_len], mimep->ext)) { return mimep->type; } } return NULL; } static int callback_http_only(struct lws *wsi, enum lws_callback_reasons reason, void *user UNUSED_PARAM, void *in, size_t len) { const char *mimetype; static char path[PATH_MAX]; static char real_path[PATH_MAX]; size_t path_len; size_t def_path_len; switch (reason) { case LWS_CALLBACK_HTTP: if (len < 1) { lws_return_http_status(wsi, HTTP_STATUS_BAD_REQUEST, NULL); return -1; } /* serve either the specified page or the default one */ if (strcmp(in, "/") != 0) { snprintf(path, sizeof(path), "%s/%s", pages_dir_g, (char *)in); } else { snprintf(path, sizeof(path), "%s/%s", pages_dir_g, homepage_g); } /* * avoid a security issue: "jail" inside pages_dir_g * XXX: beware, a race condition might occur between realpath() and * open() which is called by lwss_serve_http_file() */ if (realpath(path, real_path) == NULL) { lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); return -1; } path_len = strlen(real_path); def_path_len = strlen(pages_dir_g); if (path_len <= def_path_len || strncmp(real_path, pages_dir_g, def_path_len) != 0) { lws_return_http_status(wsi, HTTP_STATUS_FORBIDDEN, NULL); return -1; } /* check if the mimetype if supported */ mimetype = get_mimetype(path); if (mimetype == NULL) { caod_err("Unknown mimetype for %s", path); lws_return_http_status(wsi, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, NULL); return -1; } if (lws_serve_http_file(wsi, path, mimetype, NULL, 0)) { /* * close the socket if either an error occured or the file has * been sent completely */ return -1; } break; case LWS_CALLBACK_HTTP_BODY: break; case LWS_CALLBACK_HTTP_BODY_COMPLETION: break; case LWS_CALLBACK_HTTP_FILE_COMPLETION: break; case LWS_CALLBACK_HTTP_WRITEABLE: break; case LWS_CALLBACK_FILTER_NETWORK_CONNECTION: break; default: break; } return 0; } static void set_shunt_vals(int *vals) { FILE *shfd; int numvals = 8; int i; FILE *chfd; char channels[8]; int hwind = 1; ssize_t rd; chfd = fopen("/etc/caod/channels", "r"); if (!chfd) goto err; shfd = fopen("/etc/caod/shunt_vals", "w"); if (!shfd) goto err; for (i = 0; i < numvals; i++) { if (vals[i] > 0) { fprintf(shfd, "%d\n", vals[i]); } } fclose(shfd); rd = fread(channels, 1, sizeof(channels), chfd); if (rd != sizeof(channels)) { goto err; } fclose(chfd); for (i = 0; i < numvals; i++) { if (channels[i] == 'I') { char cmd[256]; snprintf(cmd, sizeof(cmd), "/root/cao-suite/scripts/cao_ctl.py set_shunt %d %d %d", hwind, i+1, vals[i]); caod_info("Executing: %s", cmd); system(cmd); hwind++; } else if (channels[i] == 'T') { hwind++; } } sync(); return; err: caod_err("Error setting shunt values"); } static int callback_cao_prot(struct lws *wsi UNUSED_PARAM, enum lws_callback_reasons reason, void *user, void *in UNUSED_PARAM, size_t len UNUSED_PARAM) { struct per_session_data_cao_prot *user_data = user; switch (reason) { case LWS_CALLBACK_ESTABLISHED: user_data->params.sps = 0; user_data->handshake_ok = false; break; case LWS_CALLBACK_CLOSED: break; case LWS_CALLBACK_SERVER_WRITEABLE: break; case LWS_CALLBACK_RECEIVE: set_shunt_vals(in); break; case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION: break; default: break; } return 0; } static struct lws_context_creation_info info_g = { .port = DEFAULT_PORT, .iface = NULL, /* list the protocols available */ .protocols = (struct lws_protocols[]) { { .name = "http-only", /* name */ .callback = callback_http_only, .per_session_data_size = 0, }, { .name = "game", .callback = callback_cao_prot, .per_session_data_size = sizeof(struct per_session_data_cao_prot), }, { /* empty field needed at the end */ .name = NULL, .callback = NULL, .per_session_data_size = 0, }, }, .options = 0, }; /* display an usage message and exit */ NORETURN static void usage(const char *progname) { fprintf(stderr, "Usage: %s [OPTIONS]\n" "\n" "-p - override the default port (%d),\n" "-h - display this help and exit,\n" "-d - fork in the background,\n" "-v - increase verbosity (1-info, 2-debug),\n" "-w - set the path to the WWW directory (default: '%s'),\n" "-H - set the filename of the homepage in the www directory\n" "-c - specify a custom path to the config file (default: '%s')," " (default: '%s').\n", progname, DEFAULT_PORT, DEFAULT_PAGES_DIR, DEFAULT_HOMEPAGE, DEFAULT_CONFIG_FILE); exit(EX_USAGE); } static void errmsg_and_usage(const char *progname, const char *fmt, ...) { va_list va; va_start(va, fmt); vfprintf(stderr, fmt, va); fprintf(stderr, "\n"); va_end(va); usage(progname); } /* * search options passed to the program and handle them * argc will be modified accordingly * argv will be returned shifted accordingly */ static char **parse_opt(int *argc, char *argv[]) { int opt; char *errptr; while ((opt = getopt(*argc, argv, "v:hdp:w:H:c:")) != -1) { switch (opt) { case 'p': /* change the default port */ info_g.port = strtol(optarg, &errptr, 10); if (*errptr != '\0') { usage(argv[0]); } break; case 'h': /* display the help (and exit) */ usage(argv[0]); break; case 'd': /* fork on the background */ daemonize_g = true; break; case 'v': /* increase verbosity */ if ((strcmp(optarg, "1") == 0) || (strcmp(optarg, "2") == 0)) verbosity_g = atoi(optarg); else errmsg_and_usage(argv[0], "Invalid verbosity level\n"); break; case 'w': if (optarg[0] != '/') errmsg_and_usage(argv[0], "Must specify an absolute path for -w\n"); pages_dir_g = optarg; break; case 'H': homepage_g = optarg; break; case 'c': config_file_g = optarg; break; default: /* noreturn */ usage(argv[0]); } } *argc -= optind; return &(argv[optind]); } static void handle_signal(int sig) { switch (sig) { case SIGINT: case SIGTERM: asked_to_quit_g = true; break; } } static int callback_http_on_bug(struct lws *wsi, enum lws_callback_reasons reason, void *user UNUSED_PARAM, void *in UNUSED_PARAM, size_t len) { char path[256]; memset(path, 0, sizeof(path)); snprintf(path, sizeof(path), "%s/error.html", pages_dir_g); switch (reason) { case LWS_CALLBACK_HTTP: if (len < 1) { lws_return_http_status(wsi, HTTP_STATUS_BAD_REQUEST, NULL); return -1; } if (lws_serve_http_file(wsi, path, "text/html", NULL, 0)) { /* * close the socket if either an error occured or the file has * been sent completely */ return -1; } break; default: break; } return 0; } static struct lws_context_creation_info info_bug = { .port = DEFAULT_PORT, .iface = NULL, /* list the protocols available */ .protocols = (struct lws_protocols[]) { { .name = "http-only", /* name */ .callback = callback_http_on_bug, .per_session_data_size = 0, }, { /* empty field needed at the end */ .name = NULL, .callback = NULL, .per_session_data_size = 0, }, }, .options = 0, }; /* * If an error happened - let this function run eternally to inform users * about the bug via HTTP on default port. */ static void bug_on(void) { struct lws_context *context; if ((context = lws_create_context(&info_bug)) == NULL) { caod_err("lws init failed"); abort(); } caod_err("Entering bug_on state..."); while (!asked_to_quit_g) { if (lws_service(context, 5) < 0) { caod_err("lws_service() returned with an error"); } } } /* * The general idea is to read the configuration and create the data model, * then create a pipe and fork. The child process will then try to stick to * the given sampling rate and perform the measurements on time, feeding all * the results into the pipe, while the parent will read from it, store these * values in a buffer and send them periodically to the client. * * The buffer size should be (client_refresh_rate / sampling_rate). The buffer * is only allocated in the parent process, although both processes share * the rest of the data model. */ int main(int argc, char *argv[]) { const char *progname = argv[0]; int ret = EXIT_SUCCESS; int dbglvl; argv = parse_opt(&argc, argv); /* XXX argc and argv have been shifted */ /* no argument are accepted for now on */ if (argc > 0) { usage(progname); } if (daemonize_g && lws_daemonize("/tmp/.caod-lock") != 0) { caod_err("failed to daemonize"); return -1; } caod_setup_logs(); dbglvl = LLL_ERR | LLL_WARN | LLL_NOTICE; switch (verbosity_g) { case 2: dbglvl |= LLL_DEBUG; /* Fall through. */ case 1: dbglvl |= LLL_INFO; } lws_set_log_level(dbglvl, &caod_log_hook); signal(SIGINT, &handle_signal); signal(SIGTERM, &handle_signal); if ((context_g = lws_create_context(&info_g)) == NULL) { caod_err("lws init failed"); ret = EXIT_FAILURE; return ret; } while (!asked_to_quit_g) { if (lws_service(context_g, 5) < 0) { caod_err("lws_service() returned with an error"); ret = EXIT_FAILURE; break; } } lws_context_destroy(context_g); return ret; }