diff options
Diffstat (limited to 'xs_fcgi.h')
| -rw-r--r-- | xs_fcgi.h | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/xs_fcgi.h b/xs_fcgi.h new file mode 100644 index 0000000..97f14d5 --- /dev/null +++ b/xs_fcgi.h | |||
| @@ -0,0 +1,365 @@ | |||
| 1 | /* copyright (c) 2022 - 2023 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 | struct 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 | |||
| 49 | struct 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 | |||
| 65 | struct 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 | |||
| 79 | xs_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 | /* the packet is complete; fill the payload info and finish */ | ||
| 226 | *payload = (xs_str *)buf; | ||
| 227 | *p_size = b_size; | ||
| 228 | |||
| 229 | const char *ct = xs_dict_get(req, "content-type"); | ||
| 230 | |||
| 231 | if (*payload && ct && strcmp(ct, "application/x-www-form-urlencoded") == 0) { | ||
| 232 | xs *upl = xs_url_dec(*payload); | ||
| 233 | p_vars = xs_url_vars(upl); | ||
| 234 | } | ||
| 235 | else | ||
| 236 | if (*payload && ct && xs_startswith(ct, "multipart/form-data")) { | ||
| 237 | p_vars = xs_multipart_form_data(*payload, *p_size, ct); | ||
| 238 | } | ||
| 239 | else | ||
| 240 | p_vars = xs_dict_new(); | ||
| 241 | |||
| 242 | if (q_vars == NULL) | ||
| 243 | q_vars = xs_dict_new(); | ||
| 244 | |||
| 245 | req = xs_dict_append(req, "q_vars", q_vars); | ||
| 246 | req = xs_dict_append(req, "p_vars", p_vars); | ||
| 247 | |||
| 248 | /* disconnect the payload from the buf variable */ | ||
| 249 | buf = NULL; | ||
| 250 | |||
| 251 | goto end; | ||
| 252 | } | ||
| 253 | |||
| 254 | break; | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | end: | ||
| 259 | /* any kind of error? notify and cleanup */ | ||
| 260 | if (p_status != FCGI_REQUEST_COMPLETE) { | ||
| 261 | struct fcgi_end_request ereq = {0}; | ||
| 262 | |||
| 263 | /* complete the connection */ | ||
| 264 | ereq.app_status = 0; | ||
| 265 | ereq.protocol_status = p_status; | ||
| 266 | |||
| 267 | /* reuse header */ | ||
| 268 | hdr.type = FCGI_ABORT_REQUEST; | ||
| 269 | hdr.content_len = htons(sizeof(ereq)); | ||
| 270 | |||
| 271 | fwrite(&hdr, sizeof(hdr), 1, f); | ||
| 272 | fwrite(&ereq, sizeof(ereq), 1, f); | ||
| 273 | |||
| 274 | /* session closed */ | ||
| 275 | *fcgi_id = -1; | ||
| 276 | |||
| 277 | /* request dict is not valid */ | ||
| 278 | req = xs_free(req); | ||
| 279 | } | ||
| 280 | |||
| 281 | xs_free(buf); | ||
| 282 | return req; | ||
| 283 | } | ||
| 284 | |||
| 285 | |||
| 286 | void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int fcgi_id) | ||
| 287 | /* writes an FCGI response */ | ||
| 288 | { | ||
| 289 | struct fcgi_record_header hdr = {0}; | ||
| 290 | struct fcgi_end_request ereq = {0}; | ||
| 291 | xs *out = xs_str_new(NULL); | ||
| 292 | xs_dict *p; | ||
| 293 | xs_str *k; | ||
| 294 | xs_str *v; | ||
| 295 | |||
| 296 | /* no previous id? it's an error */ | ||
| 297 | if (fcgi_id == -1) | ||
| 298 | return; | ||
| 299 | |||
| 300 | /* create the headers */ | ||
| 301 | { | ||
| 302 | xs *s1 = xs_fmt("status: %d\r\n", status); | ||
| 303 | out = xs_str_cat(out, s1); | ||
| 304 | } | ||
| 305 | |||
| 306 | p = headers; | ||
| 307 | while (xs_dict_iter(&p, &k, &v)) { | ||
| 308 | xs *s1 = xs_fmt("%s: %s\r\n", k, v); | ||
| 309 | out = xs_str_cat(out, s1); | ||
| 310 | } | ||
| 311 | |||
| 312 | if (b_size > 0) { | ||
| 313 | xs *s1 = xs_fmt("content-length: %d\r\n", b_size); | ||
| 314 | out = xs_str_cat(out, s1); | ||
| 315 | } | ||
| 316 | |||
| 317 | out = xs_str_cat(out, "\r\n"); | ||
| 318 | |||
| 319 | /* everything is text by now */ | ||
| 320 | int size = strlen(out); | ||
| 321 | |||
| 322 | /* add the body */ | ||
| 323 | if (body != NULL && b_size > 0) | ||
| 324 | out = xs_append_m(out, body, b_size); | ||
| 325 | |||
| 326 | /* now send all the STDOUT in packets */ | ||
| 327 | hdr.version = FCGI_VERSION_1; | ||
| 328 | hdr.type = FCGI_STDOUT; | ||
| 329 | hdr.id = fcgi_id; | ||
| 330 | |||
| 331 | size += b_size; | ||
| 332 | int offset = 0; | ||
| 333 | |||
| 334 | while (offset < size) { | ||
| 335 | int sz = size - offset; | ||
| 336 | if (sz > 0xffff) | ||
| 337 | sz = 0xffff; | ||
| 338 | |||
| 339 | hdr.content_len = htons(sz); | ||
| 340 | |||
| 341 | fwrite(&hdr, sizeof(hdr), 1, f); | ||
| 342 | fwrite(out + offset, 1, sz, f); | ||
| 343 | |||
| 344 | offset += sz; | ||
| 345 | } | ||
| 346 | |||
| 347 | /* final STDOUT packet with 0 size */ | ||
| 348 | hdr.content_len = 0; | ||
| 349 | fwrite(&hdr, sizeof(hdr), 1, f); | ||
| 350 | |||
| 351 | /* complete the connection */ | ||
| 352 | ereq.app_status = 0; | ||
| 353 | ereq.protocol_status = FCGI_REQUEST_COMPLETE; | ||
| 354 | |||
| 355 | hdr.type = FCGI_END_REQUEST; | ||
| 356 | hdr.content_len = htons(sizeof(ereq)); | ||
| 357 | |||
| 358 | fwrite(&hdr, sizeof(hdr), 1, f); | ||
| 359 | fwrite(&ereq, sizeof(ereq), 1, f); | ||
| 360 | } | ||
| 361 | |||
| 362 | |||
| 363 | #endif /* XS_IMPLEMENTATION */ | ||
| 364 | |||
| 365 | #endif /* XS_URL_H */ | ||