main xs_fcgi.h
  1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
  2
  3/*
  4    This is an intentionally-dead-simple FastCGI implementation;
  5    only FCGI_RESPONDER type with *no* FCGI_KEEP_CON flag is supported.
  6    This means only one simultaneous connection and no multiplexing.
  7    It seems it's enough for nginx and OpenBSD's httpd, so here it goes.
  8    Almost fully compatible with xs_httpd.h
  9*/
 10
 11#ifndef _XS_FCGI_H
 12
 13#define _XS_FCGI_H
 14
 15#include "xs.h"
 16#include "xs_url.h"
 17#include "xs_match.h"
 18#include <arpa/inet.h>
 19
 20 xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *id);
 21 void xs_fcgi_response(FILE *f, int status, const xs_dict *headers, const xs_str *body, int b_size, int id);
 22
 23
 24#ifdef XS_IMPLEMENTATION
 25
 26struct fcgi_record_header {
 27    unsigned char  version;
 28    unsigned char  type;
 29    unsigned short id;
 30    unsigned short content_len;
 31    unsigned char  padding_len;
 32    unsigned char  reserved;
 33} __attribute__((packed));
 34
 35/* version */
 36
 37#define FCGI_VERSION_1           1
 38
 39/* types */
 40
 41#define FCGI_BEGIN_REQUEST       1
 42#define FCGI_ABORT_REQUEST       2
 43#define FCGI_END_REQUEST         3
 44#define FCGI_PARAMS              4
 45#define FCGI_STDIN               5
 46#define FCGI_STDOUT              6
 47#define FCGI_STDERR              7
 48#define FCGI_DATA                8
 49#define FCGI_GET_VALUES          9
 50#define FCGI_GET_VALUES_RESULT  10
 51#define FCGI_UNKNOWN_TYPE       11
 52#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
 53
 54struct fcgi_begin_request {
 55    unsigned short role;
 56    unsigned char  flags;
 57    unsigned char  reserved[5];
 58} __attribute__((packed));
 59
 60/* roles */
 61
 62#define FCGI_RESPONDER  1
 63#define FCGI_AUTHORIZER 2
 64#define FCGI_FILTER     3
 65
 66/* flags */
 67
 68#define FCGI_KEEP_CONN  1
 69
 70struct fcgi_end_request {
 71    unsigned int app_status;
 72    unsigned char protocol_status;
 73    unsigned char reserved[3];
 74} __attribute__((packed));
 75
 76/* protocol statuses */
 77
 78#define FCGI_REQUEST_COMPLETE 0
 79#define FCGI_CANT_MPX_CONN    1
 80#define FCGI_OVERLOADED       2
 81#define FCGI_UNKNOWN_ROLE     3
 82
 83
 84xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *fcgi_id)
 85/* keeps receiving FCGI packets until a complete request is finished */
 86{
 87    unsigned char p_buf[100000];
 88    struct fcgi_record_header hdr;
 89    struct fcgi_begin_request *breq = (struct fcgi_begin_request *)&p_buf;
 90    unsigned char *buf = NULL;
 91    int b_size = 0;
 92    xs_dict *req = NULL;
 93    unsigned char p_status = FCGI_REQUEST_COMPLETE;
 94    xs *q_vars = NULL;
 95    xs *p_vars = NULL;
 96
 97    *fcgi_id = -1;
 98
 99    for (;;) {
100        int sz, psz;
101
102        /* read the packet header */
103        if (fread(&hdr, sizeof(hdr), 1, f) != 1)
104            break;
105
106        /* read the packet body */
107        if ((psz = ntohs(hdr.content_len)) > 0) {
108            if ((sz = fread(p_buf, 1, psz, f)) != psz)
109                break;
110        }
111
112        /* read (and drop) the padding */
113        if (hdr.padding_len > 0)
114            fread(p_buf + sz, 1, hdr.padding_len, f);
115
116        switch (hdr.type) {
117        case FCGI_BEGIN_REQUEST:
118            /* fail on unsupported roles */
119            if (ntohs(breq->role) != FCGI_RESPONDER) {
120                p_status = FCGI_UNKNOWN_ROLE;
121                goto end;
122            }
123
124            /* fail on unsupported flags */
125            if (breq->flags & FCGI_KEEP_CONN) {
126                p_status = FCGI_CANT_MPX_CONN;
127                goto end;
128            }
129
130            /* store the id for later */
131            *fcgi_id = (int) hdr.id;
132
133            break;
134
135        case FCGI_PARAMS:
136            /* unknown id? fail */
137            if (hdr.id != *fcgi_id) {
138                p_status = FCGI_CANT_MPX_CONN;
139                goto end;
140            }
141
142            if (psz) {
143                /* add to the buffer */
144                buf = xs_realloc(buf, b_size + psz);
145                memcpy(buf + b_size, p_buf, psz);
146                b_size += psz;
147            }
148            else {
149                /* no size, so the packet is complete; process it */
150                xs *cgi_vars = xs_dict_new();
151
152                req = xs_dict_new();
153
154                int offset = 0;
155                while (offset < b_size) {
156                    unsigned int ksz = buf[offset++];
157
158                    if (ksz & 0x80) {
159                        ksz &= 0x7f;
160                        ksz = (ksz << 8) | buf[offset++];
161                        ksz = (ksz << 8) | buf[offset++];
162                        ksz = (ksz << 8) | buf[offset++];
163                    }
164
165                    unsigned int vsz = buf[offset++];
166                    if (vsz & 0x80) {
167                        vsz &= 0x7f;
168                        vsz = (vsz << 8) | buf[offset++];
169                        vsz = (vsz << 8) | buf[offset++];
170                        vsz = (vsz << 8) | buf[offset++];
171                    }
172
173                    /* get the key */
174                    xs *k = xs_str_new_sz((char *)&buf[offset], ksz);
175                    offset += ksz;
176
177                    /* get the value */
178                    xs *v = xs_str_new_sz((char *)&buf[offset], vsz);
179                    offset += vsz;
180
181                    if (!xs_is_string(k) || !xs_is_string(v))
182                        continue;
183
184                    cgi_vars = xs_dict_append(cgi_vars, k, v);
185
186                    if (strcmp(k, "REQUEST_METHOD") == 0)
187                        req = xs_dict_append(req, "method", v);
188                    else
189                    if (strcmp(k, "REQUEST_URI") == 0) {
190                        req = xs_dict_append(req, "raw_path", v);
191
192                        xs *pnv = xs_split_n(v, "?", 1);
193
194                        /* store the path */
195                        req = xs_dict_append(req, "path", xs_list_get(pnv, 0));
196
197                        /* get the variables */
198                        q_vars = xs_url_vars(xs_list_get(pnv, 1));
199                    }
200                    else
201                    if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|REMOTE_ADDR|HTTP_*")) {
202                        if (xs_startswith(k, "HTTP_"))
203                            k = xs_crop_i(k, 5, 0);
204
205                        k = xs_tolower_i(k);
206                        k = xs_replace_i(k, "_", "-");
207
208                        req = xs_dict_append(req, k, v);
209                    }
210                }
211
212                req = xs_dict_append(req, "cgi_vars", cgi_vars);
213
214                buf    = xs_free(buf);
215                b_size = 0;
216            }
217
218            break;
219
220        case FCGI_STDIN:
221            /* unknown id? fail */
222            if (hdr.id != *fcgi_id) {
223                p_status = FCGI_CANT_MPX_CONN;
224                goto end;
225            }
226
227            if (psz) {
228                /* add to the buffer */
229                buf = xs_realloc(buf, b_size + psz);
230                memcpy(buf + b_size, p_buf, psz);
231                b_size += psz;
232            }
233            else {
234                /* add an asciiz to be able to treat it as a string */
235                buf = xs_realloc(buf, _xs_blk_size(b_size + 1));
236                buf[b_size] = '\0';
237
238                /* fill the payload info and finish */
239                *payload = (xs_str *)buf;
240                *p_size  = b_size;
241
242                const char *ct = xs_dict_get(req, "content-type");
243
244                if (*payload && ct && strcmp(ct, "application/x-www-form-urlencoded") == 0) {
245                    p_vars  = xs_url_vars(*payload);
246                }
247                else
248                if (*payload && ct && xs_startswith(ct, "multipart/form-data")) {
249                    p_vars = xs_multipart_form_data(*payload, *p_size, ct);
250                }
251                else
252                    p_vars = xs_dict_new();
253
254                if (q_vars == NULL)
255                    q_vars = xs_dict_new();
256
257                req = xs_dict_append(req, "q_vars", q_vars);
258                req = xs_dict_append(req, "p_vars", p_vars);
259
260                /* disconnect the payload from the buf variable */
261                buf = NULL;
262
263                goto end;
264            }
265
266            break;
267        }
268    }
269
270end:
271    /* any kind of error? notify and cleanup */
272    if (p_status != FCGI_REQUEST_COMPLETE) {
273        struct fcgi_end_request ereq = {0};
274
275        /* complete the connection */
276        ereq.app_status      = 0;
277        ereq.protocol_status = p_status;
278
279        /* reuse header */
280        hdr.type        = FCGI_ABORT_REQUEST;
281        hdr.content_len = htons(sizeof(ereq));
282
283        fwrite(&hdr, sizeof(hdr), 1, f);
284        fwrite(&ereq, sizeof(ereq), 1, f);
285
286        /* session closed */
287        *fcgi_id = -1;
288
289        /* request dict is not valid */
290        req = xs_free(req);
291    }
292
293    xs_free(buf);
294    return req;
295}
296
297
298void xs_fcgi_response(FILE *f, int status, const xs_dict *headers, const xs_str *body, int b_size, int fcgi_id)
299/* writes an FCGI response */
300{
301    struct fcgi_record_header hdr = {0};
302    struct fcgi_end_request ereq = {0};
303    xs *out = xs_str_new(NULL);
304    const xs_str *k;
305    const xs_str *v;
306
307    /* no previous id? it's an error */
308    if (fcgi_id == -1)
309        return;
310
311    /* create the headers */
312    {
313        xs *s1 = xs_fmt("status: %d\r\n", status);
314        out = xs_str_cat(out, s1);
315    }
316
317    xs_dict_foreach(headers, k, v) {
318        xs *s1 = xs_fmt("%s: %s\r\n", k, v);
319        out = xs_str_cat(out, s1);
320    }
321
322    if (b_size > 0) {
323        xs *s1 = xs_fmt("content-length: %d\r\n", b_size);
324        out = xs_str_cat(out, s1);
325    }
326
327    out = xs_str_cat(out, "\r\n");
328
329    /* everything is text by now */
330    int size = strlen(out);
331
332    /* add the body */
333    if (body != NULL && b_size > 0) {
334        out = xs_append_m(out, body, b_size);
335        size += b_size;
336    }
337
338    /* now send all the STDOUT in packets */
339    hdr.version = FCGI_VERSION_1;
340    hdr.type    = FCGI_STDOUT;
341    hdr.id      = fcgi_id;
342
343    int offset = 0;
344
345    while (offset < size) {
346        size_t sz = size - offset;
347        if (sz > 0xffff)
348            sz = 0xffff;
349
350        hdr.content_len = htons(sz);
351
352        /* write or fail */
353        if (!fwrite(&hdr, sizeof(hdr), 1, f) || fwrite(out + offset, 1, sz, f) != sz)
354            return;
355
356        offset += sz;
357    }
358
359    /* final STDOUT packet with 0 size */
360    hdr.content_len = 0;
361    if (!fwrite(&hdr, sizeof(hdr), 1, f))
362        return;
363
364    /* complete the connection */
365    ereq.app_status      = 0;
366    ereq.protocol_status = FCGI_REQUEST_COMPLETE;
367
368    hdr.type        = FCGI_END_REQUEST;
369    hdr.content_len = htons(sizeof(ereq));
370
371    if (fwrite(&hdr, sizeof(hdr), 1, f))
372        fwrite(&ereq, sizeof(ereq), 1, f);
373}
374
375
376#endif /* XS_IMPLEMENTATION */
377
378#endif /* XS_URL_H */