changeset 1784:9ee269ae165d

Initial import of map search WebSockets server code. Does not actually do any searching yet.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 29 Oct 2017 04:52:50 +0200
parents fe17380576ac
children 7ec862ed6514
files Makefile Makefile.gen mapsearch.c
diffstat 3 files changed, 637 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Oct 29 03:08:49 2017 +0200
+++ b/Makefile	Sun Oct 29 04:52:50 2017 +0200
@@ -23,6 +23,10 @@
 LIBPNG_CFLAGS ?= $(shell pkg-config --cflags libpng)
 LIBPNG_LDFLAGS ?= $(shell pkg-config --libs libpng)
 
+HAVE_LIBWEBSOCKETS ?= $(shell pkg-config --atleast-version=2 libwebsockets && echo "yes")
+LIBWEBSOCKETS_CFLAGS ?= $(shell pkg-config --cflags libwebsockets)
+LIBWEBSOCKETS_LDFLAGS ?= $(shell pkg-config --libs libwebsockets)
+
 
 ###
 ### Unix targets
--- a/Makefile.gen	Sun Oct 29 03:08:49 2017 +0200
+++ b/Makefile.gen	Sun Oct 29 04:52:50 2017 +0200
@@ -19,6 +19,7 @@
 MAP2PPM_BIN=$(BINPATH)map2ppm$(EXEEXT)
 COMBINE_BIN=$(BINPATH)combine$(EXEEXT)
 MAPSTATS_BIN=$(BINPATH)mapstats$(EXEEXT)
+MAPSEARCH_BIN=$(BINPATH)mapsearch$(EXEEXT)
 
 
 MAP_FILES=votk.html votk.map \
@@ -42,6 +43,11 @@
 	$(addprefix $(MAP_PATH),$(filter %.html,$(MAP_FILES)))
 
 
+ifeq ($(HAVE_LIBWEBSOCKETS),yes)
+MAPSEARCH_CFLAGS += -D_POSIX_C_SOURCE=200112 $(LIBWEBSOCKETS_CFLAGS)
+MAPSEARCH_LDFLAGS += $(LIBWEBSOCKETS_LDFLAGS)
+endif
+
 ifeq ($(HAVE_LIBPNG),yes)
 MAP2PPM_CFLAGS += -DHAVE_LIBPNG=1 $(LIBPNG_CFLAGS)
 MAP2PPM_LDFLAGS += $(LIBPNG_LDFLAGS)
@@ -61,6 +67,10 @@
 	@echo " LINK $@"
 	@$(CC) $(CFLAGS) -o $@ $< $(LIBMAPUTILS_OBJ) $(MAP2PPM_CFLAGS) $(MAP2PPM_LDFLAGS) $(THLIBS_A) $(LDFLAGS)
 
+$(MAPSEARCH_BIN): mapsearch.c $(LIBMAPUTILS_OBJ) $(THLIBS_A) $(THLIBS_DEP)
+	@echo " LINK $@"
+	@$(CC) $(CFLAGS) -o $@ $< $(LIBMAPUTILS_OBJ) $(LIBLOCFILE_OBJ) $(THLIBS_A) $(MAPSEARCH_CFLAGS) $(MAPSEARCH_LDFLAGS) $(LDFLAGS)
+
 
 $(BINPATH)%$(EXEEXT): %.c $(LIBMAPUTILS_OBJ) $(LIBLOCFILE_OBJ) $(THLIBS_A) $(THLIBS_DEP)
 	@echo " LINK $@"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mapsearch.c	Sun Oct 29 04:52:50 2017 +0200
@@ -0,0 +1,623 @@
+/*
+ * PupuMaps Search WebSockets server
+ * Written by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
+ * (C) Copyright 2017 Tecnic Software productions (TNSP)
+ */
+#include "th_args.h"
+#include "th_config.h"
+#include "liblocfile.h"
+#include "libmaputils.h"
+#include <libwebsockets.h>
+
+
+/* Default settings etc. constants
+ */
+#define SET_MAX_MAPS        16  // Maximum number of maps allowed to be loaded
+#define SET_MAX_LISTEN      4   // Maximum number of interfaces to listen
+#define SET_MAX_MATCHES     32  // Maximum number of match results per query
+
+
+// List of default SSL/TLS ciphers to use/allowed
+#define SET_DEF_CIPHERS	\
+        "ECDHE-ECDSA-AES256-GCM-SHA384:"	\
+        "ECDHE-RSA-AES256-GCM-SHA384:"		\
+        "DHE-RSA-AES256-GCM-SHA384:"		\
+        "ECDHE-RSA-AES256-SHA384:"		\
+        "HIGH:!aNULL:!eNULL:!EXPORT:"		\
+        "!DES:!MD5:!PSK:!RC4:!HMAC_SHA1:"	\
+        "!SHA1:!DHE-RSA-AES128-GCM-SHA256:"	\
+        "!DHE-RSA-AES128-SHA256:"		\
+        "!AES128-GCM-SHA256:"			\
+        "!AES128-SHA256:"			\
+        "!DHE-RSA-AES256-SHA256:"		\
+        "!AES256-GCM-SHA384:"			\
+        "!AES256-SHA256"
+
+
+/* Structure that holds information about one listen interface
+ */
+typedef struct
+{
+    char *interface;         // Listen interface (* = listen all)
+    int port;                // Port number
+
+    BOOL useIPv6;
+    BOOL useSSL;             // Use SSL/TLS?
+    char *sslCertFile,       // Certificate file
+         *sslKeyFile,        // Key file
+         *sslCAFile;         // CA file
+
+    struct lws_vhost *vhost; // LWS vhost info
+} MAPListenerCtx;
+
+
+/* Structure for holding information about one map and its locations
+ */
+typedef struct
+{
+    char *mapFilename;       // Filename of the map data
+    LocFileInfo locFile;     // Location file info struct
+    MapBlock *map;           // Map data, when loaded
+    MapLocations loc;        // Map locations, when loaded
+} MAPInfoCtx;
+
+
+/* Options
+ */
+MAPInfoCtx optMaps[SET_MAX_MAPS];
+MAPListenerCtx *optListenTo[SET_MAX_LISTEN];
+int     optNMaps = 0;
+int     optNListenTo = 0;
+char    *optCleanChars = " *@?%C";
+char    *optSSLCipherList = SET_DEF_CIPHERS;
+struct lws_context *setLWSContext = NULL;
+int     optWorldXC = 0, optWorldYC = 0;
+
+
+/* Arguments
+ */
+static const th_optarg optList[] =
+{
+    { 0, '?', "help",          "Show this help and be so very much verbose that it almost hurts you", OPT_NONE },
+    { 1, 'v', "verbose",       "Be more verbose", OPT_NONE },
+    { 2, 'l', "listen",        "Listen to interface (see below)", OPT_ARGREQ },
+    { 3,   0, "ssl-ciphers",   "Specify list of SSL/TLS ciphers", OPT_ARGREQ },
+    { 4,   0, "clean-chars",   "String of characters to 'clean up' from map blocks.", OPT_ARGREQ },
+    { 5, 'w', "world-origin",  "Specify the world origin <x, y> which map offsets relate to.", OPT_ARGREQ },
+};
+
+static const int optListN = sizeof(optList) / sizeof(optList[0]);
+
+
+void argShowHelp(void)
+{
+    th_print_banner(stdout, th_prog_name, "[options] <map spec>");
+    th_args_help(stdout, optList, optListN, 0);
+
+    fprintf(stdout,
+        "\n"
+        "Listening interface(s) are specified with following syntax:\n"
+        "-l \"[@]<interface/IP/host>:<port>[=<SSL/TLS spec>]\"\n"
+        "\n"
+        "IPv6 addresses should be specified with the square bracket notation.\n"
+        "To force IPv6, use IPv6 address [] or prefix spec with @. In order to\n"
+        "listen to all interfaces, you can specify an asterisk (*) as host. Example:\n"
+        "\n"
+        "-l *:3491 -l *:3492=<ssl_cert_file.crt>:<ssl_key_file.key>:<ca_file.crt>\n"
+        "\n"
+        "Would listen for normal WebSocket (ws://) connections on port 3491 and for\n"
+        "secure SSL/TLS WebSocket (wss://) connections on port 3492 of all interfaces.\n"
+        "\n"
+        "Maps and location files for each map are specified as follows:\n"
+        "<filename.map>:<locfilename.loc>:<map/continent name>[:<world x-offset>:<world y-offset>]\n"
+    );
+}
+
+
+BOOL mapParseCoordPair(const char *str, int *xc, int *yc)
+{
+    char *piece, *tmp, *fmt = th_strdup(str);
+    BOOL ret = FALSE;
+
+    if ((piece = strchr(fmt, ':')) == NULL)
+        goto err;
+    *piece++ = 0;
+
+    tmp = th_strdup_trim(fmt, TH_TRIM_BOTH);
+    *xc = atoi(tmp);
+    th_free(tmp);
+
+    tmp = th_strdup_trim(piece, TH_TRIM_BOTH);
+    *yc = atoi(tmp);
+    th_free(tmp);
+
+    ret = TRUE;
+
+err:
+    th_free(fmt);
+    return ret;
+}
+
+
+BOOL mapParseMapSpec(const char *str, MAPInfoCtx *info)
+{
+    char *piece, *start, *fmt = th_strdup(str);
+    BOOL ret = FALSE;
+
+    // Check for map filename end
+    if ((piece = strchr(fmt, ':')) == NULL)
+        goto err;
+    *piece++ = 0;
+
+    info->mapFilename = th_strdup_trim(fmt, TH_TRIM_BOTH);
+    start = piece;
+
+    // Check for loc filename end
+    if ((piece = strchr(start, ':')) == NULL)
+        goto err;
+    *piece++ = 0;
+
+    info->locFile.filename = th_strdup_trim(start, TH_TRIM_BOTH);
+    start = piece;
+
+    // Check for world x-offset separator
+    if ((piece = strchr(start, ':')) != NULL)
+        *piece++ = 0;
+
+    info->locFile.continent = th_strdup_trim(start, TH_TRIM_BOTH);
+
+    // Get world X/Y offsets, if any
+    if (piece != NULL &&
+        !mapParseCoordPair(piece, &info->locFile.x, &info->locFile.y))
+        goto err;
+
+    ret = TRUE;
+
+err:
+    th_free(fmt);
+    return ret;
+}
+
+
+MAPListenerCtx *mapNewListenCtx(void)
+{
+    return th_malloc0(sizeof(MAPListenerCtx));
+}
+
+
+void mapFreeListenCtxR(MAPListenerCtx *ctx)
+{
+    if (ctx != NULL)
+    {
+        th_free(ctx->interface);
+        th_free(ctx->sslCertFile);
+        th_free(ctx->sslKeyFile);
+        th_free(ctx->sslCAFile);
+    }
+}
+
+
+void mapFreeListenCtx(MAPListenerCtx *ctx)
+{
+    mapFreeListenCtxR(ctx);
+    th_free(ctx);
+}
+
+
+MAPListenerCtx *mapParseListenerSpec(const char *cfmt)
+{
+    char *start, *end, *flags, *port = NULL, *interface, *fmt = th_strdup(cfmt);
+    BOOL ret = FALSE;
+    MAPListenerCtx *ctx;
+
+    if ((ctx = mapNewListenCtx()) == NULL)
+        goto out;
+
+    interface = fmt;
+    if (*interface == '@')
+    {
+        interface++;
+        ctx->useIPv6 = TRUE;
+    }
+
+    if (*interface == '[')
+    {
+        // IPv6 IP address is handled in a special case
+        interface++;
+        if ((end = strchr(interface, ']')) == NULL)
+        {
+            THERR("Invalid IPv6 IP address '%s'.\n", cfmt);
+            goto out;
+        }
+        *end++ = 0;
+        ctx->useIPv6 = TRUE;
+    }
+    else
+    {
+        end = strchr(interface, ':');
+    }
+
+    // Find port number separator
+    if (end == NULL || *end != ':')
+    {
+        THERR("Missing listening port in '%s'.\n", cfmt);
+        goto out;
+    }
+    *end++ = 0;
+    start = end;
+
+    // Check for '=flags' at the end
+    if ((flags = strchr(start, '=')) != NULL)
+        *flags++ = 0;
+
+    // Get the interface name
+    ctx->interface = th_strdup_trim(interface, TH_TRIM_BOTH);
+    if (strcmp(ctx->interface, "*") == 0)
+    {
+        th_free(ctx->interface);
+        ctx->interface = NULL;
+    }
+
+    // Get port number
+    if ((port = th_strdup_trim(start, TH_TRIM_BOTH)) == NULL)
+    {
+        THERR("Missing listening port in '%s'.\n", cfmt);
+        goto out;
+    }
+    ctx->port = atoi(port);
+
+    // Parse the SSL/TLS spec, if any
+    if (flags != NULL)
+    {
+        char *cstart = flags, *cend;
+        if ((cend = strchr(cstart, ':')) == NULL)
+        {
+            THERR("Invalid SSL/TLS spec '%s'\n", flags);
+            goto out;
+        }
+        *cend++ = 0;
+
+        ctx->sslCertFile = th_strdup_trim(cstart, TH_TRIM_BOTH);
+        cstart = cend;
+
+        if ((cend = strchr(cend, ':')) == NULL)
+        {
+            THERR("Invalid SSL/TLS spec '%s'\n", flags);
+            goto out;
+        }
+        *cend++ = 0;
+
+        ctx->sslKeyFile = th_strdup_trim(cstart, TH_TRIM_BOTH);
+        ctx->sslCAFile = th_strdup_trim(cend, TH_TRIM_BOTH);
+        ctx->useSSL = TRUE;
+    }
+
+    // Check for duplicates
+    for (int n = 0; n < optNListenTo; n++)
+    {
+        MAPListenerCtx *chk = optListenTo[n];
+        if ((
+            (ctx->interface == NULL && chk->interface == NULL) ||
+            (ctx->interface != NULL && chk->interface != NULL && strcmp(ctx->interface, chk->interface) == 0)) &&
+            chk->port == ctx->port)
+        {
+            THERR("Duplicate listener spec (%s:%d)\n",
+                ctx->interface != NULL ? ctx->interface : "*", ctx->port);
+            goto out;
+        }
+    }
+    ret = TRUE;
+
+out:
+    th_free(fmt);
+    th_free(port);
+
+    if (ret)
+        return ctx;
+
+    mapFreeListenCtx(ctx);
+    return NULL;
+}
+
+
+BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
+{
+    switch (optN)
+    {
+    case 0:
+        argShowHelp();
+        exit(0);
+        break;
+
+    case 1:
+        th_verbosityLevel++;
+        break;
+
+    case 2:
+        if (optNListenTo < SET_MAX_LISTEN)
+        {
+            MAPListenerCtx *ctx;
+            if ((ctx = mapParseListenerSpec(optArg)) != NULL)
+                optListenTo[optNListenTo++] = ctx;
+            else
+                return FALSE;
+        }
+        else
+        {
+            THERR("Maximum number of listener specs already specified.\n");
+            return FALSE;
+        }
+        break;
+
+    case 3:
+        optSSLCipherList = optArg;
+        break;
+
+    case 4:
+        optCleanChars = optArg;
+        break;
+
+    case 5:
+        if (!mapParseCoordPair(optArg, &optWorldXC, &optWorldYC))
+        {
+            THERR("Invalid world origin coordinates '%s'.\n", optArg);
+            return FALSE;
+        }
+        break;
+
+    default:
+        THERR("Unknown option '%s'.\n", currArg);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+
+BOOL argHandleFile(char *currArg)
+{
+    if (optNMaps < SET_MAX_MAPS)
+    {
+        if (!mapParseMapSpec(currArg, &optMaps[optNMaps]))
+        {
+            THERR("Invalid map spec '%s'.\n", currArg);
+            return FALSE;
+        }
+
+        optNMaps++;
+        return TRUE;
+    }
+    else
+    {
+        THERR("Maximum number of map specs already specified.\n");
+        return FALSE;
+    }
+}
+
+
+void mapSigHandler(uv_signal_t *watcher, int signum)
+{
+    (void) signum;
+
+    THERR("Signal %d caught, exiting...\n", watcher->signum);
+
+    switch (watcher->signum)
+    {
+        case SIGTERM:
+        case SIGINT:
+            break;
+
+        default:
+            signal(SIGABRT, SIG_DFL);
+            abort();
+            break;
+    }
+
+    lws_libuv_stop(setLWSContext);
+}
+
+
+
+
+int mapLWSCallback(struct lws *wsi,
+    enum lws_callback_reasons reason,
+    void *user, void *in, size_t len)
+{
+    (void) user;
+
+    switch (reason)
+    {
+        case LWS_CALLBACK_CLIENT_WRITEABLE:
+            THPRINT(0, "Connection established.\n");
+            break;
+
+        case LWS_CALLBACK_RECEIVE:
+            {
+            lws_close_reason(wsi, LWS_CLOSE_STATUS_NOSTATUS, NULL, 0);
+            }
+            break;
+        default:
+            break;
+    }
+
+    return 0;
+}
+
+
+static const struct lws_extension mapLWSExtensions[] =
+{
+    {
+       "permessage-deflate",
+        lws_extension_callback_pm_deflate,
+        "permessage-deflate"
+    },
+    {
+        "deflate-frame",
+        lws_extension_callback_pm_deflate,
+        "deflate_frame"
+    },
+    { NULL, NULL, NULL }
+};
+
+
+static const struct lws_protocols mapLWSProtocols[] =
+{
+    { "default", &mapLWSCallback, 0, 0, 0, NULL },
+    { NULL, NULL, 0, 0, 0, NULL }
+};
+
+
+int main(int argc, char *argv[])
+{
+
+    // Initialize
+    th_init("MapSearch", "Map Search WebSockets server", "0.1", NULL, NULL);
+    th_verbosityLevel = 0;
+
+    memset(&optMaps, 0, sizeof(optMaps));
+    memset(&optListenTo, 0, sizeof(optListenTo));
+
+    // Parse command line arguments
+    BOOL argsOK = th_args_process(argc, argv, optList, optListN, argHandleOpt, argHandleFile, 0);
+
+    if (!argsOK)
+        return -2;
+
+    if (optNMaps == 0)
+    {
+        THERR("No maps specified.\n");
+        goto exit;
+    }
+
+    if (optNListenTo == 0)
+    {
+        THERR("No listeners specified.\n");
+        goto exit;
+    }
+
+    // Load maps
+    THMSG(1, "Trying to load %d map specs ..\n", optNMaps);
+    for (int n = 0; n < optNMaps; n++)
+    {
+        MAPInfoCtx *info = &optMaps[n];
+        FILE *fh;
+
+        THMSG(1, "Map file '%s', map '%s', locations '%s' @ %d, %d\n",
+            info->locFile.continent,
+            info->mapFilename,
+            info->locFile.filename,
+            info->locFile.x, info->locFile.y);
+
+        if ((info->map = mapBlockParseFile(info->mapFilename, FALSE)) == NULL)
+        {
+            THERR("Could not read map data file '%s'.\n", info->mapFilename);
+            goto exit;
+        }
+
+        if ((fh = fopen(info->locFile.filename, "rb")) == NULL)
+        {
+            THERR("Could not open location file '%s' for reading.\n",
+                info->locFile.filename);
+            goto exit;
+        }
+
+        if (!locParseLocStream(fh, &info->locFile, &(info->loc), info->locFile.x, info->locFile.y))
+        {
+            fclose(fh);
+            goto exit;
+        }
+
+        fclose(fh);
+    }
+
+    // Initialize libwebsockets and create context
+    THMSG(1, "Creating libwebsockets context.\n");
+
+    MAPListenerCtx *ctx = optListenTo[0];
+    struct lws_context_creation_info info;
+    memset(&info, 0, sizeof(info));
+
+    info.port = ctx->port;
+    info.iface = ctx->interface;
+    info.max_http_header_pool = 16;
+    info.options =
+        LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
+	LWS_SERVER_OPTION_VALIDATE_UTF8 |
+	LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
+	LWS_SERVER_OPTION_LIBUV; // do we need this?
+
+    info.protocols = mapLWSProtocols;
+    info.extensions = mapLWSExtensions;
+    info.timeout_secs = 5;
+    info.ssl_cipher_list = optSSLCipherList;
+
+    if ((setLWSContext = lws_create_context(&info)) == NULL)
+    {
+        THERR("libwebsocket initialization failed.\n");
+        goto exit;
+    }
+
+    // Create listener LWS vhosts ..
+    for (int n = 0; n < optNListenTo; n++)
+    {
+        ctx = optListenTo[n];
+
+        if (ctx->useSSL)
+        {
+            THMSG(1, "Listen to %s:%d (wss) [cert='%s', key='%s', ca='%s']\n",
+                ctx->interface != NULL ? ctx->interface : "*", ctx->port,
+                ctx->sslCertFile, ctx->sslKeyFile, ctx->sslCAFile);
+        }
+        else
+        {
+            THMSG(1, "Listen to %s:%d (ws)\n",
+                ctx->interface != NULL ? ctx->interface : "*", ctx->port);
+        }
+
+        info.port = ctx->port;
+        info.iface = ctx->interface;
+        if ((ctx->vhost = lws_create_vhost(setLWSContext, &info)) == NULL)
+        {
+            THERR("LWS vhost creation failed!\n");
+            goto exit;
+        }
+    }
+
+    // Set up signal handlers
+    THMSG(1, "Setting up signal handlers.\n");
+    lws_uv_sigint_cfg(setLWSContext, 1, mapSigHandler);
+
+    THMSG(1, "Waiting for connections...\n");
+    if (lws_uv_initloop(setLWSContext, NULL, 0))
+    {
+        THERR("lws_uv_initloop() failed.\n");
+        goto exit;
+    }
+
+    // Start running ..
+    lws_libuv_run(setLWSContext, 0);
+
+exit:
+    // Shutdown sequence
+    THMSG(1, "Server shutting down.\n");
+
+    if (setLWSContext != NULL)
+        lws_context_destroy(setLWSContext);
+
+    for (int n = 0; n < optNListenTo; n++)
+        mapFreeListenCtx(optListenTo[n]);
+
+    for (int n = 0; n < optNMaps; n++)
+    {
+        MAPInfoCtx *info = &optMaps[n];
+
+        th_free(info->mapFilename);
+        th_free(info->locFile.filename);
+        th_free(info->locFile.continent);
+
+        mapBlockFree(info->map);
+        locFreeMapLocations(&info->loc);
+    }
+
+    return 0;
+}