aboutsummaryrefslogtreecommitdiff
path: root/fcgi.c
diff options
context:
space:
mode:
authorOmar Polo <op@omarpolo.com>2021-05-09 18:23:36 +0000
committerOmar Polo <op@omarpolo.com>2021-05-09 18:23:36 +0000
commit8ad1c570242cd93f0802931621b49b2510b338e7 (patch)
tree361394003bca869780ace3a3391ff13b2439a6e2 /fcgi.c
parent50310aff335912edde625a5cde3729e34783fd7c (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.c469
1 files changed, 469 insertions, 0 deletions
diff --git a/fcgi.c b/fcgi.c
new file mode 100644
index 0000000..fe4db2f
--- /dev/null
+++ b/fcgi.c
@@ -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, &copy_mbuf, c, NULL);
+ return;
+ case TLS_WANT_POLLOUT:
+ event_once(c->fd, EV_WRITE, &copy_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, &copy_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, &copy_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);
+}