2.53 xs_fcgi.h
  1/* copyright (c) 2022 - 2024 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, xs_dict *headers, 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                    cgi_vars = xs_dict_append(cgi_vars, k, v);
177
178                    if (strcmp(k, "REQUEST_METHOD") == 0)
179                        req = xs_dict_append(req, "method", v);
180                    else
181                    if (strcmp(k, "REQUEST_URI") == 0) {
182                        xs *udp = xs_url_dec(v);
183                        xs *pnv = xs_split_n(udp, "?", 1);
184
185                        /* store the path */
186                        req = xs_dict_append(req, "path", xs_list_get(pnv, 0));
187
188                        /* get the variables */
189                        q_vars = xs_url_vars(xs_list_get(pnv, 1));
190                    }
191                    else
192                    if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|HTTP_*")) {
193                        if (xs_startswith(k, "HTTP_"))
194                            k = xs_crop_i(k, 5, 0);
195
196                        k = xs_tolower_i(k);
197                        k = xs_replace_i(k, "_", "-");
198
199                        req = xs_dict_append(req, k, v);
200                    }
201                }
202
203                req = xs_dict_append(req, "cgi_vars", cgi_vars);
204
205                buf    = xs_free(buf);
206                b_size = 0;
207            }
208
209            break;
210
211        case FCGI_STDIN:
212            /* unknown id? fail */
213            if (hdr.id != *fcgi_id) {
214                p_status = FCGI_CANT_MPX_CONN;
215                goto end;
216            }
217
218            if (psz) {
219                /* add to the buffer */
220                buf = xs_realloc(buf, b_size + psz);
221                memcpy(buf + b_size, p_buf, psz);
222                b_size += psz;
223            }
224            else {
225                /* add an asciiz to be able to treat it as a string */
226                buf = xs_realloc(buf, _xs_blk_size(b_size + 1));
227                buf[b_size] = '\0';
228
229                /* fill the payload info and finish */
230                *payload = (xs_str *)buf;
231                *p_size  = b_size;
232
233                const char *ct = xs_dict_get(req, "content-type");
234
235                if (*payload && ct && strcmp(ct, "application/x-www-form-urlencoded") == 0) {
236                    xs *upl = xs_url_dec(*payload);
237                    p_vars  = xs_url_vars(upl);
238                }
239                else
240                if (*payload && ct && xs_startswith(ct, "multipart/form-data")) {
241                    p_vars = xs_multipart_form_data(*payload, *p_size, ct);
242                }
243                else
244                    p_vars = xs_dict_new();
245
246                if (q_vars == NULL)
247                    q_vars = xs_dict_new();
248
249                req = xs_dict_append(req, "q_vars", q_vars);
250                req = xs_dict_append(req, "p_vars", p_vars);
251
252                /* disconnect the payload from the buf variable */
253                buf = NULL;
254
255                goto end;
256            }
257
258            break;
259        }
260    }
261
262end:
263    /* any kind of error? notify and cleanup */
264    if (p_status != FCGI_REQUEST_COMPLETE) {
265        struct fcgi_end_request ereq = {0};
266
267        /* complete the connection */
268        ereq.app_status      = 0;
269        ereq.protocol_status = p_status;
270
271        /* reuse header */
272        hdr.type        = FCGI_ABORT_REQUEST;
273        hdr.content_len = htons(sizeof(ereq));
274
275        fwrite(&hdr, sizeof(hdr), 1, f);
276        fwrite(&ereq, sizeof(ereq), 1, f);
277
278        /* session closed */
279        *fcgi_id = -1;
280
281        /* request dict is not valid */
282        req = xs_free(req);
283    }
284
285    xs_free(buf);
286    return req;
287}
288
289
290void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int fcgi_id)
291/* writes an FCGI response */
292{
293    struct fcgi_record_header hdr = {0};
294    struct fcgi_end_request ereq = {0};
295    xs *out = xs_str_new(NULL);
296    xs_str *k;
297    xs_str *v;
298
299    /* no previous id? it's an error */
300    if (fcgi_id == -1)
301        return;
302
303    /* create the headers */
304    {
305        xs *s1 = xs_fmt("status: %d\r\n", status);
306        out = xs_str_cat(out, s1);
307    }
308
309    int c = 0;
310    while (xs_dict_next(headers, &k, &v, &c)) {
311        xs *s1 = xs_fmt("%s: %s\r\n", k, v);
312        out = xs_str_cat(out, s1);
313    }
314
315    if (b_size > 0) {
316        xs *s1 = xs_fmt("content-length: %d\r\n", b_size);
317        out = xs_str_cat(out, s1);
318    }
319
320    out = xs_str_cat(out, "\r\n");
321
322    /* everything is text by now */
323    int size = strlen(out);
324
325    /* add the body */
326    if (body != NULL && b_size > 0) {
327        out = xs_append_m(out, body, b_size);
328        size += b_size;
329    }
330
331    /* now send all the STDOUT in packets */
332    hdr.version = FCGI_VERSION_1;
333    hdr.type    = FCGI_STDOUT;
334    hdr.id      = fcgi_id;
335
336    int offset = 0;
337
338    while (offset < size) {
339        size_t sz = size - offset;
340        if (sz > 0xffff)
341            sz = 0xffff;
342
343        hdr.content_len = htons(sz);
344
345        /* write or fail */
346        if (!fwrite(&hdr, sizeof(hdr), 1, f) || fwrite(out + offset, 1, sz, f) != sz)
347            return;
348
349        offset += sz;
350    }
351
352    /* final STDOUT packet with 0 size */
353    hdr.content_len = 0;
354    if (!fwrite(&hdr, sizeof(hdr), 1, f))
355        return;
356
357    /* complete the connection */
358    ereq.app_status      = 0;
359    ereq.protocol_status = FCGI_REQUEST_COMPLETE;
360
361    hdr.type        = FCGI_END_REQUEST;
362    hdr.content_len = htons(sizeof(ereq));
363
364    if (fwrite(&hdr, sizeof(hdr), 1, f))
365        fwrite(&ereq, sizeof(ereq), 1, f);
366}
367
368
369#endif /* XS_IMPLEMENTATION */
370
371#endif /* XS_URL_H */