view mapsearch.c @ 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
children 7ec862ed6514
line wrap: on
line source

/*
 * 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;
}