diff options
author | Omar Polo <op@omarpolo.com> | 2021-05-09 18:23:36 +0000 |
---|---|---|
committer | Omar Polo <op@omarpolo.com> | 2021-05-09 18:23:36 +0000 |
commit | 8ad1c570242cd93f0802931621b49b2510b338e7 (patch) | |
tree | 361394003bca869780ace3a3391ff13b2439a6e2 /fcgi.c | |
parent | 50310aff335912edde625a5cde3729e34783fd7c (diff) |
fastcgi: a first implementation
Not production-ready yet, but it's a start.
This adds a third ``backend'' for gmid: until now there it served
local files or CGI scripts, now FastCGI applications too.
FastCGI is meant to be an improvement over CGI: instead of exec'ing a
script for every request, it allows to open a single connection to an
``application'' and send the requests/receive the responses over that
socket using a simple binary protocol.
At the moment gmid supports three different methods of opening a
fastcgi connection:
- local unix sockets, with: fastcgi "/path/to/sock"
- network sockets, with: fastcgi tcp "host" [port]
port defaults to 9000 and can be either a string or a number
- subprocess, with: fastcgi spawn "/path/to/program"
the fastcgi protocol is done over the executed program stdin
of these, the last is only for testing and may be removed in the
future.
P.S.: the fastcgi rule is per-location of course :)
Diffstat (limited to 'fcgi.c')
-rw-r--r-- | fcgi.c | 469 |
1 files changed, 469 insertions, 0 deletions
@@ -0,0 +1,469 @@ +/* + * Copyright (c) 2021 Omar Polo <op@omarpolo.com> + * + * 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 "gmid.h" + +#include <assert.h> +#include <errno.h> +#include <string.h> + +/* + * Sometimes it can be useful to inspect the fastcgi traffic as + * received by gmid. + * + * This will make gmid connect to a `debug.sock' socket (that must + * exists) in the current directory and send there a copy of what gets + * read. The socket can be created and monitored e.g. with + * + * rm -f debug.sock ; nc -Ulk ./debug.sock | hexdump -C + * + * NB: the sandbox must be disabled for this to work. + */ +#define DEBUG_FCGI 0 + +#ifdef DEBUG_FCGI +# include <sys/un.h> +static int debug_socket = -1; +#endif + +struct fcgi_header { + unsigned char version; + unsigned char type; + unsigned char req_id1; + unsigned char req_id0; + unsigned char content_len1; + unsigned char content_len0; + unsigned char padding; + unsigned char reserved; +}; + +/* + * number of bytes in a FCGI_HEADER. Future version of the protocol + * will not reduce this number. + */ +#define FCGI_HEADER_LEN 8 + +/* + * values for the version component + */ +#define FCGI_VERSION_1 1 + +/* + * values for the type component + */ +#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 fcgi_begin_req { + unsigned char role1; + unsigned char role0; + unsigned char flags; + unsigned char reserved[5]; +}; + +struct fcgi_begin_req_record { + struct fcgi_header header; + struct fcgi_begin_req body; +}; + +/* + * mask for flags; + */ +#define FCGI_KEEP_CONN 1 + +/* + * values for the role + */ +#define FCGI_RESPONDER 1 +#define FCGI_AUTHORIZER 2 +#define FCGI_FILTER 3 + +struct fcgi_end_req_body { + unsigned char app_status3; + unsigned char app_status2; + unsigned char app_status1; + unsigned char app_status0; + unsigned char proto_status; + unsigned char reserved[3]; +}; + +/* + * values for proto_status + */ +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN 1 +#define FCGI_OVERLOADED 2 +#define FCGI_UNKNOWN_ROLE 3 + +/* + * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT + * records. + */ +#define FCGI_MAX_CONNS "FCGI_MAX_CONNS" +#define FCGI_MAX_REQS "FCGI_MAX_REQS" +#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS" + +static int +prepare_header(struct fcgi_header *h, int type, int id, size_t size, + size_t padding) +{ + memset(h, 0, sizeof(*h)); + + /* + * id=0 is reserved for status messages. + */ + id++; + + h->version = FCGI_VERSION_1; + h->type = type; + h->req_id1 = (id >> 8); + h->req_id0 = (id & 0xFF); + h->content_len1 = (size >> 8); + h->content_len0 = (size & 0xFF); + h->padding = padding; + + return 0; +} + +static int +fcgi_begin_request(int sock, int id) +{ + struct fcgi_begin_req_record r; + + if (id > UINT16_MAX) + return -1; + + memset(&r, 0, sizeof(r)); + prepare_header(&r.header, FCGI_BEGIN_REQUEST, id, + sizeof(r.body), 0); + assert(sizeof(r.body) == FCGI_HEADER_LEN); + + r.body.role1 = 0; + r.body.role0 = FCGI_RESPONDER; + r.body.flags = FCGI_KEEP_CONN; + + if (write(sock, &r, sizeof(r)) != sizeof(r)) + return -1; + return 0; +} + +static int +fcgi_send_param(int sock, int id, const char *name, const char *value) +{ + struct fcgi_header h; + uint32_t namlen, vallen, padlen; + uint8_t s[8]; + size_t size; + char padding[8] = { 0 }; + + namlen = strlen(name); + vallen = strlen(value); + size = namlen + vallen + 8; /* 4 for the sizes */ + padlen = (8 - (size & 0x7)) & 0x7; + + s[0] = ( namlen >> 24) | 0x80; + s[1] = ((namlen >> 16) & 0xFF); + s[2] = ((namlen >> 8) & 0xFF); + s[3] = ( namlen & 0xFF); + + s[4] = ( vallen >> 24) | 0x80; + s[5] = ((vallen >> 16) & 0xFF); + s[6] = ((vallen >> 8) & 0xFF); + s[7] = ( vallen & 0xFF); + + prepare_header(&h, FCGI_PARAMS, id, size, padlen); + + if (write(sock, &h, sizeof(h)) != sizeof(h) || + write(sock, s, sizeof(s)) != sizeof(s) || + write(sock, name, namlen) != namlen || + write(sock, value, vallen) != vallen || + write(sock, padding, padlen) != padlen) + return -1; + + return 0; +} + +static int +fcgi_end_param(int sock, int id) +{ + struct fcgi_header h; + + prepare_header(&h, FCGI_PARAMS, id, 0, 0); + if (write(sock, &h, sizeof(h)) != sizeof(h)) + return -1; + + prepare_header(&h, FCGI_STDIN, id, 0, 0); + if (write(sock, &h, sizeof(h)) != sizeof(h)) + return -1; + + return 0; +} + +static int +fcgi_abort_request(int sock, int id) +{ + struct fcgi_header h; + + prepare_header(&h, FCGI_ABORT_REQUEST, id, 0, 0); + if (write(sock, &h, sizeof(h)) != sizeof(h)) + return -1; + + return 0; +} + +static int +must_read(int sock, char *d, size_t len) +{ + ssize_t r; + +#if DEBUG_FCGI + if (debug_socket == -1) { + struct sockaddr_un addr; + + if ((debug_socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + err(1, "socket"); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strlcpy(addr.sun_path, "./debug.sock", sizeof(addr.sun_path)); + if (connect(debug_socket, (struct sockaddr*)&addr, sizeof(addr)) + == -1) + err(1, "connect"); + } +#endif + + for (;;) { + switch (r = read(sock, d, len)) { + case -1: + case 0: + return -1; + default: +#if DEBUG_FCGI + write(debug_socket, d, r); +#endif + + if (r == (ssize_t)len) + return 0; + len -= r; + d += r; + } + } +} + +static int +fcgi_read_header(int sock, struct fcgi_header *h) +{ + if (must_read(sock, (char*)h, sizeof(*h)) == -1) + return -1; + if (h->version != FCGI_VERSION_1) { + errno = EINVAL; + return -1; + } + return 0; +} + +static inline int +recid(struct fcgi_header *h) +{ + return h->req_id0 + (h->req_id1 << 8) - 1; +} + +static inline int +reclen(struct fcgi_header *h) +{ + return h->content_len0 + (h->content_len1 << 8); +} + +static void +copy_mbuf(int fd, short ev, void *d) +{ + struct client *c = d; + struct mbuf *mbuf; + size_t len; + ssize_t r; + char *data; + + for (;;) { + mbuf = TAILQ_FIRST(&c->mbufhead); + if (mbuf == NULL) + break; + + len = mbuf->len - mbuf->off; + data = mbuf->data + mbuf->off; + switch (r = tls_write(c->ctx, data, len)) { + case -1: + /* + * Can't close_conn here. The application + * needs to be informed first, otherwise it + * can interfere with future connections. + * Check also that we're not doing recursion + * (copy_mbuf -> handle_fcgi -> copy_mbuf ...) + */ + if (c->next != NULL) + goto end; + fcgi_abort_request(0, c->id); + return; + case TLS_WANT_POLLIN: + event_once(c->fd, EV_READ, ©_mbuf, c, NULL); + return; + case TLS_WANT_POLLOUT: + event_once(c->fd, EV_WRITE, ©_mbuf, c, NULL); + return; + } + mbuf->off += r; + + if (mbuf->off == mbuf->len) { + TAILQ_REMOVE(&c->mbufhead, mbuf, mbufs); + free(mbuf); + } + } + +end: + if (c->next != NULL) + c->next(0, 0, c); +} + +static int +consume(int fd, size_t len) +{ + size_t l; + char buf[64]; + + while (len != 0) { + if ((l = len) > sizeof(buf)) + l = sizeof(buf); + if (must_read(fd, buf, l) == -1) + return 0; + len -= l; + } + + return 1; +} + +static void +close_all(struct fcgi *f) +{ + size_t i; + struct client *c; + + for (i = 0; i < MAX_USERS; i++) { + c = &clients[i]; + + if (c->fcgi != f->id) + continue; + + if (c->code != 0) + close_conn(0, 0, c); + else + start_reply(c, CGI_ERROR, "CGI error"); + } + + event_del(&f->e); + close(f->fd); + f->fd = -1; + f->s = FCGI_OFF; +} + +void +handle_fcgi(int sock, short event, void *d) +{ + struct fcgi *f = d; + struct fcgi_header h; + struct fcgi_end_req_body end; + struct client *c; + struct mbuf *mbuf; + size_t len; + + if (fcgi_read_header(sock, &h) == -1) + goto err; + + c = try_client_by_id(recid(&h)); + if (c == NULL || c->fcgi != f->id) + goto err; + + len = reclen(&h); + + switch (h.type) { + case FCGI_END_REQUEST: + if (len != sizeof(end)) + goto err; + if (must_read(sock, (char*)&end, sizeof(end)) == -1) + goto err; + /* TODO: do something with the status? */ + c->fcgi = -1; + c->next = close_conn; + event_once(c->fd, EV_WRITE, ©_mbuf, c, NULL); + break; + + case FCGI_STDERR: + /* discard stderr (for now) */ + if (!consume(sock, len)) + goto err; + break; + + case FCGI_STDOUT: + if ((mbuf = calloc(1, sizeof(*mbuf) + len)) == NULL) + fatal("calloc"); + mbuf->len = len; + if (must_read(sock, mbuf->data, len) == -1) { + free(mbuf); + goto err; + } + + if (TAILQ_EMPTY(&c->mbufhead)) { + TAILQ_INSERT_HEAD(&c->mbufhead, mbuf, mbufs); + event_once(c->fd, EV_WRITE, ©_mbuf, c, NULL); + } else + TAILQ_INSERT_TAIL(&c->mbufhead, mbuf, mbufs); + break; + + default: + log_err(NULL, "got invalid fcgi record (type=%d)", h.type); + goto err; + } + + if (!consume(sock, h.padding)) + goto err; + return; + +err: + close_all(f); +} + +void +send_fcgi_req(struct fcgi *f, struct client *c) +{ + c->next = NULL; + + fcgi_begin_request(f->fd, c->id); + fcgi_send_param(f->fd, c->id, "QUERY_STRING", c->iri.query); + fcgi_send_param(f->fd, c->id, "GEMINI_URL_PATH", c->iri.path); + fcgi_send_param(f->fd, c->id, "SERVER_SOFTWARE", "gmid/1.7"); + /* ... */ + + if (fcgi_end_param(f->fd, c->id) == -1) + close_all(f); +} |