fu/c/fcgi.c
Yorhel 48fe393d5f FastCGI: Improve handling of EPIPE while writing response
That would previously result in the worker getting killed with SIGPIPE.
Which works, but we can also recover from that error without restarting
the process.
2026-01-05 08:57:50 +01:00

454 lines
15 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 /* unexpected 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 */
#define FUFE_NOREQ -7 /* protocol-level EOF before we received anything */
#define FUFE_SEND -8 /* error in send() */
#define FUFCGI_MAX_DATA 65535
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 + FUFCGI_MAX_DATA + 255]; /* 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 = (unsigned char)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 = send(ctx->fd, buf, len, MSG_NOSIGNAL);
if (r <= 0) return FUFE_SEND;
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 == FUFE_EOF && ctx->len == 0 ? FUFE_NOREQ : 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];
if (!(p.namelen == 14 && memcmp(p.name, "content-length", 14) == 0)
&& !(p.namelen == 12 && memcmp(p.name, "content-type", 12) == 0)) {
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(pTHX_ fufcgi *ctx) {
fufcgi_rec hdr;
if (ctx->len > 0) {
hdr.len = ctx->len;
hdr.type = FCGI_STDOUT;
hdr.id = ctx->reqid;
if (fufcgi_write_record(ctx, &hdr, ctx->buf) != FUFE_OK)
croak("write error: %s", strerror(errno));
ctx->len = 0;
}
}
static void fufcgi_print(pTHX_ fufcgi *ctx, const char *buf, int len) {
int r;
while (len > 0) {
r = len > FUFCGI_MAX_DATA - ctx->len ? FUFCGI_MAX_DATA - ctx->len : len;
memcpy(ctx->buf+8+ctx->len, buf, r);
ctx->len += r;
len -= r;
buf += r;
if (ctx->len >= FUFCGI_MAX_DATA) fufcgi_flush(aTHX_ ctx);
}
}
static void fufcgi_done(pTHX_ fufcgi *ctx) {
fufcgi_rec hdr;
fufcgi_flush(aTHX_ ctx);
hdr.len = 0;
hdr.type = FCGI_STDOUT;
hdr.id = ctx->reqid;
if (fufcgi_write_record(ctx, &hdr, ctx->buf) != FUFE_OK)
croak("write error: %s", strerror(errno));
memcpy(ctx->buf+8, "\0\0\0\0\0\0\0\0", 8); /* FCGI_REQUEST_COMPLETE */
hdr.type = FCGI_END_REQUEST;
hdr.len = 8;
if (fufcgi_write_record(ctx, &hdr, ctx->buf) != FUFE_OK)
croak("write error: %s", strerror(errno));
ctx->reqid = ctx->len = ctx->off = 0;
}