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 */