1*5ffbcedbSclaudio /* $OpenBSD: qs.c,v 1.7 2024/12/03 10:38:06 claudio Exp $ */ 2e76e7180Sclaudio /* 3e76e7180Sclaudio * Copyright (c) 2020 Claudio Jeker <claudio@openbsd.org> 4e76e7180Sclaudio * 5e76e7180Sclaudio * Permission to use, copy, modify, and distribute this software for any 6e76e7180Sclaudio * purpose with or without fee is hereby granted, provided that the above 7e76e7180Sclaudio * copyright notice and this permission notice appear in all copies. 8e76e7180Sclaudio * 9e76e7180Sclaudio * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10e76e7180Sclaudio * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11e76e7180Sclaudio * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12e76e7180Sclaudio * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13e76e7180Sclaudio * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14e76e7180Sclaudio * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15e76e7180Sclaudio * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16e76e7180Sclaudio */ 17e76e7180Sclaudio 18e76e7180Sclaudio #include <sys/types.h> 19e76e7180Sclaudio #include <sys/socket.h> 20e76e7180Sclaudio #include <ctype.h> 21e76e7180Sclaudio #include <netdb.h> 22e76e7180Sclaudio #include <stdlib.h> 23e76e7180Sclaudio #include <string.h> 24e76e7180Sclaudio 25e76e7180Sclaudio #include "bgplgd.h" 26e76e7180Sclaudio #include "slowcgi.h" 27e76e7180Sclaudio 28e76e7180Sclaudio enum qs_type { 29e76e7180Sclaudio ONE, 30e76e7180Sclaudio STRING, 31e76e7180Sclaudio PREFIX, 32e76e7180Sclaudio NUMBER, 33e76e7180Sclaudio FAMILY, 34600dedbfSjob OVS, 35600dedbfSjob AVS, 36e76e7180Sclaudio }; 37e76e7180Sclaudio 38e76e7180Sclaudio const struct qs { 39e76e7180Sclaudio unsigned int qs; 40e76e7180Sclaudio const char *key; 41e76e7180Sclaudio enum qs_type type; 42e76e7180Sclaudio } qsargs[] = { 43e76e7180Sclaudio { QS_NEIGHBOR, "neighbor", STRING, }, 44e76e7180Sclaudio { QS_GROUP, "group", STRING }, 45e76e7180Sclaudio { QS_AS, "as", NUMBER }, 46e76e7180Sclaudio { QS_PREFIX, "prefix", PREFIX }, 47e76e7180Sclaudio { QS_COMMUNITY, "community", STRING }, 48e76e7180Sclaudio { QS_EXTCOMMUNITY, "ext-community", STRING }, 49e76e7180Sclaudio { QS_LARGECOMMUNITY, "large-community", STRING }, 50e76e7180Sclaudio { QS_AF, "af", FAMILY }, 51e76e7180Sclaudio { QS_RIB, "rib", STRING }, 52e76e7180Sclaudio { QS_OVS, "ovs", OVS }, 53e76e7180Sclaudio { QS_BEST, "best", ONE }, 54e76e7180Sclaudio { QS_ALL, "all", ONE }, 55e76e7180Sclaudio { QS_SHORTER, "or-shorter", ONE }, 56e76e7180Sclaudio { QS_ERROR, "error", ONE }, 57600dedbfSjob { QS_AVS, "avs", AVS }, 58cb563a9eSclaudio { QS_INVALID, "invalid", ONE }, 59cb563a9eSclaudio { QS_LEAKED, "leaked", ONE }, 60fac3be8eSclaudio { QS_FILTERED, "filtered", ONE }, 61e76e7180Sclaudio { 0, NULL } 62e76e7180Sclaudio }; 63e76e7180Sclaudio 64e76e7180Sclaudio const char *qs2str(unsigned int qs); 65e76e7180Sclaudio 66e76e7180Sclaudio static int 67e76e7180Sclaudio hex(char x) 68e76e7180Sclaudio { 69e76e7180Sclaudio if ('0' <= x && x <= '9') 70e76e7180Sclaudio return x - '0'; 71e76e7180Sclaudio if ('a' <= x && x <= 'f') 72e76e7180Sclaudio return x - 'a' + 10; 73e76e7180Sclaudio else 74e76e7180Sclaudio return x - 'A' + 10; 75e76e7180Sclaudio } 76e76e7180Sclaudio 77e76e7180Sclaudio static char * 78e76e7180Sclaudio urldecode(const char *s, size_t len) 79e76e7180Sclaudio { 80e76e7180Sclaudio static char buf[256]; 81e76e7180Sclaudio size_t i, blen = 0; 82e76e7180Sclaudio 83e76e7180Sclaudio for (i = 0; i < len; i++) { 84e76e7180Sclaudio if (blen >= sizeof(buf)) 85e76e7180Sclaudio return NULL; 86e76e7180Sclaudio if (s[i] == '+') { 87e76e7180Sclaudio buf[blen++] = ' '; 88e76e7180Sclaudio } else if (s[i] == '%' && i + 2 < len) { 89e76e7180Sclaudio if (isxdigit((unsigned char)s[i + 1]) && 90e76e7180Sclaudio isxdigit((unsigned char)s[i + 2])) { 91e76e7180Sclaudio char c; 92e76e7180Sclaudio c = hex(s[i + 1]) << 4 | hex(s[i + 2]); 93e76e7180Sclaudio /* replace NUL chars with space */ 94e76e7180Sclaudio if (c == 0) 95e76e7180Sclaudio c = ' '; 96e76e7180Sclaudio buf[blen++] = c; 97e76e7180Sclaudio i += 2; 98e76e7180Sclaudio } else 99e76e7180Sclaudio buf[blen++] = s[i]; 100e76e7180Sclaudio } else { 101e76e7180Sclaudio buf[blen++] = s[i]; 102e76e7180Sclaudio } 103e76e7180Sclaudio } 104e76e7180Sclaudio buf[blen] = '\0'; 105e76e7180Sclaudio 106e76e7180Sclaudio return buf; 107e76e7180Sclaudio } 108e76e7180Sclaudio 109e76e7180Sclaudio static int 110e76e7180Sclaudio valid_string(const char *str) 111e76e7180Sclaudio { 112e76e7180Sclaudio unsigned char c; 113e76e7180Sclaudio 114e76e7180Sclaudio while ((c = *str++) != '\0') 115e76e7180Sclaudio if (!isalnum(c) && !ispunct(c) && c != ' ') 116e76e7180Sclaudio return 0; 117e76e7180Sclaudio return 1; 118e76e7180Sclaudio } 119e76e7180Sclaudio 120e76e7180Sclaudio /* validate that the input is pure decimal number */ 121e76e7180Sclaudio static int 122e76e7180Sclaudio valid_number(const char *str) 123e76e7180Sclaudio { 124e76e7180Sclaudio unsigned char c; 125e76e7180Sclaudio int first = 1; 126e76e7180Sclaudio 127e76e7180Sclaudio while ((c = *str++) != '\0') { 128e76e7180Sclaudio /* special handling of 0 */ 129e76e7180Sclaudio if (first && c == '0') { 130e76e7180Sclaudio if (*str != '\0') 131e76e7180Sclaudio return 0; 132e76e7180Sclaudio } 133e76e7180Sclaudio first = 0; 134e76e7180Sclaudio if (!isdigit(c)) 135e76e7180Sclaudio return 0; 136e76e7180Sclaudio } 137e76e7180Sclaudio return 1; 138e76e7180Sclaudio } 139e76e7180Sclaudio 140e76e7180Sclaudio /* validate a prefix, does not support old 10/8 notation but that is ok */ 141e76e7180Sclaudio static int 142e76e7180Sclaudio valid_prefix(char *str) 143e76e7180Sclaudio { 144e76e7180Sclaudio struct addrinfo hints, *res; 145e76e7180Sclaudio char *p; 146e76e7180Sclaudio int mask; 147e76e7180Sclaudio 148e76e7180Sclaudio if ((p = strrchr(str, '/')) != NULL) { 149e76e7180Sclaudio const char *errstr; 150e76e7180Sclaudio mask = strtonum(p+1, 0, 128, &errstr); 151e76e7180Sclaudio if (errstr) 152e76e7180Sclaudio return 0; 153e76e7180Sclaudio p[0] = '\0'; 154e76e7180Sclaudio } 155e76e7180Sclaudio 156e479af8fSclaudio memset(&hints, 0, sizeof(hints)); 157e76e7180Sclaudio hints.ai_family = AF_UNSPEC; 158e76e7180Sclaudio hints.ai_socktype = SOCK_DGRAM; 159e76e7180Sclaudio hints.ai_flags = AI_NUMERICHOST; 160e76e7180Sclaudio if (getaddrinfo(str, NULL, &hints, &res) != 0) 161e76e7180Sclaudio return 0; 162e76e7180Sclaudio if (p) { 163e76e7180Sclaudio if (res->ai_family == AF_INET && mask > 32) 164e76e7180Sclaudio return 0; 165e76e7180Sclaudio p[0] = '/'; 166e76e7180Sclaudio } 167e76e7180Sclaudio freeaddrinfo(res); 168e76e7180Sclaudio return 1; 169e76e7180Sclaudio } 170e76e7180Sclaudio 171e76e7180Sclaudio static int 172e76e7180Sclaudio parse_value(struct lg_ctx *ctx, unsigned int qs, enum qs_type type, char *val) 173e76e7180Sclaudio { 174e76e7180Sclaudio /* val can only be NULL if urldecode failed. */ 175e76e7180Sclaudio if (val == NULL) { 176e76e7180Sclaudio lwarnx("urldecode of querystring failed"); 177e76e7180Sclaudio return 400; 178e76e7180Sclaudio } 179e76e7180Sclaudio 180e76e7180Sclaudio switch (type) { 181e76e7180Sclaudio case ONE: 182e76e7180Sclaudio if (strcmp("1", val) == 0) { 183e76e7180Sclaudio ctx->qs_args[qs].one = 1; 184e76e7180Sclaudio } else if (strcmp("0", val) == 0) { 185e76e7180Sclaudio /* silently ignored */ 186e76e7180Sclaudio } else { 187e76e7180Sclaudio lwarnx("%s: bad value %s expected 1", qs2str(qs), val); 188e76e7180Sclaudio return 400; 189e76e7180Sclaudio } 190e76e7180Sclaudio break; 191e76e7180Sclaudio case STRING: 192e76e7180Sclaudio /* limit string to limited ascii chars */ 193e76e7180Sclaudio if (!valid_string(val)) { 194e76e7180Sclaudio lwarnx("%s: bad string", qs2str(qs)); 195e76e7180Sclaudio return 400; 196e76e7180Sclaudio } 197e76e7180Sclaudio ctx->qs_args[qs].string = strdup(val); 198e76e7180Sclaudio if (ctx->qs_args[qs].string == NULL) { 199e76e7180Sclaudio lwarn("parse_value"); 200e76e7180Sclaudio return 500; 201e76e7180Sclaudio } 202e76e7180Sclaudio break; 203e76e7180Sclaudio case NUMBER: 204e76e7180Sclaudio if (!valid_number(val)) { 205e76e7180Sclaudio lwarnx("%s: bad number", qs2str(qs)); 206e76e7180Sclaudio return 400; 207e76e7180Sclaudio } 208e76e7180Sclaudio ctx->qs_args[qs].string = strdup(val); 209e76e7180Sclaudio if (ctx->qs_args[qs].string == NULL) { 210e76e7180Sclaudio lwarn("parse_value"); 211e76e7180Sclaudio return 500; 212e76e7180Sclaudio } 213e76e7180Sclaudio break; 214e76e7180Sclaudio case PREFIX: 215e76e7180Sclaudio if (!valid_prefix(val)) { 216e76e7180Sclaudio lwarnx("%s: bad prefix", qs2str(qs)); 217e76e7180Sclaudio return 400; 218e76e7180Sclaudio } 219e76e7180Sclaudio ctx->qs_args[qs].string = strdup(val); 220e76e7180Sclaudio if (ctx->qs_args[qs].string == NULL) { 221e76e7180Sclaudio lwarn("parse_value"); 222e76e7180Sclaudio return 500; 223e76e7180Sclaudio } 224e76e7180Sclaudio break; 225e76e7180Sclaudio case FAMILY: 226e76e7180Sclaudio if (strcasecmp("ipv4", val) == 0 || 227e76e7180Sclaudio strcasecmp("ipv6", val) == 0 || 228e76e7180Sclaudio strcasecmp("vpnv4", val) == 0 || 229e76e7180Sclaudio strcasecmp("vpnv6", val) == 0) { 230e76e7180Sclaudio ctx->qs_args[qs].string = strdup(val); 231e76e7180Sclaudio if (ctx->qs_args[qs].string == NULL) { 232e76e7180Sclaudio lwarn("parse_value"); 233e76e7180Sclaudio return 500; 234e76e7180Sclaudio } 235e76e7180Sclaudio } else { 236e76e7180Sclaudio lwarnx("%s: bad value %s", qs2str(qs), val); 237e76e7180Sclaudio return 400; 238e76e7180Sclaudio } 239e76e7180Sclaudio break; 240e76e7180Sclaudio case OVS: 241e76e7180Sclaudio if (strcmp("not-found", val) == 0 || 242e76e7180Sclaudio strcmp("valid", val) == 0 || 243e76e7180Sclaudio strcmp("invalid", val) == 0) { 244e76e7180Sclaudio ctx->qs_args[qs].string = strdup(val); 245e76e7180Sclaudio if (ctx->qs_args[qs].string == NULL) { 246e76e7180Sclaudio lwarn("parse_value"); 247e76e7180Sclaudio return 500; 248e76e7180Sclaudio } 249e76e7180Sclaudio } else { 250e76e7180Sclaudio lwarnx("%s: bad OVS value %s", qs2str(qs), val); 251e76e7180Sclaudio return 400; 252e76e7180Sclaudio } 253e76e7180Sclaudio break; 254600dedbfSjob case AVS: 255600dedbfSjob if (strcmp("unknown", val) == 0 || 256600dedbfSjob strcmp("valid", val) == 0 || 257600dedbfSjob strcmp("invalid", val) == 0) { 258600dedbfSjob ctx->qs_args[qs].string = strdup(val); 259600dedbfSjob if (ctx->qs_args[qs].string == NULL) { 260600dedbfSjob lwarn("parse_value"); 261600dedbfSjob return 500; 262e76e7180Sclaudio } 263600dedbfSjob } else { 264600dedbfSjob lwarnx("%s: bad AVS value %s", qs2str(qs), val); 265600dedbfSjob return 400; 266600dedbfSjob } 267600dedbfSjob break; 268600dedbfSjob } 269600dedbfSjob 270e76e7180Sclaudio return 0; 271e76e7180Sclaudio } 272e76e7180Sclaudio 273e76e7180Sclaudio int 274e76e7180Sclaudio parse_querystring(const char *param, struct lg_ctx *ctx) 275e76e7180Sclaudio { 276e76e7180Sclaudio size_t len, i; 277e76e7180Sclaudio int rv; 278e76e7180Sclaudio 279e76e7180Sclaudio while (param && *param) { 280e76e7180Sclaudio len = strcspn(param, "="); 281e76e7180Sclaudio for (i = 0; qsargs[i].key != NULL; i++) 282e76e7180Sclaudio if (strncmp(qsargs[i].key, param, len) == 0) 283e76e7180Sclaudio break; 284e76e7180Sclaudio if (qsargs[i].key == NULL) { 285e76e7180Sclaudio lwarnx("unknown querystring key %.*s", (int)len, param); 286e76e7180Sclaudio return 400; 287e76e7180Sclaudio } 288e76e7180Sclaudio if (((1 << qsargs[i].qs) & ctx->qs_mask) == 0) { 289e76e7180Sclaudio lwarnx("querystring param %s not allowed for command", 290e76e7180Sclaudio qsargs[i].key); 291e76e7180Sclaudio return 400; 292e76e7180Sclaudio } 293e76e7180Sclaudio if (((1 << qsargs[i].qs) & ctx->qs_set) != 0) { 294e76e7180Sclaudio lwarnx("querystring param %s already set", 295e76e7180Sclaudio qsargs[i].key); 296e76e7180Sclaudio return 400; 297e76e7180Sclaudio } 298e76e7180Sclaudio ctx->qs_set |= (1 << qsargs[i].qs); 299e76e7180Sclaudio 300e76e7180Sclaudio if (param[len] != '=') { 301e76e7180Sclaudio lwarnx("querystring %s without value", qsargs[i].key); 302e76e7180Sclaudio return 400; 303e76e7180Sclaudio } 304e76e7180Sclaudio 305e76e7180Sclaudio param += len + 1; 306e76e7180Sclaudio len = strcspn(param, "&"); 307e76e7180Sclaudio 308e76e7180Sclaudio if ((rv = parse_value(ctx, qsargs[i].qs, qsargs[i].type, 309e76e7180Sclaudio urldecode(param, len))) != 0) 310e76e7180Sclaudio return rv; 311e76e7180Sclaudio 312e76e7180Sclaudio param += len; 313e76e7180Sclaudio if (*param == '&') 314e76e7180Sclaudio param++; 315e76e7180Sclaudio } 316e76e7180Sclaudio 317e76e7180Sclaudio return 0; 318e76e7180Sclaudio } 319e76e7180Sclaudio 320e76e7180Sclaudio size_t 321e76e7180Sclaudio qs_argv(char **argv, size_t argc, size_t len, struct lg_ctx *ctx, int barenbr) 322e76e7180Sclaudio { 323e76e7180Sclaudio /* keep space for the final NULL in argv */ 324e76e7180Sclaudio len -= 1; 325e76e7180Sclaudio 326e76e7180Sclaudio /* NEIGHBOR and GROUP are exclusive */ 327e76e7180Sclaudio if (ctx->qs_set & (1 << QS_NEIGHBOR)) { 328e76e7180Sclaudio if (!barenbr) 329e76e7180Sclaudio if (argc < len) 330e76e7180Sclaudio argv[argc++] = "neighbor"; 331e76e7180Sclaudio if (argc < len) 332e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_NEIGHBOR].string; 333e76e7180Sclaudio } else if (ctx->qs_set & (1 << QS_GROUP)) { 334e76e7180Sclaudio if (argc < len) 335e76e7180Sclaudio argv[argc++] = "group"; 336e76e7180Sclaudio if (argc < len) 337e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_GROUP].string; 338e76e7180Sclaudio } 339e76e7180Sclaudio 340e76e7180Sclaudio if (ctx->qs_set & (1 << QS_AS)) { 341e76e7180Sclaudio if (argc < len) 342e76e7180Sclaudio argv[argc++] = "source-as"; 343e76e7180Sclaudio if (argc < len) 344e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_AS].string; 345e76e7180Sclaudio } 346e76e7180Sclaudio if (ctx->qs_set & (1 << QS_COMMUNITY)) { 347e76e7180Sclaudio if (argc < len) 348e76e7180Sclaudio argv[argc++] = "community"; 349e76e7180Sclaudio if (argc < len) 350e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_COMMUNITY].string; 351e76e7180Sclaudio } 352e76e7180Sclaudio if (ctx->qs_set & (1 << QS_EXTCOMMUNITY)) { 353e76e7180Sclaudio if (argc < len) 354e76e7180Sclaudio argv[argc++] = "ext-community"; 355e76e7180Sclaudio if (argc < len) 356e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_EXTCOMMUNITY].string; 357e76e7180Sclaudio } 358e76e7180Sclaudio if (ctx->qs_set & (1 << QS_LARGECOMMUNITY)) { 359e76e7180Sclaudio if (argc < len) 360e76e7180Sclaudio argv[argc++] = "large-community"; 361e76e7180Sclaudio if (argc < len) 362e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_LARGECOMMUNITY].string; 363e76e7180Sclaudio } 364e76e7180Sclaudio if (ctx->qs_set & (1 << QS_AF)) { 365e76e7180Sclaudio if (argc < len) 366e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_AF].string; 367e76e7180Sclaudio } 368e76e7180Sclaudio if (ctx->qs_set & (1 << QS_RIB)) { 369e76e7180Sclaudio if (argc < len) 370*5ffbcedbSclaudio argv[argc++] = "table"; 371e76e7180Sclaudio if (argc < len) 372e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_RIB].string; 373e76e7180Sclaudio } 374e76e7180Sclaudio if (ctx->qs_set & (1 << QS_OVS)) { 375e76e7180Sclaudio if (argc < len) 376e76e7180Sclaudio argv[argc++] = "ovs"; 377e76e7180Sclaudio if (argc < len) 378e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_OVS].string; 379e76e7180Sclaudio } 380600dedbfSjob if (ctx->qs_set & (1 << QS_AVS)) { 381600dedbfSjob if (argc < len) 382600dedbfSjob argv[argc++] = "avs"; 383600dedbfSjob if (argc < len) 384600dedbfSjob argv[argc++] = ctx->qs_args[QS_AVS].string; 385600dedbfSjob } 386fac3be8eSclaudio /* BEST, ERROR, FILTERED, INVALID and LEAKED are exclusive */ 387e76e7180Sclaudio if (ctx->qs_args[QS_BEST].one) { 388e76e7180Sclaudio if (argc < len) 389e76e7180Sclaudio argv[argc++] = "best"; 390e76e7180Sclaudio } else if (ctx->qs_args[QS_ERROR].one) { 391e76e7180Sclaudio if (argc < len) 392e76e7180Sclaudio argv[argc++] = "error"; 393fac3be8eSclaudio } else if (ctx->qs_args[QS_FILTERED].one) { 394fac3be8eSclaudio if (argc < len) 395fac3be8eSclaudio argv[argc++] = "filtered"; 396cb563a9eSclaudio } else if (ctx->qs_args[QS_INVALID].one) { 397cb563a9eSclaudio if (argc < len) 398e8adb3e3Sclaudio argv[argc++] = "disqualified"; 399cb563a9eSclaudio } else if (ctx->qs_args[QS_LEAKED].one) { 400cb563a9eSclaudio if (argc < len) 401cb563a9eSclaudio argv[argc++] = "leaked"; 402e76e7180Sclaudio } 403e76e7180Sclaudio 404e76e7180Sclaudio /* prefix must be last for show rib */ 405e76e7180Sclaudio if (ctx->qs_set & (1 << QS_PREFIX)) { 406e76e7180Sclaudio if (argc < len) 407e76e7180Sclaudio argv[argc++] = ctx->qs_args[QS_PREFIX].string; 408e76e7180Sclaudio 409e76e7180Sclaudio /* ALL and SHORTER are exclusive */ 410e76e7180Sclaudio if (ctx->qs_args[QS_ALL].one) { 411e76e7180Sclaudio if (argc < len) 412e76e7180Sclaudio argv[argc++] = "all"; 413e76e7180Sclaudio } else if (ctx->qs_args[QS_SHORTER].one) { 414e76e7180Sclaudio if (argc < len) 415e76e7180Sclaudio argv[argc++] = "or-shorter"; 416e76e7180Sclaudio } 417e76e7180Sclaudio } 418e76e7180Sclaudio 419e76e7180Sclaudio if (argc >= len) 420e76e7180Sclaudio lwarnx("hit limit of argv in qs_argv"); 421e76e7180Sclaudio 422e76e7180Sclaudio return argc; 423e76e7180Sclaudio } 424e76e7180Sclaudio 425e76e7180Sclaudio const char * 426e76e7180Sclaudio qs2str(unsigned int qs) 427e76e7180Sclaudio { 428e76e7180Sclaudio size_t i; 429e76e7180Sclaudio 430e76e7180Sclaudio for (i = 0; qsargs[i].key != NULL; i++) 431e76e7180Sclaudio if (qsargs[i].qs == qs) 432e76e7180Sclaudio return qsargs[i].key; 433e76e7180Sclaudio 434e76e7180Sclaudio lerrx(1, "unknown querystring param %d", qs); 435e76e7180Sclaudio } 436