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