From 6cd3e768908e99c9bfb5d4f896b9765a477e4214 Mon Sep 17 00:00:00 2001 From: default Date: Tue, 17 Oct 2023 20:02:08 +0200 Subject: [PATCH] Added FastCGI support. --- Makefile | 4 +- httpd.c | 22 +++- snac.c | 1 + utils.c | 3 +- xs_fcgi.h | 365 +++++++++++++++++++++++++++++++++++++++++++++++++++ xs_version.h | 2 +- 6 files changed, 389 insertions(+), 8 deletions(-) create mode 100644 xs_fcgi.h diff --git a/Makefile b/Makefile index 9cbdabc..2090e7c 100644 --- a/Makefile +++ b/Makefile @@ -43,13 +43,13 @@ html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_httpd.h xs_mime.h \ - xs_time.h xs_openssl.h snac.h + xs_time.h xs_openssl.h xs_fcgi.h snac.h main.o: main.c xs.h xs_io.h xs_json.h snac.h mastoapi.o: mastoapi.c xs.h xs_openssl.h xs_json.h xs_io.h xs_time.h \ xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h snac.h snac.o: snac.c xs.h xs_io.h xs_unicode.h xs_json.h xs_curl.h xs_openssl.h \ xs_socket.h xs_url.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h \ - xs_glob.h xs_random.h xs_match.h snac.h + xs_glob.h xs_random.h xs_match.h xs_fcgi.h snac.h upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ xs_random.h snac.h diff --git a/httpd.c b/httpd.c index 213f751..aed15dc 100644 --- a/httpd.c +++ b/httpd.c @@ -9,6 +9,7 @@ #include "xs_mime.h" #include "xs_time.h" #include "xs_openssl.h" +#include "xs_fcgi.h" #include "snac.h" @@ -24,6 +25,8 @@ #include #endif +int use_fcgi = 0; + int srv_running = 0; /* nodeinfo 2.0 template */ @@ -199,8 +202,12 @@ void httpd_connection(FILE *f) xs *etag = NULL; int p_size = 0; char *p; + int fcgi_id; - req = xs_httpd_request(f, &payload, &p_size); + if (use_fcgi) + req = xs_fcgi_request(f, &payload, &p_size, &fcgi_id); + else + req = xs_httpd_request(f, &payload, &p_size); if (req == NULL) { /* probably because a timeout */ @@ -330,7 +337,10 @@ void httpd_connection(FILE *f) headers = xs_dict_append(headers, "access-control-allow-origin", "*"); headers = xs_dict_append(headers, "access-control-allow-headers", "*"); - xs_httpd_response(f, status, headers, body, b_size); + if (use_fcgi) + xs_fcgi_response(f, status, headers, body, b_size, fcgi_id); + else + xs_httpd_response(f, status, headers, body, b_size); fclose(f); @@ -550,6 +560,8 @@ void httpd(void) char sem_name[24]; sem_t anon_job_sem; + use_fcgi = xs_type(xs_dict_get(srv_config, "fastcgi")) == XSTYPE_TRUE; + address = xs_dict_get(srv_config, "address"); port = xs_number_str(xs_dict_get(srv_config, "port")); @@ -564,7 +576,8 @@ void httpd(void) signal(SIGTERM, term_handler); signal(SIGINT, term_handler); - srv_log(xs_fmt("httpd start %s:%s %s", address, port, USER_AGENT)); + srv_log(xs_fmt("httpd%s start %s:%s %s", use_fcgi ? " (FastCGI)" : "", + address, port, USER_AGENT)); /* show the number of usable file descriptors */ struct rlimit r; @@ -651,5 +664,6 @@ void httpd(void) xs *uptime = xs_str_time_diff(time(NULL) - start_time); - srv_log(xs_fmt("httpd stop %s:%s (run time: %s)", address, port, uptime)); + srv_log(xs_fmt("httpd%s stop %s:%s (run time: %s)", use_fcgi ? " (FastCGI)" : "", + address, port, uptime)); } diff --git a/snac.c b/snac.c index 1ea61ff..ac371c5 100644 --- a/snac.c +++ b/snac.c @@ -19,6 +19,7 @@ #include "xs_glob.h" #include "xs_random.h" #include "xs_match.h" +#include "xs_fcgi.h" #include "snac.h" diff --git a/utils.c b/utils.c index dd09be6..21922d1 100644 --- a/utils.c +++ b/utils.c @@ -29,7 +29,8 @@ static const char *default_srv_config = "{" "\"admin_email\": \"\"," "\"admin_account\": \"\"," "\"title\": \"\"," - "\"short_description\": \"\"" + "\"short_description\": \"\"," + "\"fastcgi\": false" "}"; static const char *default_css = 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 @@ +/* copyright (c) 2022 - 2023 grunfink et al. / MIT license */ + +/* + This is an intentionally-dead-simple FastCGI implementation; + only FCGI_RESPONDER type with *no* FCGI_KEEP_CON flag is supported. + This means only one simultaneous connection and no multiplexing. + It seems it's enough for nginx and OpenBSD's httpd, so here it goes. + Almost fully compatible with xs_httpd.h +*/ + +#ifndef _XS_FCGI_H + +#define _XS_FCGI_H + + xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *id); + void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int id); + + +#ifdef XS_IMPLEMENTATION + +struct fcgi_record_header { + unsigned char version; + unsigned char type; + unsigned short id; + unsigned short content_len; + unsigned char padding_len; + unsigned char reserved; +} __attribute__((packed)); + +/* version */ + +#define FCGI_VERSION_1 1 + +/* types */ + +#define FCGI_BEGIN_REQUEST 1 +#define FCGI_ABORT_REQUEST 2 +#define FCGI_END_REQUEST 3 +#define FCGI_PARAMS 4 +#define FCGI_STDIN 5 +#define FCGI_STDOUT 6 +#define FCGI_STDERR 7 +#define FCGI_DATA 8 +#define FCGI_GET_VALUES 9 +#define FCGI_GET_VALUES_RESULT 10 +#define FCGI_UNKNOWN_TYPE 11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +struct fcgi_begin_request { + unsigned short role; + unsigned char flags; + unsigned char reserved[5]; +} __attribute__((packed)); + +/* roles */ + +#define FCGI_RESPONDER 1 +#define FCGI_AUTHORIZER 2 +#define FCGI_FILTER 3 + +/* flags */ + +#define FCGI_KEEP_CONN 1 + +struct fcgi_end_request { + unsigned int app_status; + unsigned char protocol_status; + unsigned char reserved[3]; +} __attribute__((packed)); + +/* protocol statuses */ + +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN 1 +#define FCGI_OVERLOADED 2 +#define FCGI_UNKNOWN_ROLE 3 + + +xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *fcgi_id) +/* keeps receiving FCGI packets until a complete request is finished */ +{ + unsigned char p_buf[100000]; + struct fcgi_record_header hdr; + struct fcgi_begin_request *breq = (struct fcgi_begin_request *)&p_buf; + unsigned char *buf = NULL; + int b_size = 0; + xs_dict *req = NULL; + unsigned char p_status = FCGI_REQUEST_COMPLETE; + xs *q_vars = NULL; + xs *p_vars = NULL; + + *fcgi_id = -1; + + for (;;) { + int sz, psz; + + /* read the packet header */ + if (fread(&hdr, sizeof(hdr), 1, f) != 1) + break; + + /* read the packet body */ + if ((psz = ntohs(hdr.content_len)) > 0) { + if ((sz = fread(p_buf, 1, psz, f)) != psz) + break; + } + + /* read (and drop) the padding */ + if (hdr.padding_len > 0) + fread(p_buf + sz, 1, hdr.padding_len, f); + + switch (hdr.type) { + case FCGI_BEGIN_REQUEST: + /* fail on unsupported roles */ + if (ntohs(breq->role) != FCGI_RESPONDER) { + p_status = FCGI_UNKNOWN_ROLE; + goto end; + } + + /* fail on unsupported flags */ + if (breq->flags & FCGI_KEEP_CONN) { + p_status = FCGI_CANT_MPX_CONN; + goto end; + } + + /* store the id for later */ + *fcgi_id = (int) hdr.id; + + break; + + case FCGI_PARAMS: + /* unknown id? fail */ + if (hdr.id != *fcgi_id) { + p_status = FCGI_CANT_MPX_CONN; + goto end; + } + + if (psz) { + /* add to the buffer */ + buf = xs_realloc(buf, b_size + psz); + memcpy(buf + b_size, p_buf, psz); + b_size += psz; + } + else { + /* no size, so the packet is complete; process it */ + xs *cgi_vars = xs_dict_new(); + + req = xs_dict_new(); + + int offset = 0; + while (offset < b_size) { + unsigned int ksz = buf[offset++]; + + if (ksz & 0x80) { + ksz &= 0x7f; + ksz = (ksz << 8) | buf[offset++]; + ksz = (ksz << 8) | buf[offset++]; + ksz = (ksz << 8) | buf[offset++]; + } + + unsigned int vsz = buf[offset++]; + if (vsz & 0x80) { + vsz &= 0x7f; + vsz = (vsz << 8) | buf[offset++]; + vsz = (vsz << 8) | buf[offset++]; + vsz = (vsz << 8) | buf[offset++]; + } + + /* get the key */ + xs *k = xs_str_new_sz((char *)&buf[offset], ksz); + offset += ksz; + + /* get the value */ + xs *v = xs_str_new_sz((char *)&buf[offset], vsz); + offset += vsz; + + cgi_vars = xs_dict_append(cgi_vars, k, v); + + if (strcmp(k, "REQUEST_METHOD") == 0) + req = xs_dict_append(req, "method", v); + else + if (strcmp(k, "REQUEST_URI") == 0) { + xs *udp = xs_url_dec(v); + xs *pnv = xs_split_n(udp, "?", 1); + + /* store the path */ + req = xs_dict_append(req, "path", xs_list_get(pnv, 0)); + + /* get the variables */ + q_vars = xs_url_vars(xs_list_get(pnv, 1)); + } + else + if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|HTTP_*")) { + if (xs_startswith(k, "HTTP_")) + k = xs_crop_i(k, 5, 0); + + k = xs_tolower_i(k); + k = xs_replace_i(k, "_", "-"); + + req = xs_dict_append(req, k, v); + } + } + + req = xs_dict_append(req, "cgi_vars", cgi_vars); + + buf = xs_free(buf); + b_size = 0; + } + + break; + + case FCGI_STDIN: + /* unknown id? fail */ + if (hdr.id != *fcgi_id) { + p_status = FCGI_CANT_MPX_CONN; + goto end; + } + + if (psz) { + /* add to the buffer */ + buf = xs_realloc(buf, b_size + psz); + memcpy(buf + b_size, p_buf, psz); + b_size += psz; + } + else { + /* the packet is complete; fill the payload info and finish */ + *payload = (xs_str *)buf; + *p_size = b_size; + + const char *ct = xs_dict_get(req, "content-type"); + + if (*payload && ct && strcmp(ct, "application/x-www-form-urlencoded") == 0) { + xs *upl = xs_url_dec(*payload); + p_vars = xs_url_vars(upl); + } + else + if (*payload && ct && xs_startswith(ct, "multipart/form-data")) { + p_vars = xs_multipart_form_data(*payload, *p_size, ct); + } + else + p_vars = xs_dict_new(); + + if (q_vars == NULL) + q_vars = xs_dict_new(); + + req = xs_dict_append(req, "q_vars", q_vars); + req = xs_dict_append(req, "p_vars", p_vars); + + /* disconnect the payload from the buf variable */ + buf = NULL; + + goto end; + } + + break; + } + } + +end: + /* any kind of error? notify and cleanup */ + if (p_status != FCGI_REQUEST_COMPLETE) { + struct fcgi_end_request ereq = {0}; + + /* complete the connection */ + ereq.app_status = 0; + ereq.protocol_status = p_status; + + /* reuse header */ + hdr.type = FCGI_ABORT_REQUEST; + hdr.content_len = htons(sizeof(ereq)); + + fwrite(&hdr, sizeof(hdr), 1, f); + fwrite(&ereq, sizeof(ereq), 1, f); + + /* session closed */ + *fcgi_id = -1; + + /* request dict is not valid */ + req = xs_free(req); + } + + xs_free(buf); + return req; +} + + +void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int fcgi_id) +/* writes an FCGI response */ +{ + struct fcgi_record_header hdr = {0}; + struct fcgi_end_request ereq = {0}; + xs *out = xs_str_new(NULL); + xs_dict *p; + xs_str *k; + xs_str *v; + + /* no previous id? it's an error */ + if (fcgi_id == -1) + return; + + /* create the headers */ + { + xs *s1 = xs_fmt("status: %d\r\n", status); + out = xs_str_cat(out, s1); + } + + p = headers; + while (xs_dict_iter(&p, &k, &v)) { + xs *s1 = xs_fmt("%s: %s\r\n", k, v); + out = xs_str_cat(out, s1); + } + + if (b_size > 0) { + xs *s1 = xs_fmt("content-length: %d\r\n", b_size); + out = xs_str_cat(out, s1); + } + + out = xs_str_cat(out, "\r\n"); + + /* everything is text by now */ + int size = strlen(out); + + /* add the body */ + if (body != NULL && b_size > 0) + out = xs_append_m(out, body, b_size); + + /* now send all the STDOUT in packets */ + hdr.version = FCGI_VERSION_1; + hdr.type = FCGI_STDOUT; + hdr.id = fcgi_id; + + size += b_size; + int offset = 0; + + while (offset < size) { + int sz = size - offset; + if (sz > 0xffff) + sz = 0xffff; + + hdr.content_len = htons(sz); + + fwrite(&hdr, sizeof(hdr), 1, f); + fwrite(out + offset, 1, sz, f); + + offset += sz; + } + + /* final STDOUT packet with 0 size */ + hdr.content_len = 0; + fwrite(&hdr, sizeof(hdr), 1, f); + + /* complete the connection */ + ereq.app_status = 0; + ereq.protocol_status = FCGI_REQUEST_COMPLETE; + + hdr.type = FCGI_END_REQUEST; + hdr.content_len = htons(sizeof(ereq)); + + fwrite(&hdr, sizeof(hdr), 1, f); + fwrite(&ereq, sizeof(ereq), 1, f); +} + + +#endif /* XS_IMPLEMENTATION */ + +#endif /* XS_URL_H */ diff --git a/xs_version.h b/xs_version.h index a30fb2d..5fa20dd 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* a9cd3893c427bbcc478c5680245d435e415fd58a */ +/* b109eea00ddc0765929e36ed1ca6f3f697262bb2 */