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