/*	$OpenBSD: qs.c,v 1.8 2025/11/04 18:48:41 claudio Exp $ */
/*
 * Copyright (c) 2020 Claudio Jeker <claudio@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <ctype.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>

#include "bgplgd.h"
#include "slowcgi.h"

enum qs_type {
	ONE,
	STRING,
	PREFIX,
	NUMBER,
	FAMILY,
	OVS,
	AVS,
};

const struct qs {
	unsigned int	qs;
	const char	*key;
	enum qs_type	type;
} qsargs[] = {
	{ QS_NEIGHBOR, "neighbor", STRING, },
	{ QS_GROUP, "group", STRING },
	{ QS_AS, "as", NUMBER },
	{ QS_PREFIX, "prefix", PREFIX },
	{ QS_COMMUNITY, "community", STRING },
	{ QS_EXTCOMMUNITY, "ext-community", STRING },
	{ QS_LARGECOMMUNITY, "large-community", STRING },
	{ QS_AF, "af", FAMILY },
	{ QS_RIB, "rib", STRING },
	{ QS_OVS, "ovs", OVS },
	{ QS_BEST, "best", ONE },
	{ QS_ALL, "all", ONE },
	{ QS_SHORTER, "or-shorter", ONE },
	{ QS_ERROR, "error", ONE },
	{ QS_AVS, "avs", AVS },
	{ QS_INVALID, "invalid", ONE },
	{ QS_LEAKED, "leaked", ONE },
	{ QS_FILTERED, "filtered", ONE },
	{ 0, NULL }
};

const char *qs2str(unsigned int qs);

static int
hex(char x)
{
	if ('0' <= x && x <= '9')
		return x - '0';
	if ('a' <= x && x <= 'f')
		return x - 'a' + 10;
	else
		return x - 'A' + 10;
}

static char *
urldecode(const char *s, size_t len)
{
	static char buf[256];
	size_t i, blen = 0;

	for (i = 0; i < len; i++) {
		if (blen >= sizeof(buf) - 1)
			return NULL;
		if (s[i] == '+') {
			buf[blen++] = ' ';
		} else if (s[i] == '%' && i + 2 < len) {
			if (isxdigit((unsigned char)s[i + 1]) &&
			    isxdigit((unsigned char)s[i + 2])) {
				char c;
				c = hex(s[i + 1]) << 4 | hex(s[i + 2]);
				/* replace NUL chars with space */
				if (c == 0)
					c = ' ';
				buf[blen++] = c;
				i += 2;
			} else
				buf[blen++] = s[i];
		} else {
			buf[blen++] = s[i];
		}
	}
	buf[blen] = '\0';

	return buf;
}

static int
valid_string(const char *str)
{
	unsigned char c;

	while ((c = *str++) != '\0')
		if (!isalnum(c) && !ispunct(c) && c != ' ')
			return 0;
	return 1;
}

/* validate that the input is pure decimal number */
static int
valid_number(const char *str)
{
	unsigned char c;
	int first = 1;

	while ((c = *str++) != '\0') {
		/* special handling of 0 */
		if (first && c == '0') {
			if (*str != '\0')
				return 0;
		}
		first = 0;
		if (!isdigit(c))
			return 0;
	}
	return 1;
}

/* validate a prefix, does not support old 10/8 notation but that is ok */
static int
valid_prefix(char *str)
{
	struct addrinfo hints, *res;
	char *p;
	int mask;

	if ((p = strrchr(str, '/')) != NULL) {
		const char *errstr;
		mask = strtonum(p+1, 0, 128, &errstr);
		if (errstr)
			return 0;
		p[0] = '\0';
	}

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_flags = AI_NUMERICHOST;
	if (getaddrinfo(str, NULL, &hints, &res) != 0)
		return 0;
	if (p) {
		if (res->ai_family == AF_INET && mask > 32)
			return 0;
		p[0] = '/';
	}
	freeaddrinfo(res);
	return 1;
}

static int
parse_value(struct lg_ctx *ctx, unsigned int qs, enum qs_type type, char *val)
{
	/* val can only be NULL if urldecode failed. */
	if (val == NULL) {
		lwarnx("urldecode of querystring failed");
		return 400;
	}

	switch (type) {
	case ONE:
		if (strcmp("1", val) == 0) {
			ctx->qs_args[qs].one = 1;
		} else if (strcmp("0", val) == 0) {
			/* silently ignored */
		} else {
			lwarnx("%s: bad value %s expected 1", qs2str(qs), val);
			return 400;
		}
		break;
	case STRING:
		/* limit string to limited ascii chars */
		if (!valid_string(val)) {
			lwarnx("%s: bad string", qs2str(qs));
			return 400;
		}
		ctx->qs_args[qs].string = strdup(val);
		if (ctx->qs_args[qs].string == NULL) {
			lwarn("parse_value");
			return 500;
		}
		break;
	case NUMBER:
		if (!valid_number(val)) {
			lwarnx("%s: bad number", qs2str(qs));
			return 400;
		}
		ctx->qs_args[qs].string = strdup(val);
		if (ctx->qs_args[qs].string == NULL) {
			lwarn("parse_value");
			return 500;
		}
		break;
	case PREFIX:
		if (!valid_prefix(val)) {
			lwarnx("%s: bad prefix", qs2str(qs));
			return 400;
		}
		ctx->qs_args[qs].string = strdup(val);
		if (ctx->qs_args[qs].string == NULL) {
			lwarn("parse_value");
			return 500;
		}
		break;
	case FAMILY:
		if (strcasecmp("ipv4", val) == 0 ||
		    strcasecmp("ipv6", val) == 0 ||
		    strcasecmp("vpnv4", val) == 0 ||
		    strcasecmp("vpnv6", val) == 0) {
			ctx->qs_args[qs].string = strdup(val);
			if (ctx->qs_args[qs].string == NULL) {
				lwarn("parse_value");
				return 500;
			}
		} else {
			lwarnx("%s: bad value %s", qs2str(qs), val);
			return 400;
		}
		break;
	case OVS:
		if (strcmp("not-found", val) == 0 ||
		    strcmp("valid", val) == 0 ||
		    strcmp("invalid", val) == 0) {
			ctx->qs_args[qs].string = strdup(val);
			if (ctx->qs_args[qs].string == NULL) {
				lwarn("parse_value");
				return 500;
			}
		} else {
			lwarnx("%s: bad OVS value %s", qs2str(qs), val);
			return 400;
		}
		break;
	case AVS:
		if (strcmp("unknown", val) == 0 ||
		    strcmp("valid", val) == 0 ||
		    strcmp("invalid", val) == 0) {
			ctx->qs_args[qs].string = strdup(val);
			if (ctx->qs_args[qs].string == NULL) {
				lwarn("parse_value");
				return 500;
			}
		} else {
			lwarnx("%s: bad AVS value %s", qs2str(qs), val);
			return 400;
		}
		break;
	}

	return 0;
}

int
parse_querystring(const char *param, struct lg_ctx *ctx)
{
	size_t len, i;
	int rv;

	while (param && *param) {
		len = strcspn(param, "=");
		for (i = 0; qsargs[i].key != NULL; i++)
			if (strncmp(qsargs[i].key, param, len) == 0)
				break;
		if (qsargs[i].key == NULL) {
			lwarnx("unknown querystring key %.*s", (int)len, param);
			return 400;
		}
		if (((1 << qsargs[i].qs) & ctx->qs_mask) == 0) {
			lwarnx("querystring param %s not allowed for command",
			    qsargs[i].key);
			return 400;
		}
		if (((1 << qsargs[i].qs) & ctx->qs_set) != 0) {
			lwarnx("querystring param %s already set",
			    qsargs[i].key);
			return 400;
		}
		ctx->qs_set |= (1 << qsargs[i].qs);

		if (param[len] != '=') {
			lwarnx("querystring %s without value", qsargs[i].key);
			return 400;
		}

		param += len + 1;
		len = strcspn(param, "&");

		if ((rv = parse_value(ctx, qsargs[i].qs, qsargs[i].type,
		    urldecode(param, len))) != 0)
			return rv;

		param += len;
		if (*param == '&')
			param++;
	}

	return 0;
}

size_t
qs_argv(char **argv, size_t argc, size_t len, struct lg_ctx *ctx, int barenbr)
{
	/* keep space for the final NULL in argv */
	len -= 1;

	/* NEIGHBOR and GROUP are exclusive */
	if (ctx->qs_set & (1 << QS_NEIGHBOR)) {
		if (!barenbr)
			if (argc < len)
				argv[argc++] = "neighbor";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_NEIGHBOR].string;
	} else if (ctx->qs_set & (1 << QS_GROUP)) {
		if (argc < len)
			argv[argc++] = "group";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_GROUP].string;
	}

	if (ctx->qs_set & (1 << QS_AS)) {
		if (argc < len)
			argv[argc++] = "source-as";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_AS].string;
	}
	if (ctx->qs_set & (1 << QS_COMMUNITY)) {
		if (argc < len)
			argv[argc++] = "community";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_COMMUNITY].string;
	}
	if (ctx->qs_set & (1 << QS_EXTCOMMUNITY)) {
		if (argc < len)
			argv[argc++] = "ext-community";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_EXTCOMMUNITY].string;
	}
	if (ctx->qs_set & (1 << QS_LARGECOMMUNITY)) {
		if (argc < len)
			argv[argc++] = "large-community";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_LARGECOMMUNITY].string;
	}
	if (ctx->qs_set & (1 << QS_AF)) {
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_AF].string;
	}
	if (ctx->qs_set & (1 << QS_RIB)) {
		if (argc < len)
			argv[argc++] = "table";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_RIB].string;
	}
	if (ctx->qs_set & (1 << QS_OVS)) {
		if (argc < len)
			argv[argc++] = "ovs";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_OVS].string;
	}
	if (ctx->qs_set & (1 << QS_AVS)) {
		if (argc < len)
			argv[argc++] = "avs";
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_AVS].string;
	}
	/* BEST, ERROR, FILTERED, INVALID and LEAKED are exclusive */
	if (ctx->qs_args[QS_BEST].one) {
		if (argc < len)
			argv[argc++] = "best";
	} else if (ctx->qs_args[QS_ERROR].one) {
		if (argc < len)
			argv[argc++] = "error";
	} else if (ctx->qs_args[QS_FILTERED].one) {
		if (argc < len)
			argv[argc++] = "filtered";
	} else if (ctx->qs_args[QS_INVALID].one) {
		if (argc < len)
			argv[argc++] = "disqualified";
	} else if (ctx->qs_args[QS_LEAKED].one) {
		if (argc < len)
			argv[argc++] = "leaked";
	}

	/* prefix must be last for show rib */
	if (ctx->qs_set & (1 << QS_PREFIX)) {
		if (argc < len)
			argv[argc++] = ctx->qs_args[QS_PREFIX].string;

		/* ALL and SHORTER are exclusive */
		if (ctx->qs_args[QS_ALL].one) {
			if (argc < len)
				argv[argc++] = "all";
		} else if (ctx->qs_args[QS_SHORTER].one) {
			if (argc < len)
				argv[argc++] = "or-shorter";
		}
	}

	if (argc >= len)
		lwarnx("hit limit of argv in qs_argv");

	return argc;
}

const char *
qs2str(unsigned int qs)
{
	size_t i;

	for (i = 0; qsargs[i].key != NULL; i++)
		if (qsargs[i].qs == qs)
			return qsargs[i].key;

	lerrx(1, "unknown querystring param %d", qs);
}
