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