fu/c/fcgi.c
Yorhel d9fba4e8d8 FU: Add FastCGI support + bunch of fixes
I initially planned to only implement the bare minimum to support
FastCGI under nginx, but ended up implementing the full protocol
instead. This is more code than I had expected and the code is also less
trivial than I had hoped. Will need to do more testing, pretty sure
there's bugs left.

Also TODO: test under alternative process managers + document
FU_LISTEN_PROTO.

I've also removed the max_request_body setting, this is something that
really ought to be configured in the web server instead.
2025-02-17 15:15:38 +01:00

427 lines
14 KiB
C

#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 FUFE_OK 0
#define FUFE_EOF -1 /* protocol-level EOF */
#define FUFE_IO -2
#define FUFE_PROTO -3
#define FUFE_PLEN -4
#define FUFE_CLEN -5
#define FUFE_ABORT -6 /* explicit abort or client-level EOF */
typedef struct {
SV *self;
int fd;
int maxproc;
int keepconn;
int reqid;
HV *headers;
HV *params;
/* Single buffer for reading & writing, we only do one thing at a time */
char buf[8 + 65536 + 256]; /* fits a maximum-length fcgi record */
int len; /* total number of bytes in the buffer */
int off; /* number of bytes consumed */
} fufcgi;
typedef struct {
unsigned char type;
unsigned short id;
int len;
char *data;
} fufcgi_rec;
/* Incremental param length & name parser */
typedef enum {
FUFC_INIT, FUFC_L1, FUFC_L2, FUFC_L3,
FUFC_V0, FUFC_V1, FUFC_V2, FUFC_V3,
FUFC_N0, FUFC_NX
} fufcgi_paramstate;
typedef struct {
int namelen;
int vallen;
int state;
int namerd;
char *name;
char namebuf[128]; /* We don't support longer param names */
} fufcgi_param;
/* Returns NULL on error or ptr to value (or 'end' if !done) */
static char *fufcgi_param_parse(fufcgi_param *p, char *buf, char *end) {
while (buf < end) {
switch (p->state) {
case FUFC_INIT:
p->vallen = p->namerd = 0;
if (*buf & 0x80) {
p->namelen = (*buf & 0x1f) << 24;
p->state = FUFC_L1;
} else {
p->namelen = *buf;
p->state = FUFC_V0;
}
break;
case FUFC_L1:
p->namelen |= ((unsigned char)*buf) << 16;
p->state = FUFC_L2;
break;
case FUFC_L2:
p->namelen |= ((unsigned char)*buf) << 8;
p->state = FUFC_L3;
break;
case FUFC_L3:
p->namelen |= (unsigned char)*buf;
p->state = FUFC_V0;
if (p->namelen > (int)sizeof(p->namebuf)) return NULL;
break;
case FUFC_V0:
if (*buf & 0x80) {
p->vallen = (*buf & 0x1f) << 24;
p->state = FUFC_V1;
} else {
p->vallen = *buf;
p->state = p->namelen ? FUFC_N0 : FUFC_INIT;
}
break;
case FUFC_V1:
p->vallen |= ((unsigned char)*buf) << 16;
if (p->vallen) return NULL; /* Let's just disallow param values > 64 KiB */
p->state = FUFC_V2;
break;
case FUFC_V2:
p->vallen |= ((unsigned char)*buf) << 8;
p->state = FUFC_V3;
break;
case FUFC_V3:
p->vallen |= (unsigned char)*buf;
p->state = FUFC_N0;
break;
case FUFC_N0:
if (p->namelen <= end - buf) {
p->name = buf;
p->state = FUFC_INIT;
return buf + p->namelen;
} else {
p->name = p->namebuf;
p->name[0] = *buf;
p->namerd = 1;
p->state = FUFC_NX;
}
break;
case FUFC_NX:
p->name[p->namerd++] = *buf;
if (p->namerd == p->namelen) {
p->state = FUFC_INIT;
return buf + 1;
}
break;
}
buf++;
}
return buf;
}
static int fufcgi_fill(fufcgi *ctx, int len) {
if ((int)sizeof(ctx->buf) - ctx->off < len) {
memmove(ctx->buf, ctx->buf+ctx->off, ctx->len - ctx->off);
ctx->len -= ctx->off;
ctx->off = 0;
}
while (ctx->len - ctx->off < len) {
ssize_t r = read(ctx->fd, ctx->buf+ctx->len, sizeof(ctx->buf) - ctx->len);
if (r <= 0) return r == 0 ? FUFE_EOF : FUFE_IO;
ctx->len += r;
}
return FUFE_OK;
}
static int fufcgi_read_record(fufcgi *ctx, fufcgi_rec *rec) {
int r;
if ((r = fufcgi_fill(ctx, 8)) != FUFE_OK) return r;
if (ctx->buf[ctx->off] != 1) return FUFE_PROTO; /* version */
rec->type = ctx->buf[ctx->off+1];
rec->id = fu_frombeU(16, ctx->buf+ctx->off+2);
rec->len = fu_frombeU(16, ctx->buf+ctx->off+4);
int pad = ctx->buf[ctx->off+6];
ctx->off += 8;
if ((r = fufcgi_fill(ctx, rec->len + pad)) != FUFE_OK) return r;
rec->data = ctx->buf + ctx->off;
ctx->off += rec->len + pad;
return FUFE_OK;
}
/* Unbuffered write of a single record, first 8 bytes of 'buf' are filled out
* by this function, record contents must come after. */
static int fufcgi_write_record(fufcgi *ctx, fufcgi_rec *hdr, char *buf) {
buf[0] = 1;
buf[1] = hdr->type;
fu_tobeU(16, buf+2, hdr->id);
fu_tobeU(16, buf+4, hdr->len);
buf[6] = 0;
buf[7] = 0;
int len = hdr->len + 8;
while (len > 0) {
int r = write(ctx->fd, buf, len);
if (r <= 0) return r == 0 ? FUFE_EOF : FUFE_IO;
buf += r;
len -= r;
}
return FUFE_OK;
}
static int fufcgi_handle_values(fufcgi *ctx, fufcgi_rec *rec, char *buf) {
int reslen = 8;
char *param = rec->data;
char *end = rec->data + rec->len;
fufcgi_param p;
p.state = FUFC_INIT;
while (param < end) {
if ((param = fufcgi_param_parse(&p, param, end)) == NULL) return FUFE_PLEN;
if (p.state != FUFC_INIT) return FUFE_PROTO;
if (p.vallen > end - param) return FUFE_PROTO;
if (reslen >= 100) return FUFE_PROTO; /* implies requested params were duplicated */
if (p.namelen == 14 && memcmp(p.name, "FCGI_MAX_CONNS", 14) == 0) {
memcpy(buf+reslen, "\x0e\0FCGI_MAX_CONNS", 16);
int l = sprintf(buf+reslen+16, "%d", ctx->maxproc);
buf[reslen+1] = l;
reslen += 16 + l;
} else if (p.namelen == 13 && memcmp(p.name, "FCGI_MAX_REQS", 13) == 0) {
memcpy(buf+reslen, "\x0d\0FCGI_MAX_REQS", 15);
int l = sprintf(buf+reslen+15, "%d", ctx->maxproc);
buf[reslen+1] = l;
reslen += 15 + l;
} else if (p.namelen == 15 && memcmp(p.name, "FCGI_MPXS_CONNS", 15) == 0) {
memcpy(buf+reslen, "\x0f\1FCGI_MPXS_CONNS0", 18);
reslen += 18;
}
param += p.vallen;
}
rec->type = FCGI_GET_VALUES_RESULT;
rec->len = reslen - 8;
return fufcgi_write_record(ctx, rec, buf);
}
/* Read a PARAMS/STDIN/ABORT record corresponding to the current id, starts
* reading a new request if id=0. */
static int fufcgi_read_req_record(fufcgi *ctx, fufcgi_rec *rec) {
int r;
char tmp[128]; /* Large enough for a FCGI_GET_VALUES_RESULT */
while (1) {
if ((r = fufcgi_read_record(ctx, rec)) != FUFE_OK) return r;
switch (rec->type) {
case FCGI_PARAMS:
case FCGI_STDIN:
case FCGI_ABORT_REQUEST:
if (rec->id != ctx->reqid) return FUFE_PROTO;
return FUFE_OK;
case FCGI_BEGIN_REQUEST:
if (!rec->id || rec->id == ctx->reqid) return FUFE_PROTO;
if (rec->len != 8) return FUFE_PROTO;
ctx->keepconn = rec->data[2] & 1;
if (rec->data[0] != 0 || rec->data[1] != 1) { /* FCGI_RESPONDER */
memcpy(tmp+8, "\0\0\0\0\3\0\0\0", 8); /* FCGI_UNKNOWN_ROLE */
rec->type = FCGI_END_REQUEST;
rec->len = 8;
if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
if (!ctx->keepconn) return FUFE_EOF;
} else if (ctx->reqid) {
memcpy(tmp+8, "\0\0\0\0\1\0\0\0", 8); /* FCGI_CANT_MPX_CONN */
rec->type = FCGI_END_REQUEST;
rec->len = 8;
if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
if (!ctx->keepconn) return FUFE_EOF;
} else {
ctx->reqid = rec->id;
}
break;
case FCGI_GET_VALUES:
if (rec->id) return FUFE_PROTO;
if ((r = fufcgi_handle_values(ctx, rec, tmp)) != FUFE_OK) return r;
break;
default:
memset(tmp+8, 0, 8);
tmp[8] = rec->type;
rec->type = FCGI_UNKNOWN_TYPE;
rec->len = 8;
rec->id = 0;
if ((r = fufcgi_write_record(ctx, rec, tmp)) != FUFE_OK) return r;
break;
}
}
}
static int fufcgi_read_params(pTHX_ fufcgi *ctx, fufcgi_rec *rec) {
int r;
fufcgi_param p;
p.state = FUFC_INIT;
SV *valsv = NULL;
char *val = NULL;
int valleft = 0;
while (1) {
if ((r = fufcgi_read_req_record(ctx, rec)) != FUFE_OK) return r;
if (rec->type == FCGI_ABORT_REQUEST) return FUFE_OK;
if (rec->type != FCGI_PARAMS) return FUFE_PROTO;
if (rec->len == 0) return p.state != FUFC_INIT || valleft ? FUFE_PROTO : FUFE_OK;
char *buf = rec->data;
char *end = rec->data + rec->len;
while (buf < end) {
if (valleft) {
r = valleft > end - buf ? end - buf : valleft;
if (val) {
memcpy(val, buf, r);
val += r;
}
valleft -= r;
buf += r;
if (val && !valleft) {
*val = 0;
SvCUR_set(valsv, p.vallen);
}
continue;
}
if ((buf = fufcgi_param_parse(&p, buf, end)) == NULL) return FUFE_PLEN;
if (p.state != FUFC_INIT) break;
valsv = NULL;
val = NULL;
valleft = p.vallen;
/* https://www.rfc-editor.org/rfc/rfc3875 */
/* Request header */
if (p.namelen > 5 && memcmp(p.name, "HTTP_", 5) == 0) {
p.namelen -= 5;
p.name += 5;
for (r=0; r<p.namelen; r++)
p.name[r] = p.name[r] == '_' ? '-' : p.name[r] >= 'A' && p.name[r] <= 'Z' ? p.name[r] | 0x20 : p.name[r];
valsv = newSV(p.vallen+1);
hv_store(ctx->headers, p.name, p.namelen, valsv, 0);
} else if (p.namelen == 14 && memcmp(p.name, "CONTENT_LENGTH", 14) == 0) {
valsv = newSV(p.vallen+1);
hv_stores(ctx->headers, "content-length", valsv);
} else if (p.namelen == 12 && memcmp(p.name, "CONTENT_TYPE", 12) == 0) {
valsv = newSV(p.vallen+1);
hv_stores(ctx->headers, "content-type", valsv);
} else if (p.namelen == 11 && memcmp(p.name, "REMOTE_ADDR", 11) == 0) {
valsv = newSV(p.vallen+1);
hv_stores(ctx->params, "ip", valsv);
} else if (p.namelen == 12 && memcmp(p.name, "QUERY_STRING", 12) == 0) {
valsv = newSV(p.vallen+1);
hv_stores(ctx->params, "qs", valsv);
} else if (p.namelen == 14 && memcmp(p.name, "REQUEST_METHOD", 14) == 0) {
valsv = newSV(p.vallen+1);
hv_stores(ctx->params, "method", valsv);
/* Not in rfc3875; there's no standardized parameter for the URI,
* but every FastCGI-capable web server includes this one */
} else if (p.namelen == 11 && memcmp(p.name, "REQUEST_URI", 11) == 0) {
valsv = newSV(p.vallen+1);
hv_stores(ctx->params, "path", valsv);
} else { /* ignore */ }
if (valsv) {
SvPOK_only(valsv);
val = SvPVX(valsv);
*val = 0; /* in case vallen = 0 */
}
}
}
}
static int fufcgi_read_req(pTHX_ fufcgi *ctx, SV *headers, SV *params) {
if (ctx->reqid) fu_confess("Invalid attempt to read FastCGI request before finishing the previous one");
fufcgi_rec rec;
int r;
ctx->off = ctx->len = 0;
ctx->headers = (HV *)SvRV(headers);
ctx->params = (HV *)SvRV(params);
if ((r = fufcgi_read_params(aTHX_ ctx, &rec)) != FUFE_OK) return r;
int stdinlen = 0;
SV **contentlength = hv_fetchs(ctx->headers, "content-length", 0);
if (contentlength && *contentlength) {
UV uv = 0;
char *v = SvPV_nolen(*contentlength);
if (*v && !grok_atoUV(v, &uv, NULL)) return FUFE_CLEN;
if (uv >= INT_MAX) return FUFE_CLEN;
stdinlen = uv;
}
SV *sv = newSV(stdinlen+1);
hv_stores(ctx->params, "body", sv);
SvPOK_only(sv);
char *stdinbuf = SvPVX(sv);
int stdinleft = stdinlen;
while (1) {
if (rec.type == FCGI_ABORT_REQUEST) return FUFE_ABORT;
else if (rec.type == FCGI_PARAMS) {
if (rec.len != 0) return FUFE_PROTO;
} else if (rec.type == FCGI_STDIN) {
if (rec.len == 0) {
*stdinbuf = 0;
SvCUR_set(sv, stdinlen - stdinleft);
return stdinleft == 0 ? FUFE_OK : FUFE_ABORT;
}
if (rec.len > stdinleft) return FUFE_PROTO;
memcpy(stdinbuf, rec.data, rec.len);
stdinbuf += rec.len;
stdinleft -= rec.len;
} else {
return FUFE_PROTO;
}
if ((r = fufcgi_read_req_record(ctx, &rec)) != FUFE_OK) return r;
}
}
static void fufcgi_flush(fufcgi *ctx) {
fufcgi_rec hdr;
if (ctx->off > 8) {
hdr.len = ctx->off - 8;
hdr.type = FCGI_STDOUT;
hdr.id = ctx->reqid;
fufcgi_write_record(ctx, &hdr, ctx->buf);
ctx->off = 8;
}
}
static void fufcgi_print(fufcgi *ctx, const char *buf, int len) {
int r;
while (len > 0) {
r = len > 65535 - ctx->off ? 65535 - ctx->off : len;
memcpy(ctx->buf+ctx->off, buf, r);
ctx->off += r;
len -= r;
buf += r;
if (ctx->off >= 65535) fufcgi_flush(ctx);
}
}