view src/mapsearch.c @ 2833:d0e186348cb2 default tip

Add mention of soft level limitation to 'Eightleg woods'.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 26 May 2024 20:33:53 +0300
parents e96e757ab01e
children
line wrap: on
line source

/*
 * PupuMaps Search WebSockets server
 * Programmed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 * (C) Copyright 2018-2023 Tecnic Software productions (TNSP)
 */
#include "th_args.h"
#include "th_datastruct.h"
#include "th_file.h"
#include "liblocfile.h"
#include "libmaputils.h"
#include <stdarg.h>
#include <libwebsockets.h>
#include <sys/types.h>
#include <signal.h>
#include <pwd.h>
#include <grp.h>
#include <math.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     64  // Maximum number of match results per query

// Define the static lws_write() buffer size
#define SET_LWS_BUF_SIZE    (256 * 1024) // 256kB probably enough for our purposes(tm)
#define SET_LWS_BUF_PAD     (((LWS_PRE / 16) + 1) * 16)


// 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)
    char *vhostname;         // Vhost name
    int port;                // Port number

    int ipvMode;             // Enable/disable IPv4/6 support for this listener

    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;


typedef struct
{
    char *map;
    int mx, my, wx, wy;
    LocMarker *marker;
    int nname;
} MAPMatch;


LocMarker **optMapLocations = NULL;
int     optNMapLocations = 0;

/* Options
 */
MAPInfoCtx optMaps[SET_MAX_MAPS];
int     optNMaps = 0;
MAPListenerCtx *optListenTo[SET_MAX_LISTEN];
int     optNListenTo = 0;
char    *optSSLCipherList = SET_DEF_CIPHERS;
struct lws_context *setLWSContext = NULL;
int     optWorldXC = 0, optWorldYC = 0;
char    *optTest = NULL;
int     optUID = -1, optGID = -1;
char    *optLogFilename = NULL;
char    *optDataPath = NULL;
FILE    *setLogFH = NULL;
int     optBenchmark = -1;

unsigned char *setLWSBuffer = NULL;


/* Arguments
 */
static const th_optarg optList[] =
{
    {  0, '?', "help",          "Show this help", 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 },
    {  5, 'w', "world-origin",  "Specify the world origin <x:y> coordinates "
    "to which the map offsets are relative to", OPT_ARGREQ },
    {  6, 'T', "test",          "Test search with given file input", OPT_ARGREQ },
    {  4, 'B', "benchmark",     "Run a benchmark on test input (-T option) for specified number cycles", OPT_ARGREQ },
    {  7, 'U', "uid",           "Run as UID", OPT_ARGREQ },
    {  8, 'G', "gid",           "Run as GID", OPT_ARGREQ },
    {  9, 'L', "log-file",      "Log to specified file", OPT_ARGREQ },
    { 10, 'D', "data-path",     "Path to data directory", 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, 80 - 2);

    fprintf(stdout,
        "\n"
        "Listening interface(s) are specified with following syntax:\n"
        "-l \"<interface/IP/host>:<port>[:no-ipv(4|6)][=<SSL/TLS spec>]\"\n"
        "\n"
        "IPv6 addresses should be specified with the square bracket notation [].\n"
        "To listen to all interfaces, you can specify an asterisk (*) as host:\n"
        "\n"
        "-l *:3491 -l *:3492=<vhostname for SNI>:<ssl_cert_file.crt>:<ssl_key_file.key>:<ca_file.crt>\n"
        "\n"
        "This 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"
        "To disable listening on IPv4/6 addresses, specify :no-ipv4 or :no-ipv6\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"
        "World offsets are optional and default to 0, 0 if not specified.\n"
        "\n"
        "All the map offsets are relative to world origin, which is 0,0 by default.\n"
    );
}


void mapMSG_V(const int level, const char *fmt, va_list ap)
{
    // Quick way out
    if (setLogFH != NULL || (level < 0 || th_verbosity >= level))
    {
        char *vtmp = th_strdup_vprintf(fmt, ap);
        char vstr[64] = "";
        time_t stamp = time(NULL);
        struct tm *stamp_tm;

        // Format timestamp
        if ((stamp_tm = localtime(&stamp)) != NULL)
            strftime(vstr, sizeof(vstr), "%c", stamp_tm);

        // Sanitize the printed string
        for (size_t i = 0; vtmp[i]; i++)
        {
            if (vtmp[i] != '\n' && (vtmp[i] < 32 || vtmp[i] > 126))
                vtmp[i] = ' ';
        }

        if (setLogFH != NULL)
        {
            fprintf(setLogFH, "[%s] %s", vstr, vtmp);
            fflush(setLogFH);
        }
        else
        {
            fprintf(stdout, "[%s] %s", vstr, vtmp);
        }

        th_free(vtmp);
    }
}


void mapMSG(const int level, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    mapMSG_V(level, fmt, ap);
    va_end(ap);
}


void mapERR(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    mapMSG_V(-1, fmt, ap);
    va_end(ap);
}


void mapLogWS(int level, const char *line)
{
    if (level <= (1 << th_verbosity))
        mapMSG(-1, "%s", line);
}


bool argParseCoordPair(const char *str, int *xc, int *yc)
{
    char *piece, *tmp, *fmt = th_strdup(str);
    bool ok = false;

    if ((piece = strchr(fmt, ':')) == NULL)
        goto out;

    *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);

    ok = true;

out:
    th_free(fmt);
    return ok;
}


bool argParseMapSpec(const char *cspec)
{
    MAPInfoCtx *info;
    char *piece, *start, *spec = th_strdup(cspec);
    bool ok = false;

    if (optNMaps >= SET_MAX_MAPS)
    {
        mapERR("Maximum number of map specs already specified.\n");
        goto out;
    }

    info = &optMaps[optNMaps++];
    memset(info, 0, sizeof(MAPInfoCtx));

    // Check for map filename end
    if ((piece = strchr(spec, ':')) == NULL)
    {
        mapERR("Invalid map spec '%s': missing map filename separator ':'.\n", cspec);
        goto out;
    }
    *piece++ = 0;

    info->mapFilename = th_strdup_trim(spec, TH_TRIM_BOTH);
    start = piece;

    // Check for loc filename end
    if ((piece = strchr(start, ':')) == NULL)
    {
        mapERR("Invalid map spec '%s': missing loc filename separator ':'.\n", cspec);
        goto out;
    }
    *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 &&
        !argParseCoordPair(piece, &info->locFile.xoffs, &info->locFile.yoffs))
    {
        mapERR("Invalid map spec '%s': invalid coordinate offsets '%s'.\n", cspec, piece);
        goto out;
    }

    ok = true;

out:
    th_free(spec);
    return ok;
}


MAPListenerCtx *mapNewListenCtx(void)
{
    return th_malloc0(sizeof(MAPListenerCtx));
}


void mapFreeListenCtx(MAPListenerCtx *ctx)
{
    if (ctx != NULL)
    {
        th_free(ctx->vhostname);
        th_free(ctx->interface);
        th_free(ctx->sslCertFile);
        th_free(ctx->sslKeyFile);
        th_free(ctx->sslCAFile);
        th_free(ctx);
    }
}


bool argParseListenerSpec(const char *cspec)
{
    MAPListenerCtx *ctx = NULL;
    char *start, *end, *flags, *interface,
         *port = NULL, *spec = th_strdup(cspec);
    bool ok = false;

    if (optNListenTo >= SET_MAX_LISTEN)
    {
        mapERR("Maximum number of listener specs already specified.\n");
        goto out;
    }

    if ((ctx = mapNewListenCtx()) == NULL)
    {
        mapERR("Could not allocate memory for listener spec!\n");
        goto out;
    }

    interface = spec;
    if (*interface == '[')
    {
        // IPv6 IP address is handled in a special case
        interface++;
        if ((end = strchr(interface, ']')) == NULL)
        {
            mapERR("Invalid IPv6 IP address '%s'.\n", cspec);
            goto out;
        }
        *end++ = 0;

        for (size_t n = 0; interface[n]; n++)
        if (!isxdigit(interface[n]) && interface[n] != ':')
        {
            mapERR("Invalid IPv6 IP address '%s'.\n", interface);
            goto out;
        }
    }
    else
    {
        end = strchr(interface, ':');
    }

    // Find port number separator
    if (end == NULL || *end != ':')
    {
        mapERR("Missing listening port in '%s'.\n", cspec);
        goto out;
    }
    *end++ = 0;
    start = end;

    // Check for '=<SSL/TLS spec>' at the end
    if ((flags = strchr(start, '=')) != NULL)
        *flags++ = 0;

    // Check for ':no-ipv4' or ':no-ipv6' flag
    if ((end = strstr(start, ":no-ipv")) != NULL &&
        (end[7] == '4' || end[7] == '6'))
    {
        *end = 0;

        if (end[7] == '4')
            ctx->ipvMode = 6;
        else
            ctx->ipvMode = 4;
    }

    // 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)
    {
        mapERR("Missing listening port in '%s'.\n", cspec);
        goto out;
    }
    if ((ctx->port = atoi(port)) < 1)
    {
        mapERR("Invalid listening port %d in '%s'.\n", ctx->port, cspec);
        goto out;
    }

    // Parse the SSL/TLS spec, if any
    if (flags != NULL)
    {
        char *cstart, *cend;

        // Check for separator
        cstart = flags;
        if ((cend = strchr(cstart, ':')) == NULL)
        {
            mapERR("Invalid SSL/TLS spec '%s'\n", flags);
            goto out;
        }
        *cend++ = 0;

        // Get the vhost name
        ctx->vhostname = th_strdup_trim(cstart, TH_TRIM_BOTH);
        if (strlen(ctx->vhostname) == 0)
        {
            th_free(ctx->vhostname);
            ctx->vhostname = NULL;
        }

        // Check for separator
        cstart = cend;
        if ((cend = strchr(cend, ':')) == NULL)
        {
            mapERR("Invalid SSL/TLS spec, missing certificate file.\n");
            goto out;
        }
        *cend++ = 0;

        // Get certificate file path
        ctx->sslCertFile = th_strdup_trim(cstart, TH_TRIM_BOTH);

        // Check for separator
        cstart = cend;
        if ((cend = strchr(cend, ':')) == NULL)
        {
            mapERR("Invalid SSL/TLS spec, missing key file.\n");
            goto out;
        }
        *cend++ = 0;

        // Get the rest
        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)
        {
            mapERR("Duplicate listener spec (%s:%d)\n",
                ctx->interface != NULL ? ctx->interface : "*", ctx->port);
            goto out;
        }
    }
    ok = true;

out:
    th_free(spec);
    th_free(port);

    if (ok)
    {
        optListenTo[optNListenTo++] = ctx;
        return true;
    }

    mapFreeListenCtx(ctx);
    return false;
}


bool argParseUID(const char *str, int *val)
{
    if (sscanf(str, "%d", val) != 1)
    {
        struct passwd *info = getpwnam(str);
        if (info == NULL)
        {
            mapERR("Invalid UID '%s'.\n", str);
            return false;
        }
        *val = info->pw_uid;
    }
    return true;
}


bool argParseGID(const char *str, int *val)
{
    if (sscanf(str, "%d", val) != 1)
    {
        struct group *info = getgrnam(str);
        if (info == NULL)
        {
            mapERR("Invalid GID '%s'.\n", str);
            return false;
        }
        *val = info->gr_gid;
    }
    return true;
}


bool argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN)
    {
    case 0:
        argShowHelp();
        exit(0);
        break;

    case 1:
        th_verbosity++;
        break;

    case 2:
        return argParseListenerSpec(optArg);

    case 3:
        optSSLCipherList = optArg;
        break;

    case 5:
        if (!argParseCoordPair(optArg, &optWorldXC, &optWorldYC))
        {
            mapERR("Invalid world origin coordinates '%s'.\n", optArg);
            return false;
        }
        break;

    case 6:
        optTest = optArg;
        break;

    case 4:
        {
            int tmp = atoi(optArg);
            if (tmp < 10)
            {
                mapERR("Invalid bechmark cycle count %d.\n", optBenchmark);
                return false;
            }
            optBenchmark = tmp;
        }
        break;

    case 7:
        return argParseUID(optArg, &optUID);

    case 8:
        return argParseGID(optArg, &optGID);

    case 9:
        optLogFilename = optArg;
        break;

    case 10:
        optDataPath = optArg;
        break;

    default:
        mapERR("Unknown option '%s'.\n", currArg);
        return false;
    }

    return true;
}


bool argHandleFile(char *currArg)
{
    return argParseMapSpec(currArg);
}


// Find the actual data boundaries of the block
// ignoring any null characters around it.
void mapBlockFindBoundaries(const MapBlock *map, int *x0, int *y0, int *x1, int *y1, const bool precrop)
{
    if (!precrop)
    {
        *x0 = map->width - 1;
        *y0 = map->height - 1;
        *x1 = 0;
        *y1 = 0;
    }

    for (int yc1 = 0, yc2 = map->height - 1; yc1 < map->height; yc1++, yc2--)
    {
        const unsigned char
            *sp0 = map->data + (yc1 * map->scansize),
            *sp1 = map->data + (yc2 * map->scansize);

        int minX = -1,
            maxX = -1;

        for (int xc1 = 0, xc2 = map->width - 1; xc1 < map->width; xc1++, xc2--)
        {
            if (minX == -1 && sp0[xc1] != 0)
                minX = xc1;

            if (maxX == -1 && sp0[xc2] != 0)
                maxX = xc2;

            if (sp0[xc1] != 0 && yc1 < *y0)
                *y0 = yc1;

            if (sp1[xc1] != 0 && yc2 > *y1)
                *y1 = yc2;
        }

        if (minX != -1 && minX < *x0)
            *x0 = minX;

        if (maxX != -1 && maxX > *x1)
            *x1 = maxX;
    }
}


bool mapBlockCrop(MapBlock **pdst, const MapBlock *src, const int x0, const int y0, const int x1, const int y1)
{
    // Check dimensions
    if (x1 - x0 < 0 || y1 - y0 < 0)
        return false;

    // Allocate new block and copy the cropped data
    if ((*pdst = mapBlockAlloc(x1 - x0 + 1, y1 - y0 + 1)) == NULL)
        return false;

    for (int yc = 0; yc < (*pdst)->height; yc++)
    {
        const unsigned char *sp = src->data + ((yc + y0) * src->scansize) + x0;
        unsigned char       *dp = (*pdst)->data + (yc * (*pdst)->scansize);

        for (int xc = 0; xc < (*pdst)->width; xc++)
            *dp++ = *sp++;
    }

    return true;
}


bool mapBlockAutoCrop(MapBlock **pdst, const MapBlock *src,
    const unsigned char *symbols, const size_t nsymbols,
    int x0, int y0, int x1, int y1, const bool precrop)
{
    MapBlock *clean;

    // Step #1: Detect crop boundaries
    if ((clean = mapBlockCopy(src)) == NULL)
        return false;

    mapBlockClean(clean, symbols, nsymbols);
    mapBlockFindBoundaries(clean, &x0, &y0, &x1, &y1, precrop);
    mapBlockFree(clean);

    // Step #2: Check if the boundaries are any smaller than
    // the current block size
    if (x0 == 0 && y0 == 0 &&
        x1 == src->width - 1 && y1 == src->height - 1)
        return false;

    // Step #3: Crop it
    if (!mapBlockCrop(pdst, src, x0, y0, x1, y1))
        return false;

    return true;
}


void mapBlockParseDimensions(const unsigned char *data, const size_t len, int *width, int *height)
{
    size_t offs = 0;
    int x1 = 0, x2 = 0;

    *width = *height = 0;

    while (offs < len)
    {
        const unsigned char ch = data[offs++];
        if (ch == '\n')
        {
            if (x1 > *width)
                *width = x1;

            (*height)++;
            x1 = x2 = 0;
        }
        else
        {
            x2++;
            if (ch != ' ')
                x1 = x2;
        }
    }

    if (x1 > *width)
        *width = x1;

    if (x1 > 0)
        (*height)++;
}


bool mapBlockParse(const unsigned char *data, const size_t len, MapBlock *res)
{
    size_t offs = 0;

    for (int yc = 0; yc < res->height; yc++)
    {
        unsigned char *dp = res->data + (yc * res->scansize);

        if (offs < len && data[offs] != '\n')
        for (int xc = 0; xc < res->width; xc++)
        {
            if (offs < len && data[offs] != '\n')
                dp[xc] = data[offs++];
            else
                break;
        }

        while (offs < len && data[offs] != '\n')
            offs++;

        if (offs < len && data[offs] == '\n')
            offs++;
    }

    return offs == len;
}


// Find "center" coordinates (which may not be actual center)
// for given map block based on list of center symbols.
// Returns true if center marker matching one of the symbols found.
bool mapBlockFindCenter(const MapBlock *block,
    const unsigned char *symbols, const size_t nsymbols,
    int *cx, int *cy, const int tolerance)
{
    const int
        x0 = (block->width * tolerance) / 100,
        x1 = (block->width * (100 - tolerance)) / 100,
        y0 = (block->height * tolerance) / 100,
        y1 = (block->height * (100 - tolerance)) / 100;

    *cx = *cy = 0;
    for (int yc = 0; yc < block->height; yc++)
    {
        const unsigned char *dp = block->data + (yc * block->scansize);
        for (int xc = 0; xc < block->width; xc++)
        {
            if (xc >= x0 && xc <= x1 &&
                yc >= y0 && yc <= y1 &&
                muStrChr(symbols, nsymbols, dp[xc]))
            {
                *cx = xc;
                *cy = yc;
                return true;
            }
        }
    }

    return false;
}


// Calculate entropy value for the given map block, excluding
// the specified characters. TODO: This function is not very good.
// It does not take into account spatial entropy.
int mapBlockGetEntropy(const MapBlock *map, const char *exclude, const int nexclude)
{
    unsigned char *list;
    int num, i;

    // Allocate memory for entropy array
    if ((list = th_malloc0(256)) == NULL)
        return -1;

    // Collect sums into entropy array
    for (int yc = 0; yc < map->height; yc++)
    {
        unsigned char *sp = map->data + (yc * map->scansize);
        for (int xc = 0; xc < map->width; xc++)
            list[sp[xc]]++;
    }

    // Handle exclusions
    if (exclude != NULL && nexclude > 0)
    {
        for (i = 0; i < nexclude; i++)
            list[(int) exclude[i]] = 0;
    }

    // Calculate simple entropy
    for (num = 0, i = 0; i < 256; i++)
        if (list[i]) num++;

    th_free(list);
    return num;
}


// Match given "pattern" block against given "map" at
// specified offset coordinates (ox, oy). Return true if
// there is a match, false otherwise.
bool mapMatchBlock(const MapBlock *map, const MapBlock *pattern, const int ox, const int oy)
{
    const unsigned char
        *sp = map->data + (oy * map->scansize) + ox,
        *dp = pattern->data;

    int yc = pattern->height;
    while (yc--)
    {
        for (int xc = 0; xc < pattern->width; xc++)
        {
            if (dp[xc] != 0 && dp[xc] != sp[xc])
                return false;
        }

        sp += map->scansize;
        dp += pattern->scansize;
    }

    return true;
}


// Simple implementation of atoi() without support for other than base 10
int mapAtoI(const char *str, const size_t len)
{
    int value = 0;
    size_t i = 0;
    bool neg = false;

    while (th_isspace(str[i])) i++;

    if (str[i] == '-')
    {
        neg = true;
        i++;
    }

    for (; i < len; i++)
    {
        if (str[i] >= '0' && str[i] <= '9')
        {
            value *= 10;
            value += str[i] - '0';
        }
        else
            break;
    }

    return neg ? -value : value;
}


//
// This wrapper function for lws_write exists because of the
// libwebsockets' requirement for pre-pad of the data buffer
// for header information.
//
int mapLWSWrite(struct lws *wsi, const unsigned char *data, const size_t len)
{
    if (wsi == NULL)
        return 0;

    if (len >= SET_LWS_BUF_SIZE - SET_LWS_BUF_PAD)
        return -1;

    // Costs us an extra copy
    memcpy(setLWSBuffer + SET_LWS_BUF_PAD, data, len);

    return lws_write(wsi, setLWSBuffer + SET_LWS_BUF_PAD, len, LWS_WRITE_TEXT);
}


// Creates a JSON format results string from the list of matches,
// with additional information in the first array.
void mapCreateResultStr(char **buf, size_t *bufLen, const MAPMatch *matches,
    const int nmatches, const int nlimit, const bool type, const bool centered,
    const int centerX, const int centerY, const MapBlock *pattern)
{
    size_t bufSize = 0;
    char *vstr;
    *bufLen = 0;
    *buf = NULL;

    if (type)
    {
        vstr = th_strdup_printf(
            "RESULT:[[%d,%d,%d,%d,%d,%d,%d]",
            nmatches, nlimit,
            centered, centerX, centerY,
            pattern != NULL ? pattern->width : -1,
            pattern != NULL ? pattern->height : -1);
    }
    else
    {
        vstr = th_strdup_printf(
            "RESULT:[[%d,%d]",
            nmatches, nlimit);
    }

    th_strbuf_puts(buf, &bufSize, bufLen, vstr);
    th_free(vstr);

    for (int n = 0; n < nmatches; n++)
    {
        const MAPMatch *match = &matches[n];
        const char *vstart = (n == 0) ? "," : "";
        const char *vend = (n < nmatches - 1) ? "," : "";

        if (type)
        {
            vstr = th_strdup_printf(
                "%s[\"%s\",%d,%d,%d,%d]%s",
                vstart,
                match->map,
                match->mx, match->my,
                match->wx, match->wy,
                vend);

            th_strbuf_puts(buf, &bufSize, bufLen, vstr);
            th_free(vstr);
        }
        else
        {
            vstr = th_strdup_printf(
                "%s[\"%s\",%d,%d,%d,%d,%d,%d,%1.3f,[",
                vstart,
                match->map,
                match->mx, match->my,
                match->wx, match->wy,
                match->marker->flags,
                match->nname,
                match->marker->vsort.v_float);

            th_strbuf_puts(buf, &bufSize, bufLen, vstr);
            th_free(vstr);

            for (int i = 0; i < match->marker->nnames; i++)
            {
                vstr = th_strdup_printf("\"%s\"%s",
                    match->marker->names[i].name,
                    (i < match->marker->nnames - 1) ? "," : "");

                th_strbuf_puts(buf, &bufSize, bufLen, vstr);
                th_free(vstr);
            }

            th_strbuf_puts(buf, &bufSize, bufLen, "]]");
            th_strbuf_puts(buf, &bufSize, bufLen, vend);
        }
    }

    th_strbuf_puts(buf, &bufSize, bufLen, "]");
}


bool mapParseIntValue(const unsigned char *data, const size_t len, size_t *offs, int *val)
{
    size_t start = *offs;

    for (; *offs < len && data[*offs] != ':';)
        (*offs)++;

    if (data[*offs] != ':' || *offs >= len)
        return false;

    *val = mapAtoI((char *) data + start, *offs);
    return true;
}


void mapPerformSearch(struct lws *wsi, const unsigned char *data, const size_t len, char **verr)
{
    static const char *cleanChars = " *@?%CX";
    size_t ncleanChars = strlen(cleanChars);
    MapBlock *pattern = NULL, *ptmp = NULL;
    bool mapList[SET_MAX_MAPS];
    MAPMatch matches[SET_MAX_MATCHES];
    int width, height, centerX = 0, centerY = 0,
        nmatches = 0, nmapList, reqMatches,
        maxMatches = SET_MAX_MATCHES;
    bool centered = false;
    size_t offs = 0;

    // Get requested number of matches
    if (!mapParseIntValue(data, len, &offs, &reqMatches))
    {
        *verr = "Invalid search query.";
        goto out;
    }

    reqMatches = maxMatches;
    if (maxMatches < 1 || maxMatches > SET_MAX_MATCHES)
        maxMatches = SET_MAX_MATCHES;

    mapMSG(2, "Requested %d matches, limiting to %d.\n",
        reqMatches, maxMatches);

    // Get active map list
    nmapList = 0;
    memset(&mapList, 0, sizeof(mapList));
    while (offs < len)
    {
        size_t offs2;
        bool found = false;

        // Find next separator or end \n
        for (offs2 = offs; offs2 < len && data[offs2] != ':' && data[offs2] != '\n';)
            offs2++;

        // Check against map list
        if (offs2 > offs)
        {
            for (int nmap = 0; nmap < optNMaps && !found; nmap++)
            {
                const char *name = optMaps[nmap].locFile.continent;
                const size_t slen = strlen(name);

                // If the map name matches requested, enable it
                if (offs2 >= offs + slen &&
                    memcmp(data + offs, name, slen) == 0)
                {
                    mapList[nmap] = true;
                    nmapList++;
                    found = true;
                }
            }

            if (!found)
            {
                char *tmps = th_strndup_no0((const char *) data + offs, offs2 - offs);

                mapMSG(-1, "Unknown map spec '%s'.\n", tmps);
                *verr = "Unknown map spec.";

                th_free(tmps);
                goto out;
            }
        }

        // Check for separator or end
        if (data[offs2] != ':')
        {
            offs = offs2;
            break;
        }
        else
            offs = offs2 + 1;
    }

    // Check for remaining data
    if (offs + 1 >= len || data[offs] != '\n')
    {
        *verr = "No map pattern data!";
        goto out;
    }
    offs++;

    // Parse pattern block dimensions
    mapBlockParseDimensions(data + offs, len - offs, &width, &height);
    if (width <= 0 || height <= 0)
    {
        *verr = "Could not parse map block dimensions.";
        goto out;
    }

    mapMSG(2, "Parsed block size %d x %d\n", width, height);

    // Do basic checks for sanity
    if (width * height < 3)
    {
        *verr = "Search block pattern too small.";
        goto out;
    }

    if (width * height > 30 * 30)
    {
        *verr = "Search block pattern too large.";
        goto out;
    }

    // Allocate and attempt to parse the block
    if ((pattern = mapBlockAlloc(width, height)) == NULL)
        goto out;

    if (!mapBlockParse(data + offs, len - offs, pattern))
    {
        *verr = "Error parsing map block data.";
        goto out;
    }

    // Crop the pattern block
    if (mapBlockAutoCrop(&ptmp, pattern,
        (unsigned char *) cleanChars, ncleanChars,
        0, 0, 0, 0, false))
    {
        mapBlockFree(pattern);
        pattern = ptmp;
        mapMSG(2, "Cropped block size: %d x %d\n",
            pattern->width, pattern->height);
    }

    // Sanity checks against the cropped block
    if (pattern->width * pattern->height < 3)
    {
        *verr = "Search block pattern too small.";
        goto out;
    }

    // Print the cropped block
    if (th_verbosity >= 2)
    {
        FILE *tmp = (setLogFH != NULL) ? setLogFH : stdout;
        fprintf(tmp, "----------------------------\n");
        mapBlockPrint(tmp, pattern);
        fprintf(tmp, "----------------------------\n");
    }

    // Entropy check
    int entropy = mapBlockGetEntropy(pattern, cleanChars, ncleanChars);
    mapMSG(2, "Block entropy %d\n", entropy);

    if ((entropy < 2 && width < 4 && height < 4) ||
        (entropy < 3 && width * height < 4))
    {
        *verr = "Search block entropy insufficient.";
        goto out;
    }

    // Find pattern center marker, if any
    centered = mapBlockFindCenter(pattern,
        (unsigned char*) "*@X", 3,
        &centerX, &centerY, 10);

    if (centered)
        mapMSG(2, "Center at %d, %d\n", centerX, centerY);

    // Clean the pattern from characters we do not want to match
    mapBlockClean(pattern, (unsigned char *) cleanChars, ncleanChars);

    //
    // Search the maps .. enabled or if none specified, all of them
    //
    for (int nmap = 0; nmap < optNMaps; nmap++)
    if (mapList[nmap] || nmapList == 0)
    {
        MAPInfoCtx *info = &optMaps[nmap];

        for (int oy = 0; oy < info->map->height - pattern->height; oy++)
        for (int ox = 0; ox < info->map->width - pattern->width; ox++)
        {
            // Check for match
            if (mapMatchBlock(info->map, pattern, ox, oy))
            {
                // Okay, add the match to our list
                MAPMatch *match = &matches[nmatches++];

                match->marker = NULL;
                match->map = info->locFile.continent;
                match->mx = ox + 1 + centerX;
                match->my = oy + 1 + centerY;
                match->wx = optWorldXC + info->locFile.xoffs + ox + centerX;
                match->wy = optWorldYC + info->locFile.yoffs + oy + centerY;

                // Check for max matches
                if (nmatches >= maxMatches)
                    goto out;
            }
        }
    }

out:
    // If an error occured, bail out now
    if (*verr != NULL)
    {
        mapBlockFree(pattern);
        return;
    }

    // We got some matches, output them as a JSON array
    char *buf;
    size_t bufLen;

    mapCreateResultStr(&buf, &bufLen,
        matches, nmatches, maxMatches,
        true, centered, centerX, centerY, pattern);

    mapBlockFree(pattern);

    mapMSG(2, "%s\n", buf);
    mapLWSWrite(wsi, (unsigned char *) buf, bufLen);
    th_free(buf);
}


int mapCompareDistance(const void *pa, const void *pb)
{
    const LocMarker *va = *(const LocMarker **) pa,
        *vb = *(const LocMarker **) pb;

    return va->vsort.v_float - vb->vsort.v_float;
}


int mapNearbyLocationSearch(LocMarker ***pnearest,
    const int xc, const int yc, const int maxDist, char **verr)
{
    int nnearest = 0;
    LocMarker **nearest;

    // Allocate memory for results
    if ((*pnearest = nearest = th_malloc(sizeof(LocMarker *) * optNMapLocations)) == NULL)
    {
        *verr = "Could not allocate memory for temporary sorting buffer.";
        goto out;
    }

    // Calculate distances and filter by maxDist
    for (int nloc = 0; nloc < optNMapLocations; nloc++)
    {
        LocMarker *marker = optMapLocations[nloc];
        int dx = xc - (optWorldXC + marker->xc + marker->file->xoffs),
            dy = yc - (optWorldYC + marker->yc + marker->file->yoffs);
        float dist = sqrt(dx * dx + dy * dy);

        if (maxDist < 0 || dist <= maxDist)
        {
            marker->vsort.v_float = dist;
            nearest[nnearest++] = marker;
        }
    }

    // Sort the locations based on distance
    if (nnearest > 0)
        qsort(nearest, nnearest, sizeof(LocMarker *), mapCompareDistance);

out:

    return nnearest;
}


void mapLocationSearch(struct lws *wsi, const unsigned char *data, const size_t len, char **verr)
{
    MAPMatch matches[SET_MAX_MATCHES];
    int nmatches = 0;
    char *pattern = NULL;
    size_t offs1, offs2, slen;

    if (len == 0)
    {
        *verr = "Search pattern too short.";
        goto out;
    }

    // Check search pattern length
    for (offs1 = 0; offs1 < len && th_isspace(data[offs1]); ) offs1++;
    for (offs2 = len - 1; offs2 > offs1 && (data[offs2] == 0 || th_isspace(data[offs2])); ) offs2--;

    slen = offs2 - offs1 + 1;
    if (slen < 2)
    {
        *verr = "Search pattern too short.";
        goto out;
    }

    if (slen > 25)
    {
        *verr = "Search pattern too long.";
        goto out;
    }

    if ((pattern = th_strndup((char *) data + offs1, slen)) == NULL)
    {
        *verr = "Could not allocate search pattern memory.";
        goto out;
    }

    mapMSG(2, "Search pattern: '%s'\n", pattern);

    // Search the locations
    for (int nmap = 0; nmap < optNMaps; nmap++)
    {
        MAPInfoCtx *info = &optMaps[nmap];
        for (int nloc = 0; nloc < info->loc.nlocations; nloc++)
        {
            LocMarker *marker = info->loc.locations[nloc];
            for (int nname = 0; nname < marker->nnames; nname++)
            if ((marker->flags & LOCF_INVIS) == 0 &&
                th_strcasematch(marker->names[nname].name, pattern, false))
            {
                // Okay, add the match to our list
                MAPMatch *match = &matches[nmatches++];

                match->marker = marker;
                match->nname = nname;
                match->map = info->locFile.continent;
                match->mx = marker->xc + 1;
                match->my = marker->yc + 1;
                match->wx = optWorldXC + info->locFile.xoffs + marker->xc;
                match->wy = optWorldYC + info->locFile.yoffs + marker->yc;

                // Check for max matches
                if (nmatches >= SET_MAX_MATCHES)
                    goto out;

                // Bail out from this location, in order not to have dupes for it
                break;
            }
        }
    }

out:
    th_free(pattern);

    // If an error occured, bail out now
    if (*verr != NULL)
        return;

    // We got some matches, output them as a JSON array
    char *buf;
    size_t bufLen;

    mapCreateResultStr(&buf, &bufLen,
        matches, nmatches, SET_MAX_MATCHES,
        false, false, 0, 0, NULL);

    mapMSG(2, "%s\n", buf);
    mapLWSWrite(wsi, (unsigned char *) buf, bufLen);
    th_free(buf);
}


void mapNearLocationSearch(struct lws *wsi, const unsigned char *data, const size_t len, char **verr)
{
    MAPMatch matches[SET_MAX_MATCHES];
    LocMarker **nearest = NULL;
    int findXC, findYC, maxDist, nnearest = 0, maxMatches = SET_MAX_MATCHES;
    size_t offs = 0;

    // Get coordinates
    if (!mapParseIntValue(data, len, &offs, &findXC))
    {
        *verr = "Invalid search query, no global X coordinate specified.";
        goto out;
    }

    offs++;

    if (!mapParseIntValue(data, len, &offs, &findYC))
    {
        *verr = "Invalid search query, no global Y coordinate specified.";
        goto out;
    }

    offs++;

    // Get max distance, default to some value
    if (!mapParseIntValue(data, len, &offs, &maxDist))
        maxDist = 50;
    else
    {
        offs++;

        // Get max matches amount
        if (!mapParseIntValue(data, len, &offs, &maxMatches))
            maxMatches = SET_MAX_MATCHES;
    }

    // Check values
    if (findXC < 0 || findYC < 0)
    {
        *verr = "Invalid search coordinates.";
        goto out;
    }

    if (maxDist < 1 || maxDist > 1000)
    {
        *verr = "Invalid maximum search distance.";
        goto out;
    }

    if (maxMatches < 1)
        maxMatches = 1;
    else
    if (maxMatches > SET_MAX_MATCHES)
        maxMatches = SET_MAX_MATCHES;

    // Find nearest locations
    nnearest = mapNearbyLocationSearch(&nearest, findXC, findYC, maxDist, verr);
    if (*verr != NULL)
        goto out;

    if (nnearest >= maxMatches)
        nnearest = maxMatches;

    for (int nloc = 0; nloc < nnearest; nloc++)
    {
        LocMarker *marker = nearest[nloc];
        MAPMatch *match = &matches[nloc];

        match->map = marker->file->continent;
        match->marker = marker;
        match->nname = 0;
        match->mx = marker->xc + 1;
        match->my = marker->yc + 1;
        match->wx = optWorldXC + marker->file->xoffs + marker->xc;
        match->wy = optWorldYC + marker->file->yoffs + marker->yc;
    }

out:
    th_free(nearest);

    // If an error occured, bail out now
    if (*verr != NULL)
        return;

    // We got some matches, output them as a JSON array
    char *buf;
    size_t bufLen;

    mapCreateResultStr(&buf, &bufLen,
        matches, nnearest, SET_MAX_MATCHES,
        false, false, 0, 0, NULL);

    mapMSG(2, "%s\n", buf);
    mapLWSWrite(wsi, (unsigned char *) buf, bufLen);
    th_free(buf);
}


void mapHandleRequest(struct lws *wsi, char *data, const size_t len, char **verr)
{
    unsigned char *udata = (unsigned char *) data;

    // Check what the request is about?
    if (len >= 10 + 2 && strncmp(data, "MAPSEARCH:", 10) == 0)
    {
        mapPerformSearch(wsi, udata + 10, len - 10, verr);
    }
    else
    if (len >= 7 && strncmp(data, "GETMAPS", 7) == 0)
    {
        // Client wants a list of available maps
        char *buf = NULL;
        size_t bufLen = 0, bufSize = 0;

        mapMSG(1, "[%p] Sending map information.\n", wsi);

        th_strbuf_puts(&buf, &bufSize, &bufLen, "MAPS:[");

        for (int n = 0; n < optNMaps; n++)
        {
            MAPInfoCtx *info = &optMaps[n];
            char *vstr = th_strdup_printf(
                "[\"%s\",%d,%d]%s",
                info->locFile.continent,
                info->locFile.xoffs + optWorldXC,
                info->locFile.yoffs + optWorldYC,
                (n < optNMaps - 1) ? "," : "");

            th_strbuf_puts(&buf, &bufSize, &bufLen, vstr);
            th_free(vstr);
        }

        th_strbuf_puts(&buf, &bufSize, &bufLen, "]");
        mapLWSWrite(wsi, (unsigned char *) buf, bufLen);
        th_free(buf);
    }
    else
    if (len >= 10 + 1 && strncmp(data, "LOCSEARCH:", 10) == 0)
    {
        mapLocationSearch(wsi, udata + 10, len - 10, verr);
    }
    else
    if (len >= 8 + 3 && strncmp(data, "LOCNEAR:", 8) == 0)
    {
        mapNearLocationSearch(wsi, udata + 8, len - 8, verr);
    }
    else
    {
        // Unknown or invalid query
        *verr = "Invalid command, and/or not enough data.";
    }
}


int mapLWSCallback(struct lws *wsi,
    enum lws_callback_reasons reason,
    void *user, void *in, size_t len)
{
    (void) user;

    switch (reason)
    {
        case LWS_CALLBACK_ESTABLISHED:
            {
            char strName[256], strIP[64];
            int fd = lws_get_socket_fd(wsi);
            lws_get_peer_addresses(wsi, fd, strName, sizeof(strName), strIP, sizeof(strIP));
            mapMSG(2, "[%p] Client connection from %s [%s]\n", wsi, strIP, strName);
            }
            break;

        case LWS_CALLBACK_RECEIVE:
            {
            char *verr = NULL;

            mapHandleRequest(wsi, (char *) in, len, &verr);

            // Check for errors ..
            if (verr != NULL)
            {
                char *vstr = th_strdup_printf("ERROR:%s", verr);
                mapERR("[%p] %s\n", wsi, verr);
                mapLWSWrite(wsi, (unsigned char *) vstr, strlen(vstr));
                th_free(vstr);
            }

            // End communication
            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
#ifdef HAVE_LIBWEBSOCKETS22
    , 0
#endif
    },

    { NULL, NULL, 0, 0, 0, NULL
#ifdef HAVE_LIBWEBSOCKETS22
    , 0
#endif
    },
};


int mapReadFile(const char *filename, uint8_t **pbuf, size_t *pbufSize,
    const size_t bufInit, const size_t bufGrow)
{
    size_t readSize, dataSize, dataPos;
    FILE *fh = NULL;
    int res = THERR_OK;

    if ((fh = fopen(filename, "rb")) == NULL)
    {
        res = THERR_FOPEN;
        goto out;
    }

    // Allocate initial data buffer
    readSize = dataSize = bufInit;
    if ((*pbuf = th_malloc(dataSize)) == NULL)
    {
        res = THERR_MALLOC;
        goto out;
    }

    dataPos = 0;
    *pbufSize = 0;
    while (!feof(fh) && !ferror(fh))
    {
        size_t read = fread((*pbuf) + dataPos, 1, readSize, fh);
        dataPos += read;
        (*pbufSize) += read;

        if (*pbufSize >= dataSize)
        {
            readSize = bufGrow;
            dataSize += bufGrow;
            if ((*pbuf = th_realloc(*pbuf, dataSize)) == NULL)
            {
                res = THERR_MALLOC;
                goto out;
            }
        }
        else
            break;
    }

out:
    if (fh != NULL)
        fclose(fh);

    return res;
}


bool mapLoadMapsData(void)
{
    char *mapFilename = NULL, *locFilename = NULL;
    FILE *fh = NULL;
    bool res = false;

    for (int nmap = 0; nmap < optNMaps; nmap++)
    {
        MAPInfoCtx *info = &optMaps[nmap];

        mapFilename = (optDataPath != NULL && strchr("/\\", info->mapFilename[0]) == NULL) ?
            th_strdup_printf("%s%s%s", optDataPath, TH_DIR_SEPARATOR_STR, info->mapFilename)
            :
            th_strdup(info->mapFilename);

        locFilename = (optDataPath != NULL && strchr("/\\", info->locFile.filename[0]) == NULL) ?
            th_strdup_printf("%s%s%s", optDataPath, TH_DIR_SEPARATOR_STR, info->locFile.filename)
            :
            th_strdup(info->locFile.filename);

        mapMSG(1, "Map ID '%s', data '%s', locations '%s' at [%d, %d]\n",
            info->locFile.continent,
            mapFilename,
            locFilename,
            info->locFile.xoffs, info->locFile.yoffs);

        if ((info->map = mapBlockParseFile(mapFilename, false)) == NULL)
        {
            mapERR("Could not read map data file '%s'.\n",
                mapFilename);
            goto err;
        }

        if ((fh = fopen(locFilename, "rb")) == NULL)
        {
            mapERR("Could not open location file '%s' for reading.\n",
                locFilename);
            goto err;
        }

        if (!locParseLocStream(fh, &info->locFile, &(info->loc), 0, 0))
            goto err;

        fclose(fh);
        fh = NULL;

        th_free_r(&mapFilename);
        th_free_r(&locFilename);
    }

    res = true;

err:
    if (fh != NULL)
        fclose(fh);

    th_free_r(&mapFilename);
    th_free_r(&locFilename);

    return res;
}


bool mapLoadMaps(void)
{
    mapMSG(1, "Trying to load %d map specs. World origin at [%d, %d].\n",
        optNMaps, optWorldXC, optWorldYC);

    if (!mapLoadMapsData())
        return false;

    // Get total number of non-invis locations
    optNMapLocations = 0;
    for (int nmap = 0; nmap < optNMaps; nmap++)
    {
        MAPInfoCtx *info = &optMaps[nmap];
        for (int nloc = 0; nloc < info->loc.nlocations; nloc++)
        {
            LocMarker *marker = info->loc.locations[nloc];
            if ((marker->flags & LOCF_INVIS) == 0)
                optNMapLocations++;
        }
    }

    // Create large array of location pointers for sorting purposes
    if ((optMapLocations = th_malloc(sizeof(LocMarker *) * optNMapLocations)) == NULL)
    {
        mapERR("Could not allocate memory for location sorting buffer of %d entries.\n",
            optNMapLocations);
        return false;
    }

    for (int nmap = 0, index = 0; nmap < optNMaps; nmap++)
    {
        MAPInfoCtx *info = &optMaps[nmap];
        for (int nloc = 0; nloc < info->loc.nlocations; nloc++)
        {
            LocMarker *marker = info->loc.locations[nloc];
            if ((marker->flags & LOCF_INVIS) == 0)
                optMapLocations[index++] = marker;
        }
    }

    return true;
}


void mapFreeMaps(void)
{
    for (int n = 0; n < optNMaps; n++)
    {
        MAPInfoCtx *info = &optMaps[n];

        mapBlockFree(info->map);
        locFreeMapLocations(&info->loc);
    }

    th_free_r(&optMapLocations);
}


#ifdef HAVE_LIBWEBSOCKETS32
void mapSigHandler(void *handle, int signum)
#else
void mapSigHandler(uv_signal_t *handle, int signum)
#endif
{
    (void) handle;

    switch (signum)
    {
        case SIGTERM:
        case SIGINT:
            mapERR("Signal %d caught, exiting...\n", signum);
#ifdef HAVE_LIBWEBSOCKETS32
            lws_context_destroy(setLWSContext);
#else
            lws_libuv_stop(setLWSContext);
#endif
            break;

        default:
            mapERR("Signal %d caught, aborting...\n", signum);
            signal(SIGABRT, SIG_DFL);
            abort();
            break;
    }
}


int main(int argc, char *argv[])
{
    int res = 0;

    // Initialize
    th_init("MapSearch", "Map Search WebSockets server", "0.8", NULL, NULL);
    th_verbosity = 0;

    memset(&optMaps, 0, sizeof(optMaps));
    memset(&optListenTo, 0, sizeof(optListenTo));



    // Parse command line arguments
    if (!th_args_process(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, 0))
    {
        res = 1;
        goto out;
    }

    if (optNMaps == 0)
    {
        argShowHelp();
        mapERR("No maps specified.\n");
        res = 1;
        goto out;
    }

    if (optNListenTo == 0 && optTest == NULL)
    {
        mapERR("No listeners specified.\n");
        res = 1;
        goto out;
    }

    // Initialize logging
    if (optLogFilename != NULL && optTest == NULL &&
        (setLogFH = fopen(optLogFilename, "a")) == NULL)
    {
        res = th_get_error();
        mapERR("Could not open log file '%s' for writing: %s\n",
            optLogFilename, th_error_str(res));

        goto out;
    }

    // Load maps
    if (!mapLoadMaps())
    {
        res = -12;
        goto out;
    }

    // Check for test mode
    if (optTest != NULL)
    {
        mapMSG(1, "Running in test mode, input '%s'.\n", optTest);
        uint8_t *buf = NULL;
        size_t bufSize;

        if (mapReadFile(optTest, &buf, &bufSize, 512, 512) == THERR_OK)
        {
            char *verr = NULL;
            if (optBenchmark > 0)
            {
                int save = th_verbosity;
                mapMSG(0, "Benchmarking for %d cycles ..\n", optBenchmark);
                th_verbosity = -1;
                for (int n = 0; n < optBenchmark; n++)
                {
                    printf(".");
                    fflush(stdout);
                    mapHandleRequest(NULL, (char *) buf, bufSize, &verr);
                    if (verr != NULL)
                    {
                        mapERR("%s\n", verr);
                        break;
                    }
                }
                th_verbosity = save;
                printf("\n");
                mapMSG(0, "Finished.\n");
            }
            else
            {
                mapHandleRequest(NULL, (char *) buf, bufSize, &verr);
                if (verr != NULL)
                    mapERR("%s\n", verr);
            }
        }
        else
            mapERR("Could not read test file.\n");

        th_free(buf);
        goto out;
    }

    // Initialize libwebsockets and create context
    mapMSG(1, "Creating libwebsockets context.\n");

    if ((setLWSBuffer = th_malloc(SET_LWS_BUF_SIZE)) == NULL)
    {
        mapERR("Could not allocate %d bytes of memory for LWS buffer.\n",
            SET_LWS_BUF_SIZE);

        res = -13;
        goto out;
    }

    // Setup log handler
    lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, mapLogWS);

    // Create the LWS context(s)
    MAPListenerCtx *ctx = optListenTo[0];
    struct lws_context_creation_info info;
    int info_options;
    memset(&info, 0, sizeof(info));

    info.gid = optGID;
    info.uid = optUID;
    info.max_http_header_pool = 16;
    info_options = info.options =
        LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
        LWS_SERVER_OPTION_VALIDATE_UTF8 |
        LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT
#if defined(LWS_WITH_LIBUV) || defined(LWS_USE_LIBUV) || !defined(HAVE_LIBWEBSOCKETS32)
        | LWS_SERVER_OPTION_LIBUV
#endif
        ;

    info.protocols = mapLWSProtocols;
    info.extensions = mapLWSExtensions;
    info.ssl_cipher_list = optSSLCipherList;
    info.timeout_secs = 5;
#ifdef HAVE_LIBWEBSOCKETS32
    info.signal_cb = mapSigHandler;
#endif

    if ((setLWSContext = lws_create_context(&info)) == NULL)
    {
        mapERR("libwebsocket initialization failed.\n");
        res = -14;
        goto out;
    }

    // Create listener LWS vhosts ..
    for (int n = 0; n < optNListenTo; n++)
    {
        char *ipvMode = "";
        ctx = optListenTo[n];

        info.port = ctx->port;
        info.iface = ctx->interface;
        info.options = info_options;

        switch (ctx->ipvMode)
        {
            case 0:
                ipvMode = "IPv4 + IPv6";
                break;

            case 4:
                ipvMode = "IPv4 only";
                info.options |= LWS_SERVER_OPTION_DISABLE_IPV6;
                break;

            case 6:
                ipvMode = "IPv6 only";
                info.options |= LWS_SERVER_OPTION_IPV6_V6ONLY_MODIFY | LWS_SERVER_OPTION_IPV6_V6ONLY_VALUE;
                break;
        }

        if (ctx->useSSL)
        {
            mapMSG(1, "Listen to %s:%d (wss) [vhost='%s', cert='%s', key='%s', ca='%s'] [%s]\n",
                ctx->interface != NULL ? ctx->interface : "*",
                ctx->port,
                ctx->vhostname,
                ctx->sslCertFile, ctx->sslKeyFile,
                (ctx->sslCAFile != NULL && ctx->sslCAFile[0]) ? ctx->sslCAFile : "NONE",
                ipvMode);

            info.vhost_name = ctx->vhostname;
            info.ssl_cert_filepath = ctx->sslCertFile;
            info.ssl_private_key_filepath = ctx->sslKeyFile;
            info.ssl_ca_filepath = (ctx->sslCAFile != NULL && ctx->sslCAFile[0]) ? ctx->sslCAFile : NULL;
        }
        else
        {
            mapMSG(1, "Listen to %s:%d (ws) [%s]\n",
                ctx->interface != NULL ? ctx->interface : "*",
                ctx->port,
                ipvMode);

            info.vhost_name = NULL;
            info.ssl_cert_filepath = NULL;
            info.ssl_private_key_filepath = NULL;
            info.ssl_ca_filepath = NULL;
        }

        if ((ctx->vhost = lws_create_vhost(setLWSContext, &info)) == NULL)
        {
            mapERR("LWS vhost creation failed!\n");
            res = -15;
            goto out;
        }
    }

    // Drop privileges
    lws_finalize_startup(setLWSContext);

#ifndef HAVE_LIBWEBSOCKETS32
    // Set up signal handlers
    mapMSG(1, "Setting up signal handlers.\n");
    lws_uv_sigint_cfg(setLWSContext, 1, mapSigHandler);

    // Set up libuv event loop
    if (lws_uv_initloop(setLWSContext, NULL, 0))
    {
        mapERR("lws_uv_initloop() failed.\n");
        res = -16;
        goto out;
    }
#endif

    // Start running ..
    mapMSG(1, "Waiting for connections...\n");
    lws_service(setLWSContext, 0);

out:
    // Shutdown sequence
    mapMSG(1, "Server shutting down.\n");

    if (setLWSContext != NULL)
        lws_context_destroy(setLWSContext);

    for (int n = 0; n < optNListenTo; n++)
        mapFreeListenCtx(optListenTo[n]);

    mapFreeMaps();

    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);
    }

    th_free(setLWSBuffer);

    if (setLogFH)
        fclose(setLogFH);

    return res;
}