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