snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

xs_fcgi.h (11188B)


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