From 4677877693196823e8d806b0a0f520a35dd08533 Mon Sep 17 00:00:00 2001 From: Platon Ryzhikov Date: Sun, 17 Mar 2019 11:44:36 +0300 Subject: [PATCH] Add basic cgi support --- http.c | 67 ++++++++++++++++++++++++++++++++++++++++---------- http.h | 3 +++ main.c | 25 +++++++++++++++++-- quark.1 | 20 ++++++++++++++- resp.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ resp.h | 1 + util.h | 8 ++++++ 7 files changed, 184 insertions(+), 16 deletions(-) diff --git a/http.c b/http.c index efc4136..d3af686 100644 --- a/http.c +++ b/http.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -30,10 +31,12 @@ const char *req_field_str[] = { const char *req_method_str[] = { [M_GET] = "GET", [M_HEAD] = "HEAD", + [M_POST] = "POST", }; const char *status_str[] = { [S_OK] = "OK", + [S_NO_CONTENT] = "No content", [S_PARTIAL_CONTENT] = "Partial Content", [S_MOVED_PERMANENTLY] = "Moved Permanently", [S_NOT_MODIFIED] = "Not Modified", @@ -97,6 +100,7 @@ http_get_request(int fd, struct request *r) size_t hlen, i, mlen; ssize_t off; char h[HEADER_MAX], *p, *q; + size_t clen; /* empty all fields */ memset(r, 0, sizeof(*r)); @@ -111,23 +115,23 @@ http_get_request(int fd, struct request *r) break; } hlen += off; - if (hlen >= 4 && !memcmp(h + hlen - 4, "\r\n\r\n", 4)) { - break; + if (hlen >= 4 && strstr(h, "\r\n\r\n")) { + if (strstr(h, "Content-Length:")) { + /* Make sure that all data is read */ + sscanf(strstr(h, "Content-Length:"), "Content-Length: %lu", &clen); + if (strlen(strstr(h, "\r\n\r\n")) == 4 + clen) { + break; + } + } + else { + break; + } } if (hlen == sizeof(h)) { return http_send_status(fd, S_REQUEST_TOO_LARGE); } } - /* remove terminating empty line */ - if (hlen < 2) { - return http_send_status(fd, S_BAD_REQUEST); - } - hlen -= 2; - - /* null-terminate the header */ - h[hlen] = '\0'; - /* * parse request line */ @@ -137,6 +141,7 @@ http_get_request(int fd, struct request *r) mlen = strlen(req_method_str[i]); if (!strncmp(req_method_str[i], h, mlen)) { r->method = i; + setenv("REQUEST_METHOD", req_method_str[i], 1); break; } } @@ -161,7 +166,6 @@ http_get_request(int fd, struct request *r) return http_send_status(fd, S_REQUEST_TOO_LARGE); } memcpy(r->target, p, q - p + 1); - decode(r->target, r->target); /* basis for next step */ p = q + 1; @@ -200,7 +204,11 @@ http_get_request(int fd, struct request *r) if (i == NUM_REQ_FIELDS) { /* unmatched field, skip this line */ if (!(q = strstr(p, "\r\n"))) { - return http_send_status(fd, S_BAD_REQUEST); + if (r->method == M_POST) { + break; + } else { + return http_send_status(fd, S_BAD_REQUEST); + } } p = q + (sizeof("\r\n") - 1); continue; @@ -230,6 +238,9 @@ http_get_request(int fd, struct request *r) /* go to next line */ p = q + (sizeof("\r\n") - 1); } + + /* all other data will be later passed to script */ + sprintf(r->cgicont, "%s", p); /* * clean up host @@ -361,6 +372,36 @@ http_send_response(int fd, struct request *r) /* make a working copy of the target */ memcpy(realtarget, r->target, sizeof(realtarget)); + /* check if there is some query string */ + if (strrchr(realtarget, '?')) { + snprintf(tmptarget, sizeof(realtarget), "%s", strtok(realtarget, "?")); + setenv("QUERY_STRING", strtok(NULL, "?"), 1); + memcpy(realtarget, tmptarget, sizeof(tmptarget)); + } + decode(realtarget, tmptarget); + + /* match cgi */ + if (s.cgi) { + for (i = 0; i < s.cgi_len; i++) { + if (!regexec(&s.cgi[i].re, realtarget, 0, + NULL, 0)) { + snprintf(realtarget, sizeof(tmptarget) + sizeof(s.cgi[i].dir) - 1, "%s%s", s.cgi[i].dir, tmptarget); + if (stat(RELPATH(realtarget), &st) < 0) { + return http_send_status(fd, (errno == EACCES) ? + S_FORBIDDEN : S_NO_CONTENT); + } + setenv("SERVER_NAME", r->field[REQ_HOST], 1); + if (s.port) { + setenv("SERVER_PORT", s.port, 1); + } + setenv("SCRIPT_NAME", realtarget, 1); + return resp_cgi(fd, RELPATH(realtarget), r, &st); + } + } + } + + memcpy(realtarget, tmptarget, sizeof(tmptarget)); + /* match vhost */ vhostmatch = NULL; if (s.vhost) { diff --git a/http.h b/http.h index cd1ba22..b438759 100644 --- a/http.h +++ b/http.h @@ -19,6 +19,7 @@ extern const char *req_field_str[]; enum req_method { M_GET, M_HEAD, + M_POST, NUM_REQ_METHODS, }; @@ -28,10 +29,12 @@ struct request { enum req_method method; char target[PATH_MAX]; char field[NUM_REQ_FIELDS][FIELD_MAX]; + char cgicont[PATH_MAX]; }; enum status { S_OK = 200, + S_NO_CONTENT = 204, S_PARTIAL_CONTENT = 206, S_MOVED_PERMANENTLY = 301, S_NOT_MODIFIED = 304, diff --git a/main.c b/main.c index 9e7788f..471a3a7 100644 --- a/main.c +++ b/main.c @@ -165,7 +165,7 @@ static void usage(void) { const char *opts = "[-u user] [-g group] [-n num] [-d dir] [-l] " - "[-i file] [-v vhost] ... [-m map] ..."; + "[-i file] [-v vhost] ... [-m map] ... [-c cgi] ..."; die("usage: %s -h host -p port %s\n" " %s -U file [-p port] %s", argv0, @@ -195,11 +195,23 @@ main(int argc, char *argv[]) s.host = s.port = NULL; s.vhost = NULL; s.map = NULL; - s.vhost_len = s.map_len = 0; + s.cgi = NULL; + s.vhost_len = s.map_len = s.cgi_len = 0; s.docindex = "index.html"; s.listdirs = 0; ARGBEGIN { + case 'c': + if (spacetok(EARGF(usage()), tok, 2) || !tok[0] || !tok[1]) { + usage(); + } + if (!(s.cgi = reallocarray(s.cgi, ++s.cgi_len, + sizeof(struct cgi)))) { + die("reallocarray:"); + } + s.cgi[s.cgi_len - 1].regex = tok[0]; + s.cgi[s.cgi_len - 1].dir = tok[1]; + break; case 'd': servedir = EARGF(usage()); break; @@ -286,6 +298,15 @@ main(int argc, char *argv[]) } } + /* compile and check the supplied cgi regexes */ + for (i = 0; i < s.cgi_len; i++) { + if (regcomp(&s.cgi[i].re, s.cgi[i].regex, + REG_EXTENDED | REG_ICASE | REG_NOSUB)) { + die("regcomp '%s': invalid regex", + s.cgi[i].regex); + } + } + /* raise the process limit */ rlim.rlim_cur = rlim.rlim_max = maxnprocs; if (setrlimit(RLIMIT_NPROC, &rlim) < 0) { diff --git a/quark.1 b/quark.1 index ce315b5..cbbcff3 100644 --- a/quark.1 +++ b/quark.1 @@ -16,6 +16,7 @@ .Op Fl i Ar file .Oo Fl v Ar vhost Oc ... .Oo Fl m Ar map Oc ... +.Oo Fl c Ar cgi Oc ... .Nm .Fl U Ar file .Op Fl p Ar port @@ -27,11 +28,28 @@ .Op Fl i Ar file .Oo Fl v Ar vhost Oc ... .Oo Fl m Ar map Oc ... +.Oo Fl c Ar cgi Oc ... .Sh DESCRIPTION .Nm -is a simple HTTP GET/HEAD-only web server for static content. +is a simple HTTP web server. .Sh OPTIONS .Bl -tag -width Ds +.It Fl c Ar cgi +Add the target prefix mapping rule for dynamic content specified by +.Ar cgi , +which has the form +.Qq Pa regex dir , +where each element is separated with spaces (0x20) that can be +escaped with '\\'. +.Pp +A request matching cgi regular expression +.Pa regex +(see +.Xr regex 3 ) +executes script located in +.Pa dir +passing data to it via QUERY_STRING environment variable +or via stdout and then sends its stdout. .It Fl d Ar dir Serve .Ar dir diff --git a/resp.c b/resp.c index 3075c28..dccdc3f 100644 --- a/resp.c +++ b/resp.c @@ -38,6 +38,82 @@ suffix(int t) return ""; } +enum status +resp_cgi(int fd, char *name, struct request *r, struct stat *st) +{ + enum status sta; + int tocgi[2], fromcgi[2]; + pid_t script; + ssize_t bread, bwritten; + static char buf[BUFSIZ], t[TIMESTAMP_LEN]; + + /* check if script is executable */ + if (!(st->st_mode & S_IXOTH)) { + return http_send_status(fd, S_FORBIDDEN); + } + + /* open two pipes in case for POST method; this doesn't break operation if GET method is used */ + if (pipe(fromcgi) < 0) { + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); + } + + if (pipe(tocgi) < 0) { + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); + } + + /* start script */ + if (!(script = fork())) { + close(0); + close(1); + close(fromcgi[0]); + close(tocgi[1]); + dup2(fromcgi[1], 1); + dup2(tocgi[0], 0); + execlp(name, name, (char*) NULL); + } + + if (script < 0) { + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); + } + close(fromcgi[1]); + close(tocgi[0]); + + /* POST method should obtain its data */ + if (dprintf(tocgi[1], "%s\n", r->cgicont) < 0) { + return http_send_status(fd, S_INTERNAL_SERVER_ERROR); + } + close(tocgi[1]); + + /* send header as late as possible */ + if (dprintf(fd, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n", + S_OK, status_str[S_OK], timestamp(time(NULL), t)) < 0) { + sta = S_REQUEST_TIMEOUT; + goto cleanup; + } + + while ((bread = read(fromcgi[0], buf, BUFSIZ)) > 0) { + if (bread < 0) { + return S_INTERNAL_SERVER_ERROR; + } + + bwritten = write(fd, buf, bread); + + if (bwritten < 0) { + return S_REQUEST_TIMEOUT; + } + } + sta = S_OK; +cleanup: + if (fromcgi[0]) { + close(fromcgi[0]); + } + + return sta; +} + enum status resp_dir(int fd, char *name, struct request *r) { diff --git a/resp.h b/resp.h index d5928ef..2705364 100644 --- a/resp.h +++ b/resp.h @@ -7,6 +7,7 @@ #include "http.h" +enum status resp_cgi(int, char *, struct request *, struct stat *); enum status resp_dir(int, char *, struct request *); enum status resp_file(int, char *, struct request *, struct stat *, char *, off_t, off_t); diff --git a/util.h b/util.h index 12b7bd8..ef1a8b3 100644 --- a/util.h +++ b/util.h @@ -23,6 +23,12 @@ struct map { char *to; }; +struct cgi { + char *regex; + char *dir; + regex_t re; +}; + extern struct server { char *host; char *port; @@ -32,6 +38,8 @@ extern struct server { size_t vhost_len; struct map *map; size_t map_len; + struct cgi *cgi; + size_t cgi_len; } s; #undef MIN -- 2.21.0