Mercurial > hg > batmud > maputils
view src/mapsearch.c @ 2499:9ed6bde2401c
Fixes.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Mon, 03 Jul 2023 01:19:55 +0300 |
parents | 0e60d6fdfe9f |
children | e96e757ab01e |
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, ¢erX, ¢erY, 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 = -10; goto out; } if (optNMaps == 0) { argShowHelp(); mapERR("No maps specified.\n"); res = -10; goto out; } if (optNListenTo == 0 && optTest == NULL) { mapERR("No listeners specified.\n"); res = -10; 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; }