# HG changeset patch # User Matti Hamalainen # Date 1509245570 -7200 # Node ID 9ee269ae165d8778983e90d3225d26ba370388dc # Parent fe17380576accd2f7290a3656361ff640f8760c8 Initial import of map search WebSockets server code. Does not actually do any searching yet. diff -r fe17380576ac -r 9ee269ae165d Makefile --- 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 diff -r fe17380576ac -r 9ee269ae165d Makefile.gen --- 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 $@" diff -r fe17380576ac -r 9ee269ae165d mapsearch.c --- /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 + * (C) Copyright 2017 Tecnic Software productions (TNSP) + */ +#include "th_args.h" +#include "th_config.h" +#include "liblocfile.h" +#include "libmaputils.h" +#include + + +/* 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 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] "); + th_args_help(stdout, optList, optListN, 0); + + fprintf(stdout, + "\n" + "Listening interface(s) are specified with following syntax:\n" + "-l \"[@]:[=]\"\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=::\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" + "::[::]\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; +}