From: claudio Date: Tue, 28 Jun 2022 16:11:30 +0000 (+0000) Subject: Add bgplgd a fastcgi daemon that provide a REST JSON api to bgpctl. X-Git-Url: http://artulab.com/gitweb/?a=commitdiff_plain;h=e76e7180ef44bd7168efdb5e33737f0b23c51e48;p=openbsd Add bgplgd a fastcgi daemon that provide a REST JSON api to bgpctl. Most code stolen from slowcgi, it just exec bgpctl with -j. Put it in tb@ deraadt@ --- diff --git a/usr.sbin/bgplgd/Makefile b/usr.sbin/bgplgd/Makefile new file mode 100644 index 00000000000..6944734671c --- /dev/null +++ b/usr.sbin/bgplgd/Makefile @@ -0,0 +1,14 @@ +# $OpenBSD: Makefile,v 1.1 2022/06/28 16:11:30 claudio Exp $ + +PROG= bgplgd +SRCS= bgplgd.c slowcgi.c qs.c +CFLAGS+= -Wall +CFLAGS+= -Wstrict-prototypes -Wmissing-prototypes +CLFAGS+= -Wmissing-declarations -Wredundant-decls +CFLAGS+= -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+= -Wsign-compare +LDADD= -levent +DPADD= ${LIBEVENT} +MAN= bgplgd.8 + +.include diff --git a/usr.sbin/bgplgd/bgplgd.8 b/usr.sbin/bgplgd/bgplgd.8 new file mode 100644 index 00000000000..8d8f021f575 --- /dev/null +++ b/usr.sbin/bgplgd/bgplgd.8 @@ -0,0 +1,179 @@ +.\" $OpenBSD: bgplgd.8,v 1.1 2022/06/28 16:11:30 claudio Exp $ +.\" +.\" Copyright (c) 2021 Claudio Jeker +.\" +.\" 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. +.\" +.Dd $Mdocdate: June 28 2022 $ +.Dt BGPLGD 8 +.Os +.Sh NAME +.Nm bgplgd +.Nd a bgpctl FastCGI server +.Sh SYNOPSIS +.Nm +.Op Fl d +.Op Fl p Ar path +.Op Fl S Ar socket +.Op Fl s Ar socket +.Op Fl U Ar user +.Op Fl u Ar user +.Sh DESCRIPTION +.Nm +is a server which implements the FastCGI Protocol to execute +.Xr bgpctl 8 +commands. +.Nm +is a simple server that implements a simple web API to query +.Xr bgpd 8 . +.Pp +.Nm +opens a socket at +.Pa /var/www/run/bgplgd.sock , +owned by www:www, +with permissions 0660. +It will then +and drop privileges to user +.Qq www , +.Xr unveil 2 +the +.Xr bgpctl 8 +binary +and restrict itself with +.Xr pledge 2 . +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl d +Do not daemonize. +If this option is specified, +.Nm +will run in the foreground and log to stderr. +.It Fl p Ar path +Use +.Ar path +instead of +.Xr bgpctl 8 +to query +.Xr bgpd 8 . +.It Fl S Ar socket +Use +.Ar socket +instead of the default +.Pa /var/run/bgpd.rsock +to communicate with +.Xr bgpd 8 . +.It Fl s Ar socket +Create and bind to alternative local socket at +.Ar socket . +.It Fl U Ar user +Change the owner of +.Pa /var/www/run/bgplgd.sock +to +.Ar user +and its primary group instead of the default www:www. +.El +.Pp +.Nm +provides the following API endpoints. +Unless further specified the endpoints do not take any parameters: +.Bl -tag -width Ds +.It Pa /interfaces +Show the interface states. +.It Pa /memory +Show RIB memory statistics. +.It Pa /neighbors +Show detailed neighbors information. +The output can be limited with the following parameters: +.Pp +.Bl -tag -width "neighbor=peer" -compact +.It Cm neighbor Ns = Ns Ar peer +Show infromation for a specific neighbor. +.Ar peer +may be the neighbor's address or description. +.It Cm group Ns = Ns Ar name +Show only entries from the specified peer group. +.El +.It Pa /nexthops +Show the list of BGP nexthops and the result of their validity check. +.It Pa /rib +Show routes from the bgpd(8) Routing Information Base. +The following parameters can be used to filter the output: +.Pp +.Bl -tag -width "neighbor=peer" -compact +.It Cm neighbor Ns = Ns Ar peer +Show infromation for a specific neighbor. +.Ar peer +may be the neighbor's address or description. +.It Cm group Ns = Ns Ar name +Show only entries from the specified peer group. +.It Cm as Ns = Ns Ar number +Show only entries with the specified source AS number. +.It Cm community Ns = Ns Ar string +.It Cm ext-community Ns = Ns Ar string +.It Cm large-community Ns = Ns Ar string +Show only entries that match the specified community. +.It Xo +.Ic af Ns = Ns +.Pq Ic ipv4 | ipv6 | vpnv4 | vpnv6 +.Xc +Show only entries that match the specified address family. +.It Cm rib Ns = Ns Ar name +Show only entries from the RIB with name +.Ar name . +.It Xo +.Ic ovs Ns = Ns +.Pq Ic valid | not-found | invalid +.Xc +Show only prefixes that match the specified Origin Validation State. +.It Cm best Ns = Ns 1 +Show only selected routes. +.It Cm error Ns = Ns 1 +Show only prefixes which are marked invalid and were treated as withdrawn. +.It Cm prefix Ns = Ns Ar addr +Show only entries that match prefix either as the best matching route or +show the entry for this CIDR prefix. +.It Cm all Ns = Ns 1 +Show all entries in the specified prefix range. +.It Cm or-shorter Ns = Ns 1 +Show all entries covering and including the specified prefix. +.El +.It Pa /rtr +Show a list of all RTR sessions. +.It Pa /sets +Show a list summarizing all roa-set, as-set, prefix-set, and origin-set tables. +.It Pa /summary +Show a list of all neighbors, including information about the session state +and message counters. +.El +.Sh EXAMPLES +An example setup in +.Xr httpd 8 +is: +.Bd -literal -offset indent + location "/bgplgd/*" { + fastcgi socket "/run/bgplgd.sock" + request strip 1 + } +.Ed +.Sh SEE ALSO +.Xr bgpctl 8 , +.Xr bgpd 8 , +.Xr httpd 8 +.Sh HISTORY +The +.Nm +server first appeared in +.Ox 7.2 . +.Sh AUTHORS +.An Claudio Jeker Aq Mt claudio@openbsd.org diff --git a/usr.sbin/bgplgd/bgplgd.c b/usr.sbin/bgplgd/bgplgd.c new file mode 100644 index 00000000000..12d993eedb7 --- /dev/null +++ b/usr.sbin/bgplgd/bgplgd.c @@ -0,0 +1,116 @@ +/* $OpenBSD: bgplgd.c,v 1.1 2022/06/28 16:11:30 claudio Exp $ */ +/* + * Copyright (c) 2020 Claudio Jeker + * + * 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 +#include +#include +#include +#include +#include +#include + +#include "bgplgd.h" + +#define NCMDARGS 4 + +const struct cmd { + const char *path; + char *args[NCMDARGS]; + unsigned int qs_mask; + int barenbr; +} cmds[] = { + { "/interfaces", { "show", "interfaces", NULL }, 0 }, + { "/memory", { "show", "rib", "memory", NULL }, 0 }, + { "/neighbors", { "show", "neighbor", NULL }, QS_MASK_NEIGHBOR, 1 }, + { "/nexthops", { "show", "nexthop", NULL }, 0 }, + { "/rib", { "show", "rib", "detail", NULL }, QS_MASK_RIB }, + { "/rtr", { "show", "rtr", NULL }, 0 }, + { "/sets", { "show", "sets", NULL }, 0 }, + { "/summary", { "show", NULL }, 0 }, + { NULL } +}; + +static int +command_from_path(const char *path, struct lg_ctx *ctx) +{ + size_t i; + + for (i = 0; cmds[i].path != NULL; i++) { + if (strcmp(cmds[i].path, path) == 0) { + ctx->command = &cmds[i]; + ctx->qs_mask = cmds[i].qs_mask; + return 0; + } + } + return 404; +} + +/* + * Prepare a request into a context to call bgpctl. + * Parse method, path and querystring. On failure return the correct + * HTTP error code. On success 0 is returned. + */ +int +prep_request(struct lg_ctx *ctx, const char *meth, const char *path, + const char *qs) +{ + if (meth == NULL || path == NULL) + return 500; + if (strcmp(meth, "GET") != 0) + return 405; + if (command_from_path(path, ctx) != 0) + return 404; + if (parse_querystring(qs, ctx) != 0) + return 400; + + return 0; +} + +/* + * Entry point from the FastCGI handler. + * This runs as an own process and must use STDOUT and STDERR. + * The log functions should no longer be used here. + */ +void +bgpctl_call(struct lg_ctx *ctx) +{ + char *argv[64]; + size_t i, argc = 0; + + argv[argc++] = bgpctlpath; + argv[argc++] = "-j"; + argv[argc++] = "-s"; + argv[argc++] = bgpctlsock; + + for (i = 0; ctx->command->args[i] != NULL; i++) + argv[argc++] = ctx->command->args[i]; + + argc = qs_argv(argv, argc, sizeof(argv) / sizeof(argv[0]), ctx, + ctx->command->barenbr); + + argv[argc++] = NULL; + + signal(SIGPIPE, SIG_DFL); + + /* Write server header first */ + printf("Content-type: application/json\r\n\r\n"); + fflush(stdout); + + execvp(bgpctlpath, argv); + + err(1, "failed to execute %s", bgpctlpath); +} diff --git a/usr.sbin/bgplgd/bgplgd.h b/usr.sbin/bgplgd/bgplgd.h new file mode 100644 index 00000000000..3856b8a2f01 --- /dev/null +++ b/usr.sbin/bgplgd/bgplgd.h @@ -0,0 +1,65 @@ +/* $OpenBSD: bgplgd.h,v 1.1 2022/06/28 16:11:30 claudio Exp $ */ +/* + * Copyright (c) 2020 Claudio Jeker + * + * 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. + */ + +#define QS_NEIGHBOR 1 +#define QS_GROUP 2 +#define QS_AS 3 +#define QS_PREFIX 4 +#define QS_COMMUNITY 5 +#define QS_EXTCOMMUNITY 6 +#define QS_LARGECOMMUNITY 7 +#define QS_AF 8 +#define QS_RIB 9 +#define QS_OVS 10 +#define QS_BEST 11 +#define QS_ALL 12 +#define QS_SHORTER 13 +#define QS_ERROR 14 +#define QS_MAX 15 + +/* too add: empty-as, in, out, peer-as, source-as, transit-as */ + +#define QS_MASK_NEIGHBOR ((1 << QS_NEIGHBOR) | (1 << QS_GROUP)) +#define QS_MASK_RIB \ + ((1 << QS_NEIGHBOR) | (1 << QS_GROUP) | (1 << QS_AS) | \ + (1 << QS_PREFIX) | (1 << QS_COMMUNITY) | \ + (1 << QS_EXTCOMMUNITY) | (1 << QS_LARGECOMMUNITY) | \ + (1 << QS_AF) | (1 << QS_RIB) | (1 << QS_OVS) | \ + (1 << QS_BEST) | (1 << QS_ALL) | (1 << QS_SHORTER) | \ + (1 << QS_ERROR)) + +struct cmd; +struct lg_ctx { + const struct cmd *command; + unsigned int qs_mask; + unsigned int qs_set; + union { + char *string; + int one; + } qs_args[QS_MAX]; +}; + +extern char *bgpctlpath; +extern char *bgpctlsock; + +/* qs.c - query string handling */ +int parse_querystring(const char *, struct lg_ctx *); +size_t qs_argv(char **, size_t, size_t, struct lg_ctx *, int); + +/* main entry points for slowcgi */ +int prep_request(struct lg_ctx *, const char *, const char *, const char *); +void bgpctl_call(struct lg_ctx *); diff --git a/usr.sbin/bgplgd/http.h b/usr.sbin/bgplgd/http.h new file mode 100644 index 00000000000..7f68f7b65c0 --- /dev/null +++ b/usr.sbin/bgplgd/http.h @@ -0,0 +1,104 @@ +/* $OpenBSD: http.h,v 1.1 2022/06/28 16:11:30 claudio Exp $ */ + +/* + * Copyright (c) 2012 - 2015 Reyk Floeter + * + * 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. + */ + +struct http_error { + int error_code; + const char *error_name; +}; + +/* + * HTTP status codes based on IANA assignments (2014-06-11 version): + * https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * plus legacy (306) and non-standard (420). + */ +#define HTTP_ERRORS { \ + { 100, "Continue" }, \ + { 101, "Switching Protocols" }, \ + { 102, "Processing" }, \ + /* 103-199 unassigned */ \ + { 200, "OK" }, \ + { 201, "Created" }, \ + { 202, "Accepted" }, \ + { 203, "Non-Authoritative Information" }, \ + { 204, "No Content" }, \ + { 205, "Reset Content" }, \ + { 206, "Partial Content" }, \ + { 207, "Multi-Status" }, \ + { 208, "Already Reported" }, \ + /* 209-225 unassigned */ \ + { 226, "IM Used" }, \ + /* 227-299 unassigned */ \ + { 300, "Multiple Choices" }, \ + { 301, "Moved Permanently" }, \ + { 302, "Found" }, \ + { 303, "See Other" }, \ + { 304, "Not Modified" }, \ + { 305, "Use Proxy" }, \ + { 306, "Switch Proxy" }, \ + { 307, "Temporary Redirect" }, \ + { 308, "Permanent Redirect" }, \ + /* 309-399 unassigned */ \ + { 400, "Bad Request" }, \ + { 401, "Unauthorized" }, \ + { 402, "Payment Required" }, \ + { 403, "Forbidden" }, \ + { 404, "Not Found" }, \ + { 405, "Method Not Allowed" }, \ + { 406, "Not Acceptable" }, \ + { 407, "Proxy Authentication Required" }, \ + { 408, "Request Timeout" }, \ + { 409, "Conflict" }, \ + { 410, "Gone" }, \ + { 411, "Length Required" }, \ + { 412, "Precondition Failed" }, \ + { 413, "Payload Too Large" }, \ + { 414, "URI Too Long" }, \ + { 415, "Unsupported Media Type" }, \ + { 416, "Range Not Satisfiable" }, \ + { 417, "Expectation Failed" }, \ + { 418, "I'm a teapot" }, \ + /* 419-421 unassigned */ \ + { 420, "Enhance Your Calm" }, \ + { 422, "Unprocessable Entity" }, \ + { 423, "Locked" }, \ + { 424, "Failed Dependency" }, \ + /* 425 unassigned */ \ + { 426, "Upgrade Required" }, \ + /* 427 unassigned */ \ + { 428, "Precondition Required" }, \ + { 429, "Too Many Requests" }, \ + /* 430 unassigned */ \ + { 431, "Request Header Fields Too Large" }, \ + /* 432-450 unassigned */ \ + { 451, "Unavailable For Legal Reasons" }, \ + /* 452-499 unassigned */ \ + { 500, "Internal Server Error" }, \ + { 501, "Not Implemented" }, \ + { 502, "Bad Gateway" }, \ + { 503, "Service Unavailable" }, \ + { 504, "Gateway Timeout" }, \ + { 505, "HTTP Version Not Supported" }, \ + { 506, "Variant Also Negotiates" }, \ + { 507, "Insufficient Storage" }, \ + { 508, "Loop Detected" }, \ + /* 509 unassigned */ \ + { 510, "Not Extended" }, \ + { 511, "Network Authentication Required" }, \ + /* 512-599 unassigned */ \ + { 0, NULL } \ +} diff --git a/usr.sbin/bgplgd/qs.c b/usr.sbin/bgplgd/qs.c new file mode 100644 index 00000000000..dca907c76b8 --- /dev/null +++ b/usr.sbin/bgplgd/qs.c @@ -0,0 +1,400 @@ +/* $OpenBSD: qs.c,v 1.1 2022/06/28 16:11:30 claudio Exp $ */ +/* + * Copyright (c) 2020 Claudio Jeker + * + * 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 +#include +#include +#include +#include +#include + +#include "bgplgd.h" +#include "slowcgi.h" + +enum qs_type { + ONE, + STRING, + PREFIX, + NUMBER, + FAMILY, + OVS +}; + +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 }, + { 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)) + 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'; + } + + bzero(&hints, 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; + } + 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++] = "rib"; + 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; + } + /* BEST and ERROR 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"; + } + + /* 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); +} diff --git a/usr.sbin/bgplgd/slowcgi.c b/usr.sbin/bgplgd/slowcgi.c new file mode 100644 index 00000000000..c229142a6d7 --- /dev/null +++ b/usr.sbin/bgplgd/slowcgi.c @@ -0,0 +1,1279 @@ +/* $OpenBSD: slowcgi.c,v 1.1 2022/06/28 16:11:30 claudio Exp $ */ +/* + * Copyright (c) 2020 Claudio Jeker + * Copyright (c) 2019 Kristaps Dzonsons + * Copyright (c) 2013 David Gwynne + * Copyright (c) 2013 Florian Obser + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "slowcgi.h" +#include "bgplgd.h" +#include "http.h" + +#define TIMEOUT_DEFAULT 120 +#define WWW_USER "www" +#define BGPLGD_USER "_bgplgd" + +#define FCGI_CONTENT_SIZE 65535 +#define FCGI_PADDING_SIZE 255 +#define FCGI_RECORD_SIZE \ + (sizeof(struct fcgi_record_header) + FCGI_CONTENT_SIZE + FCGI_PADDING_SIZE) + +#define FCGI_ALIGNMENT 8 +#define FCGI_ALIGN(n) \ + (((n) + (FCGI_ALIGNMENT - 1)) & ~(FCGI_ALIGNMENT - 1)) + +#define STDOUT_DONE 0x1 +#define STDERR_DONE 0x2 +#define SCRIPT_DONE 0x4 + +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN 1 +#define FCGI_OVERLOADED 2 +#define FCGI_UNKNOWN_ROLE 3 + +#define FD_RESERVE 5 +#define FD_NEEDED 6 +int cgi_inflight = 0; + +struct listener { + struct event ev, pause; +}; + +struct env_val { + SLIST_ENTRY(env_val) entry; + char *key; + char *val; +}; +SLIST_HEAD(env_head, env_val); + +struct fcgi_record_header { + uint8_t version; + uint8_t type; + uint16_t id; + uint16_t content_len; + uint8_t padding_len; + uint8_t reserved; +}__packed; + +struct fcgi_response { + TAILQ_ENTRY(fcgi_response) entry; + uint8_t data[FCGI_RECORD_SIZE]; + size_t data_pos; + size_t data_len; +}; +TAILQ_HEAD(fcgi_response_head, fcgi_response); + +struct request { + LIST_ENTRY(request) entry; + struct event ev; + struct event resp_ev; + struct event tmo; + struct event script_ev; + struct event script_err_ev; + int fd; + uint8_t buf[FCGI_RECORD_SIZE]; + size_t buf_pos; + size_t buf_len; + struct fcgi_response_head response_head; + struct env_head env; + int env_count; + pid_t command_pid; + int command_status; + int script_flags; + uint16_t id; + uint8_t request_started; + uint8_t request_done; + int inflight_fds_accounted; +}; + +LIST_HEAD(requests_head, request); + +struct slowcgi_proc { + struct requests_head requests; + struct event ev_sigchld; +}; + +struct fcgi_begin_request_body { + uint16_t role; + uint8_t flags; + uint8_t reserved[5]; +}__packed; + +struct fcgi_end_request_body { + uint32_t app_status; + uint8_t protocol_status; + uint8_t reserved[3]; +}__packed; + +__dead void usage(void); +int slowcgi_listen(char *, struct passwd *); +void slowcgi_paused(int, short, void *); +int accept_reserve(int, struct sockaddr *, socklen_t *, int, + volatile int *); +void slowcgi_accept(int, short, void *); +void slowcgi_request(int, short, void *); +void slowcgi_response(int, short, void *); +void slowcgi_add_response(struct request *, struct fcgi_response *); +void slowcgi_timeout(int, short, void *); +void slowcgi_sig_handler(int, short, void *); +size_t parse_record(uint8_t * , size_t, struct request *); +void parse_begin_request(uint8_t *, uint16_t, struct request *, + uint16_t); +void parse_params(uint8_t *, uint16_t, struct request *, uint16_t); +void parse_stdin(uint8_t *, uint16_t, struct request *, uint16_t); +char *env_get(struct request *, const char *); +void exec_cgi(struct request *); +void script_std_in(int, short, void *); +void script_err_in(int, short, void *); +void create_data_record(struct request *, uint8_t, const void *, + size_t); +void create_end_record(struct request *); +void cleanup_request(struct request *); +void dump_fcgi_record(const char *, + struct fcgi_record_header *); +void dump_fcgi_record_header(const char *, + struct fcgi_record_header *); +void dump_fcgi_begin_request_body(const char *, + struct fcgi_begin_request_body *); +void dump_fcgi_end_request_body(const char *, + struct fcgi_end_request_body *); + +const struct loggers conslogger = { + err, + errx, + warn, + warnx, + warnx, /* info */ + warnx /* debug */ +}; + +__dead void syslog_err(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +__dead void syslog_errx(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void syslog_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void syslog_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void syslog_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void syslog_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void syslog_vstrerror(int, int, const char *, va_list) + __attribute__((__format__ (printf, 3, 0))); + +const struct loggers syslogger = { + syslog_err, + syslog_errx, + syslog_warn, + syslog_warnx, + syslog_info, + syslog_debug +}; + +const struct loggers *logger = &conslogger; + +__dead void +usage(void) +{ + extern char *__progname; + fprintf(stderr, + "usage: %s [-d] [-p path] [-S socket] [-s socket] [-U user]\n", + __progname); + exit(1); +} + +struct timeval timeout = { TIMEOUT_DEFAULT, 0 }; +struct slowcgi_proc slowcgi_proc; +int debug = 0; +int on = 1; +char *fcgi_socket = "/var/www/run/bgplgd.sock"; +char *bgpctlpath = "bgpctl"; +char *bgpctlsock = "/var/run/bgpd.rsock"; + + +/* + * Unveil the command we want to run. + * If this has a pathname component in it, interpret as a file + * and unveil the file directly. + * Otherwise, look up the command in our PATH. + */ +static void +unveil_command(const char *prog) +{ + const char *pp; + char *save, *cmd, *path; + struct stat st; + + if (strchr(prog, '/') != NULL) { + if (unveil(prog, "x") == -1) + err(1, "%s: unveil", prog); + return; + } + + if (getenv("PATH") == NULL) + lerrx(1, "PATH is unset"); + if ((path = strdup(getenv("PATH"))) == NULL) + lerr(1, NULL); + save = path; + while ((pp = strsep(&path, ":")) != NULL) { + if (*pp == '\0') + continue; + if (asprintf(&cmd, "%s/%s", pp, prog) == -1) + lerr(1, NULL); + if (lstat(cmd, &st) == -1) { + free(cmd); + continue; + } + if (unveil(cmd, "x") == -1) + lerr(1, "%s: unveil", cmd); + free(cmd); + break; + } + free(save); +} + +int +main(int argc, char *argv[]) +{ + extern char *__progname; + struct listener *l = NULL; + struct passwd *pw; + struct stat sb; + int c, fd; + const char *sock_user = WWW_USER; + const char *cgi_user = BGPLGD_USER; + + /* + * Ensure we have fds 0-2 open so that we have no fd overlaps + * in exec_cgi() later. Just exit on error, we don't have enough + * fds open to output an error message anywhere. + */ + for (c=0; c < 3; c++) { + if (fstat(c, &sb) == -1) { + if ((fd = open("/dev/null", O_RDWR)) != -1) { + if (dup2(fd, c) == -1) + exit(1); + if (fd > c) + close(fd); + } else + exit(1); + } + } + + while ((c = getopt(argc, argv, "dp:S:s:U:u:")) != -1) { + switch (c) { + case 'd': + debug++; + break; + case 'p': + bgpctlpath = optarg; + break; + case 'S': + bgpctlsock = optarg; + break; + case 's': + fcgi_socket = optarg; + break; + case 'U': + sock_user = optarg; + break; + default: + usage(); + /* NOTREACHED */ + } + } + + if (geteuid() != 0) + errx(1, "need root privileges"); + + if (!debug && daemon(0, 0) == -1) + err(1, "daemon"); + + if (!debug) { + openlog(__progname, LOG_PID|LOG_NDELAY, LOG_DAEMON); + logger = &syslogger; + } + + ldebug("sock_user: %s", sock_user); + pw = getpwnam(sock_user); + if (pw == NULL) + lerrx(1, "no %s user", sock_user); + + fd = slowcgi_listen(fcgi_socket, pw); + + ldebug("cgi_user: %s", cgi_user); + pw = getpwnam(cgi_user); + if (pw == NULL) + lerrx(1, "no %s user", cgi_user); + + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + lerr(1, "unable to revoke privs"); + + unveil_command(bgpctlpath); + + if (pledge("stdio rpath unix proc exec", NULL) == -1) + lerr(1, "pledge"); + + LIST_INIT(&slowcgi_proc.requests); + event_init(); + + l = calloc(1, sizeof(*l)); + if (l == NULL) + lerr(1, "listener ev alloc"); + + event_set(&l->ev, fd, EV_READ | EV_PERSIST, slowcgi_accept, l); + event_add(&l->ev, NULL); + evtimer_set(&l->pause, slowcgi_paused, l); + + signal_set(&slowcgi_proc.ev_sigchld, SIGCHLD, slowcgi_sig_handler, + &slowcgi_proc); + signal_add(&slowcgi_proc.ev_sigchld, NULL); + + signal(SIGPIPE, SIG_IGN); + + event_dispatch(); + return (0); +} + +int +slowcgi_listen(char *path, struct passwd *pw) +{ + struct sockaddr_un sun; + mode_t old_umask; + int fd; + + if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, + 0)) == -1) + lerr(1, "slowcgi_listen: socket"); + + bzero(&sun, sizeof(sun)); + sun.sun_family = AF_UNIX; + if (strlcpy(sun.sun_path, path, sizeof(sun.sun_path)) >= + sizeof(sun.sun_path)) + lerrx(1, "socket path too long"); + + if (unlink(path) == -1) + if (errno != ENOENT) + lerr(1, "slowcgi_listen: unlink %s", path); + + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + + if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) + lerr(1,"slowcgi_listen: bind: %s", path); + + umask(old_umask); + + if (chown(path, pw->pw_uid, pw->pw_gid) == -1) + lerr(1, "slowcgi_listen: chown: %s", path); + + if (listen(fd, 5) == -1) + lerr(1, "listen"); + + ldebug("socket: %s", path); + return fd; +} + +void +slowcgi_paused(int fd, short events, void *arg) +{ + struct listener *l = arg; + event_add(&l->ev, NULL); +} + +int +accept_reserve(int sockfd, struct sockaddr *addr, socklen_t *addrlen, + int reserve, volatile int *counter) +{ + int ret; + if (getdtablecount() + reserve + + ((*counter + 1) * FD_NEEDED) >= getdtablesize()) { + ldebug("inflight fds exceeded"); + errno = EMFILE; + return -1; + } + + if ((ret = accept4(sockfd, addr, addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC)) + > -1) { + (*counter)++; + ldebug("inflight incremented, now %d", *counter); + } + return ret; +} + +void +slowcgi_accept(int fd, short events, void *arg) +{ + struct listener *l; + struct sockaddr_storage ss; + struct timeval backoff; + struct request *c; + socklen_t len; + int s; + + l = arg; + backoff.tv_sec = 1; + backoff.tv_usec = 0; + c = NULL; + + len = sizeof(ss); + if ((s = accept_reserve(fd, (struct sockaddr *)&ss, + &len, FD_RESERVE, &cgi_inflight)) == -1) { + switch (errno) { + case EINTR: + case EWOULDBLOCK: + case ECONNABORTED: + return; + case EMFILE: + case ENFILE: + event_del(&l->ev); + evtimer_add(&l->pause, &backoff); + return; + default: + lerr(1, "accept"); + } + } + + c = calloc(1, sizeof(*c)); + if (c == NULL) { + lwarn("cannot calloc request"); + close(s); + cgi_inflight--; + return; + } + c->fd = s; + c->buf_pos = 0; + c->buf_len = 0; + c->request_started = 0; + c->inflight_fds_accounted = 0; + TAILQ_INIT(&c->response_head); + + event_set(&c->ev, s, EV_READ | EV_PERSIST, slowcgi_request, c); + event_add(&c->ev, NULL); + event_set(&c->resp_ev, s, EV_WRITE | EV_PERSIST, slowcgi_response, c); + evtimer_set(&c->tmo, slowcgi_timeout, c); + evtimer_add(&c->tmo, &timeout); + LIST_INSERT_HEAD(&slowcgi_proc.requests, c, entry); +} + +void +slowcgi_timeout(int fd, short events, void *arg) +{ + cleanup_request((struct request*) arg); +} + +void +slowcgi_sig_handler(int sig, short event, void *arg) +{ + struct request *c; + struct slowcgi_proc *p; + pid_t pid; + int status; + + p = arg; + + switch (sig) { + case SIGCHLD: + while ((pid = waitpid(WAIT_ANY, &status, WNOHANG)) > 0) { + LIST_FOREACH(c, &p->requests, entry) + if (c->command_pid == pid) + break; + if (c == NULL) { + lwarnx("caught exit of unknown child %i", pid); + continue; + } + + if (WIFSIGNALED(status)) + c->command_status = WTERMSIG(status); + else + c->command_status = WEXITSTATUS(status); + + ldebug("exit %s%d", + WIFSIGNALED(status) ? "signal" : "", + c->command_status); + + c->script_flags |= SCRIPT_DONE; + + if (c->script_flags == (STDOUT_DONE | STDERR_DONE | + SCRIPT_DONE)) + create_end_record(c); + } + if (pid == -1 && errno != ECHILD) + lwarn("waitpid"); + break; + default: + lerr(1, "unexpected signal: %d", sig); + break; + } +} + +void +slowcgi_add_response(struct request *c, struct fcgi_response *resp) +{ + struct fcgi_record_header *header; + size_t padded_len; + + header = (struct fcgi_record_header*)resp->data; + + /* The FastCGI spec suggests to align the output buffer */ + padded_len = FCGI_ALIGN(resp->data_len); + if (padded_len > resp->data_len) { + /* There should always be FCGI_PADDING_SIZE bytes left */ + if (padded_len > FCGI_RECORD_SIZE) + lerr(1, "response too long"); + header->padding_len = padded_len - resp->data_len; + resp->data_len = padded_len; + } + + TAILQ_INSERT_TAIL(&c->response_head, resp, entry); + event_add(&c->resp_ev, NULL); +} + +void +slowcgi_response(int fd, short events, void *arg) +{ + struct request *c; + struct fcgi_record_header *header; + struct fcgi_response *resp; + ssize_t n; + + c = arg; + + while ((resp = TAILQ_FIRST(&c->response_head))) { + header = (struct fcgi_record_header*) resp->data; + if (debug > 1) + dump_fcgi_record("resp ", header); + + n = write(fd, resp->data + resp->data_pos, resp->data_len); + if (n == -1) { + if (errno == EAGAIN || errno == EINTR) + return; + cleanup_request(c); + return; + } + resp->data_pos += n; + resp->data_len -= n; + if (resp->data_len == 0) { + TAILQ_REMOVE(&c->response_head, resp, entry); + free(resp); + } + } + + if (TAILQ_EMPTY(&c->response_head)) { + if (c->request_done) + cleanup_request(c); + else + event_del(&c->resp_ev); + } +} + +void +slowcgi_request(int fd, short events, void *arg) +{ + struct request *c; + ssize_t n; + size_t parsed; + + c = arg; + + n = read(fd, c->buf + c->buf_pos + c->buf_len, + FCGI_RECORD_SIZE - c->buf_pos-c->buf_len); + + switch (n) { + case -1: + switch (errno) { + case EINTR: + case EAGAIN: + return; + default: + goto fail; + } + break; + + case 0: + ldebug("closed connection"); + goto fail; + default: + break; + } + + c->buf_len += n; + + /* + * Parse the records as they are received. Per the FastCGI + * specification, the server need only receive the FastCGI + * parameter records in full; it is free to begin execution + * at that point, which is what happens here. + */ + do { + parsed = parse_record(c->buf + c->buf_pos, c->buf_len, c); + c->buf_pos += parsed; + c->buf_len -= parsed; + } while (parsed > 0 && c->buf_len > 0); + + /* Make space for further reads */ + if (c->buf_len > 0) { + bcopy(c->buf + c->buf_pos, c->buf, c->buf_len); + c->buf_pos = 0; + } + return; +fail: + cleanup_request(c); +} + +void +parse_begin_request(uint8_t *buf, uint16_t n, struct request *c, uint16_t id) +{ + /* XXX -- FCGI_CANT_MPX_CONN */ + if (c->request_started) { + lwarnx("unexpected FCGI_BEGIN_REQUEST, ignoring"); + return; + } + + if (n != sizeof(struct fcgi_begin_request_body)) { + lwarnx("wrong size %d != %lu", n, + sizeof(struct fcgi_begin_request_body)); + return; + } + + c->request_started = 1; + + c->id = id; + SLIST_INIT(&c->env); + c->env_count = 0; +} + +void +parse_params(uint8_t *buf, uint16_t n, struct request *c, uint16_t id) +{ + struct env_val *env_entry; + uint32_t name_len, val_len; + + if (!c->request_started) { + lwarnx("FCGI_PARAMS without FCGI_BEGIN_REQUEST, ignoring"); + return; + } + + if (c->id != id) { + lwarnx("unexpected id, ignoring"); + return; + } + + /* + * If this is the last FastCGI parameter record, + * begin execution of the CGI script. + */ + if (n == 0) { + exec_cgi(c); + return; + } + + while (n > 0) { + if (buf[0] >> 7 == 0) { + name_len = buf[0]; + n--; + buf++; + } else { + if (n > 3) { + name_len = ((buf[0] & 0x7f) << 24) + + (buf[1] << 16) + (buf[2] << 8) + buf[3]; + n -= 4; + buf += 4; + } else + return; + } + + if (n > 0) { + if (buf[0] >> 7 == 0) { + val_len = buf[0]; + n--; + buf++; + } else { + if (n > 3) { + val_len = ((buf[0] & 0x7f) << 24) + + (buf[1] << 16) + (buf[2] << 8) + + buf[3]; + n -= 4; + buf += 4; + } else + return; + } + } else + return; + + if (n < name_len + val_len) + return; + + if ((env_entry = malloc(sizeof(struct env_val))) == NULL) { + lwarnx("cannot allocate env_entry"); + return; + } + + if ((env_entry->key = malloc(name_len + 1)) == NULL) { + lwarnx("cannot allocate env_entry->key"); + free(env_entry); + return; + } + if ((env_entry->val = malloc(val_len + 1)) == NULL) { + lwarnx("cannot allocate env_entry->val"); + free(env_entry->key); + free(env_entry); + return; + } + + bcopy(buf, env_entry->key, name_len); + buf += name_len; + n -= name_len; + env_entry->key[name_len] = '\0'; + bcopy(buf, env_entry->val, val_len); + buf += val_len; + n -= val_len; + env_entry->val[val_len] = '\0'; + + SLIST_INSERT_HEAD(&c->env, env_entry, entry); + ldebug("env[%d], %s=%s", c->env_count, env_entry->key, + env_entry->val); + c->env_count++; + } +} + +void +parse_stdin(uint8_t *buf, uint16_t n, struct request *c, uint16_t id) +{ + if (c->id != id) { + lwarnx("unexpected id, ignoring"); + return; + } + + if (n != 0) + lwarnx("unexpected stdin input, ignoring"); +} + +size_t +parse_record(uint8_t *buf, size_t n, struct request *c) +{ + struct fcgi_record_header *h; + + if (n < sizeof(struct fcgi_record_header)) + return (0); + + h = (struct fcgi_record_header*) buf; + + if (debug > 1) + dump_fcgi_record("", h); + + if (n < sizeof(struct fcgi_record_header) + ntohs(h->content_len) + + h->padding_len) + return (0); + + if (h->version != 1) + lerrx(1, "wrong version"); + + switch (h->type) { + case FCGI_BEGIN_REQUEST: + parse_begin_request(buf + sizeof(struct fcgi_record_header), + ntohs(h->content_len), c, ntohs(h->id)); + break; + case FCGI_PARAMS: + parse_params(buf + sizeof(struct fcgi_record_header), + ntohs(h->content_len), c, ntohs(h->id)); + break; + case FCGI_STDIN: + parse_stdin(buf + sizeof(struct fcgi_record_header), + ntohs(h->content_len), c, ntohs(h->id)); + break; + default: + lwarnx("unimplemented type %d", h->type); + break; + } + + return (sizeof(struct fcgi_record_header) + ntohs(h->content_len) + + h->padding_len); +} + +char * +env_get(struct request *c, const char *key) +{ + struct env_val *env; + + SLIST_FOREACH(env, &c->env, entry) { + if (strcmp(env->key, key) == 0) + return (env->val); + } + return (NULL); +} + +static const char * +http_error(int *res) +{ + const struct http_error errors[] = HTTP_ERRORS; + size_t i; + + for (i = 0; errors[i].error_code != 0; i++) + if (errors[i].error_code == *res) + return errors[i].error_name; + + /* unknown error - change to 500 */ + lwarnx("unknown http error %d", *res); + *res = 500; + return "Internal Server Error"; +} + +static void +error_response(struct request *c, int res) +{ + const char *type = "text/html"; + const char *errstr = http_error(&res); + char *buf; + int len; + + lwarnx("HTTP status %d: %s", res, errstr); + + len = asprintf(&buf, + "Content-Type: %s\n" + "Status: %d\n" + "Cache-Control: no-cache\n" + "\n" + "\n" + "\n" + " \n" + " \n" + " %d %s\n" + " \n" + " \n" + "

%d %s

\n" + "
\n" + "
OpenBSD bgplgd
\n" + " \n" + "\n", + type, res, type, res, errstr, res, errstr); + + if (len == -1) + lerr(1, NULL); + + create_data_record(c, FCGI_STDOUT, buf, len); + free(buf); + c->script_flags = (STDOUT_DONE | STDERR_DONE | SCRIPT_DONE); + create_end_record(c); +} + +/* + * Fork a new CGI process to handle the request, translating + * between FastCGI parameter records and CGI's environment variables, + * as well as between the CGI process' stdin/stdout and the + * corresponding FastCGI records. + */ +void +exec_cgi(struct request *c) +{ + struct lg_ctx ctx = { 0 }; + int s_in[2], s_out[2], s_err[2], res; + pid_t pid; + + res = prep_request(&ctx, env_get(c, "REQUEST_METHOD"), + env_get(c, "PATH_INFO"), env_get(c, "QUERY_STRING")); + if (res != 0) { + error_response(c, res); + return; + } + + if (pipe(s_in) == -1) + lerr(1, "pipe"); + if (pipe(s_out) == -1) + lerr(1, "pipe"); + if (pipe(s_err) == -1) + lerr(1, "pipe"); + cgi_inflight--; + c->inflight_fds_accounted = 1; + + switch (pid = fork()) { + case -1: + c->command_status = errno; + + lwarn("fork"); + + close(s_in[0]); + close(s_out[0]); + close(s_err[0]); + + close(s_in[1]); + close(s_out[1]); + close(s_err[1]); + + c->script_flags = (STDOUT_DONE | STDERR_DONE | SCRIPT_DONE); + create_end_record(c); + return; + case 0: + /* Child process */ + if (pledge("stdio rpath exec", NULL) == -1) + lerr(1, "pledge"); + close(s_in[0]); + close(s_out[0]); + close(s_err[0]); + + if (dup2(s_in[1], STDIN_FILENO) == -1) + _exit(1); + if (dup2(s_out[1], STDOUT_FILENO) == -1) + _exit(1); + if (dup2(s_err[1], STDERR_FILENO) == -1) + _exit(1); + + close(s_in[1]); + close(s_out[1]); + close(s_err[1]); + + bgpctl_call(&ctx); + + /* should not be reached */ + _exit(1); + + } + + ldebug("fork %d", pid); + + /* Parent process*/ + close(s_in[1]); + close(s_out[1]); + close(s_err[1]); + + fcntl(s_in[0], F_SETFD, FD_CLOEXEC); + fcntl(s_out[0], F_SETFD, FD_CLOEXEC); + fcntl(s_err[0], F_SETFD, FD_CLOEXEC); + + if (ioctl(s_in[0], FIONBIO, &on) == -1) + lerr(1, "script ioctl(FIONBIO)"); + if (ioctl(s_out[0], FIONBIO, &on) == -1) + lerr(1, "script ioctl(FIONBIO)"); + if (ioctl(s_err[0], FIONBIO, &on) == -1) + lerr(1, "script ioctl(FIONBIO)"); + + close(s_in[0]); /* close stdin, bgpctl does not expect anything */ + + c->command_pid = pid; + event_set(&c->script_ev, s_out[0], EV_READ | EV_PERSIST, + script_std_in, c); + event_add(&c->script_ev, NULL); + event_set(&c->script_err_ev, s_err[0], EV_READ | EV_PERSIST, + script_err_in, c); + event_add(&c->script_err_ev, NULL); +} + +static void +script_in(int fd, struct event *ev, struct request *c, uint8_t type) +{ + struct fcgi_response *resp; + struct fcgi_record_header *header; + ssize_t n; + + if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) { + lwarnx("cannot malloc fcgi_response"); + return; + } + header = (struct fcgi_record_header*) resp->data; + header->version = 1; + header->type = type; + header->id = htons(c->id); + header->padding_len = 0; + header->reserved = 0; + + n = read(fd, resp->data + sizeof(struct fcgi_record_header), + FCGI_CONTENT_SIZE); + + if (n == -1) { + switch (errno) { + case EINTR: + case EAGAIN: + free(resp); + return; + default: + n = 0; /* fake empty FCGI_STD{OUT,ERR} response */ + } + } + header->content_len = htons(n); + resp->data_pos = 0; + resp->data_len = n + sizeof(struct fcgi_record_header); + slowcgi_add_response(c, resp); + + if (n == 0) { + if (type == FCGI_STDOUT) + c->script_flags |= STDOUT_DONE; + else + c->script_flags |= STDERR_DONE; + + if (c->script_flags == (STDOUT_DONE | STDERR_DONE | + SCRIPT_DONE)) + create_end_record(c); + event_del(ev); + close(fd); + } +} + +void +script_std_in(int fd, short events, void *arg) +{ + struct request *c = arg; + script_in(fd, &c->script_ev, c, FCGI_STDOUT); +} + +void +script_err_in(int fd, short events, void *arg) +{ + struct request *c = arg; + script_in(fd, &c->script_err_ev, c, FCGI_STDERR); +} + +void +create_data_record(struct request *c, uint8_t type, const void *buf, size_t len) +{ + struct fcgi_response *resp; + struct fcgi_record_header *header; + const char *d = buf; + size_t n; + + do { + if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) { + lwarnx("cannot malloc fcgi_response"); + return; + } + header = (struct fcgi_record_header*) resp->data; + header->version = 1; + header->type = type; + header->id = htons(c->id); + header->padding_len = 0; + header->reserved = 0; + + n = len > FCGI_CONTENT_SIZE ? FCGI_CONTENT_SIZE : len; + memcpy(resp->data + sizeof(struct fcgi_record_header), d, n); + + header->content_len = htons(n); + resp->data_pos = 0; + resp->data_len = n + sizeof(struct fcgi_record_header); + slowcgi_add_response(c, resp); + + len -= n; + d += n; + } while (len > 0); +} + +void +create_end_record(struct request *c) +{ + struct fcgi_response *resp; + struct fcgi_record_header *header; + struct fcgi_end_request_body *end_request; + + if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) { + lwarnx("cannot malloc fcgi_response"); + return; + } + header = (struct fcgi_record_header*) resp->data; + header->version = 1; + header->type = FCGI_END_REQUEST; + header->id = htons(c->id); + header->content_len = htons(sizeof(struct + fcgi_end_request_body)); + header->padding_len = 0; + header->reserved = 0; + end_request = (struct fcgi_end_request_body *) (resp->data + + sizeof(struct fcgi_record_header)); + end_request->app_status = htonl(c->command_status); + end_request->protocol_status = FCGI_REQUEST_COMPLETE; + end_request->reserved[0] = 0; + end_request->reserved[1] = 0; + end_request->reserved[2] = 0; + resp->data_pos = 0; + resp->data_len = sizeof(struct fcgi_end_request_body) + + sizeof(struct fcgi_record_header); + slowcgi_add_response(c, resp); + c->request_done = 1; +} + +void +cleanup_request(struct request *c) +{ + struct fcgi_response *resp; + struct env_val *env_entry; + + evtimer_del(&c->tmo); + if (event_initialized(&c->ev)) + event_del(&c->ev); + if (event_initialized(&c->resp_ev)) + event_del(&c->resp_ev); + if (event_initialized(&c->script_ev)) { + close(EVENT_FD(&c->script_ev)); + event_del(&c->script_ev); + } + if (event_initialized(&c->script_err_ev)) { + close(EVENT_FD(&c->script_err_ev)); + event_del(&c->script_err_ev); + } + + close(c->fd); + while (!SLIST_EMPTY(&c->env)) { + env_entry = SLIST_FIRST(&c->env); + SLIST_REMOVE_HEAD(&c->env, entry); + free(env_entry->key); + free(env_entry->val); + free(env_entry); + } + + while ((resp = TAILQ_FIRST(&c->response_head))) { + TAILQ_REMOVE(&c->response_head, resp, entry); + free(resp); + } + LIST_REMOVE(c, entry); + if (! c->inflight_fds_accounted) + cgi_inflight--; + free(c); +} + +void +dump_fcgi_record(const char *p, struct fcgi_record_header *h) +{ + dump_fcgi_record_header(p, h); + + if (h->type == FCGI_BEGIN_REQUEST) + dump_fcgi_begin_request_body(p, + (struct fcgi_begin_request_body *)(h + 1)); + else if (h->type == FCGI_END_REQUEST) + dump_fcgi_end_request_body(p, + (struct fcgi_end_request_body *)(h + 1)); +} + +void +dump_fcgi_record_header(const char* p, struct fcgi_record_header *h) +{ + ldebug("%sversion: %d", p, h->version); + ldebug("%stype: %d", p, h->type); + ldebug("%srequestId: %d", p, ntohs(h->id)); + ldebug("%scontentLength: %d", p, ntohs(h->content_len)); + ldebug("%spaddingLength: %d", p, h->padding_len); + ldebug("%sreserved: %d", p, h->reserved); +} + +void +dump_fcgi_begin_request_body(const char *p, struct fcgi_begin_request_body *b) +{ + ldebug("%srole %d", p, ntohs(b->role)); + ldebug("%sflags %d", p, b->flags); +} + +void +dump_fcgi_end_request_body(const char *p, struct fcgi_end_request_body *b) +{ + ldebug("%sappStatus: %d", p, ntohl(b->app_status)); + ldebug("%sprotocolStatus: %d", p, b->protocol_status); +} + +void +syslog_vstrerror(int e, int priority, const char *fmt, va_list ap) +{ + char *s; + + if (vasprintf(&s, fmt, ap) == -1) { + syslog(LOG_EMERG, "unable to alloc in syslog_vstrerror"); + exit(1); + } + syslog(priority, "%s: %s", s, strerror(e)); + free(s); +} + +__dead void +syslog_err(int ecode, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + syslog_vstrerror(errno, LOG_CRIT, fmt, ap); + va_end(ap); + exit(ecode); +} + +__dead void +syslog_errx(int ecode, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vsyslog(LOG_CRIT, fmt, ap); + va_end(ap); + exit(ecode); +} + +void +syslog_warn(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + syslog_vstrerror(errno, LOG_ERR, fmt, ap); + va_end(ap); +} + +void +syslog_warnx(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vsyslog(LOG_ERR, fmt, ap); + va_end(ap); +} + +void +syslog_info(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vsyslog(LOG_INFO, fmt, ap); + va_end(ap); +} + +void +syslog_debug(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vsyslog(LOG_DEBUG, fmt, ap); + va_end(ap); +} diff --git a/usr.sbin/bgplgd/slowcgi.h b/usr.sbin/bgplgd/slowcgi.h new file mode 100644 index 00000000000..1431ba703fb --- /dev/null +++ b/usr.sbin/bgplgd/slowcgi.h @@ -0,0 +1,55 @@ +/* $OpenBSD: slowcgi.h,v 1.1 2022/06/28 16:11:30 claudio Exp $ */ +/* + * Copyright (c) 2013 David Gwynne + * Copyright (c) 2013 Florian Obser + * + * 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. + */ + +#define FCGI_BEGIN_REQUEST 1 +#define FCGI_ABORT_REQUEST 2 +#define FCGI_END_REQUEST 3 +#define FCGI_PARAMS 4 +#define FCGI_STDIN 5 +#define FCGI_STDOUT 6 +#define FCGI_STDERR 7 +#define FCGI_DATA 8 +#define FCGI_GET_VALUES 9 +#define FCGI_GET_VALUES_RESULT 10 +#define FCGI_UNKNOWN_TYPE 11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +struct loggers { + __dead void (*err)(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); + __dead void (*errx)(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); + void (*warn)(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + void (*warnx)(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + void (*info)(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + void (*debug)(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +}; + +extern const struct loggers *logger; + +#define lerr(_e, _f...) logger->err((_e), _f) +#define lerrx(_e, _f...) logger->errx((_e), _f) +#define lwarn(_f...) logger->warn(_f) +#define lwarnx(_f...) logger->warnx(_f) +#define linfo(_f...) logger->info(_f) +#define ldebug(_f...) logger->debug(_f) +