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