/* * Copyright (c) 2021 Omar Polo * * 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 #include #include /* * 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 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); }