diff --git a/doc/gmnisrvini.scd b/doc/gmnisrvini.scd index 73049a9..3fbfba1 100644 --- a/doc/gmnisrvini.scd +++ b/doc/gmnisrvini.scd @@ -92,3 +92,64 @@ Within each routing section, the following keys are used to configure how "on" to enable the auto-index feature, which presents clients with a list of files in the requested directory when an index file cannot be found. Off by default. + +*cgi* + "on" to enable CGI support. *root* must also be configured. See "CGI + Support" for details. + +# CGI Support + +*gmnisrv* supports a limited version of CGI, compatible with the Jetforce +server. It is not a faithful implementation of RFC 3875, but is sufficient for +most of the needs of Gemini servers. + +Set *cgi=on* for a route configuration to enable CGI for that route and set +*root* to the path where the CGI scripts are found. If a client requests a +script, it will be executed, and must print a Gemini response (including status +code and meta) to stdout. + +The following environment variables will be set: + +[[ *Variable* +:[ *Example* +:< *Description* +| *GATEWAY_INTERFACE* +: GCI/1.1 +: CGI version +| *SERVER_PROTOCOL* +: GEMINI +: The server protocol +| *SERVER_SOFTWARE* +: gmnisrv/0.0.0 +: The gmnisrv server name and version +| *GEMINI_URL* +: See [1] +: The URL requested by the client +| *SCRIPT_NAME* +: /cgi-bin/foo.sh +: The portion of the URL referring to the script name. +| *PATH_INFO* +: /bar +: The remainder of the path following *SCRIPT_NAME*. +| *QUERY_STRING* +: hello=world +: The query string portion of the URL. +| *SERVER_NAME*, *HOSTNAME* +: example.org +: The server host name. +| *SERVER_PORT* +: 1965 +: The server port number. +| *REMOTE_HOST*, *REMOTE_ADDR* +: 10.10.0.2 +: The clients IP address. +| *TLS_CIPHER* +: TLS_AES_256_GCM_SHA384 +: The negotiated TLS cipher. +| *TLS_VERSION* +: TLSv1.3 +: The negotiated TLS version. + +\[1]: gemini://example.org/cgi-bin/foo.sh/bar?hello=world + +The exit status of the script is ignored. diff --git a/include/config.h b/include/config.h index a489f2d..3a7d7b4 100644 --- a/include/config.h +++ b/include/config.h @@ -27,6 +27,7 @@ struct gmnisrv_route { char *root; char *index; bool autoindex; + bool cgi; struct gmnisrv_route *next; }; diff --git a/src/config.c b/src/config.c index 693766e..dc2a07e 100644 --- a/src/config.c +++ b/src/config.c @@ -210,6 +210,7 @@ conf_ini_handler(void *user, const char *section, bool *value; } route_bvars[] = { { "autoindex", &route->autoindex }, + { "cgi", &route->cgi }, }; for (size_t i = 0; i < sizeof(route_strvars) / sizeof(route_strvars[0]); ++i) { diff --git a/src/serve.c b/src/serve.c index 18c2993..de38c9f 100644 --- a/src/serve.c +++ b/src/serve.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -6,6 +7,7 @@ #include #include #include +#include #include #include "config.h" #include "gemini.h" @@ -117,6 +119,82 @@ internal_error: goto exit; } +static void +serve_cgi(struct gmnisrv_client *client, const char *path) +{ + int pfd[2]; + if (pipe(pfd) == -1) { + server_error("pipe: %s", strerror(errno)); + client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE, + "Internal server error", NULL); + return; + } + + pid_t pid = fork(); + if (pid == -1) { + server_error("fork: %s", strerror(errno)); + client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE, + "Internal server error", NULL); + close(pfd[0]); + close(pfd[1]); + return; + } else if (pid == 0) { + close(pfd[0]); + dup2(pfd[1], STDOUT_FILENO); + close(pfd[1]); + + // I don't feel like freeing this stuff and this process is + // going to die soon anyway so let's just be hip and call it an + // arena allocator :^) + struct Curl_URL *url = curl_url(); + assert(url); + CURLUcode uc = curl_url_set(url, CURLUPART_URL, client->buf, 0); + assert(uc == CURLUE_OK); + + char *query; + uc = curl_url_get(url, CURLUPART_QUERY, &query, CURLU_URLDECODE); + if (uc != CURLUE_OK) { + assert(uc == CURLUE_NO_QUERY); + } else { + setenv("QUERY_STRING", query, 1); + } + + char abuf[INET6_ADDRSTRLEN + 1]; + const char *addrs = inet_ntop(client->addr.sa_family, + client->addr.sa_data, abuf, sizeof(abuf)); + assert(addrs); + + // Compatible with Jetforce + setenv("GATEWAY_INTERFACE", "GCI/1.1", 1); + setenv("SERVER_PROTOCOL", "GEMINI", 1); + setenv("SERVER_SOFTWARE", "gmnisrv/0.0.0", 1); + setenv("GEMINI_URL", client->buf, 1); + setenv("SCRIPT_NAME", path, 1); + //setenv("PATH_INFO", "", 1); // TODO + setenv("SERVER_NAME", client->host->hostname, 1); + setenv("HOSTNAME", client->host->hostname, 1); + //setenv("SERVER_PORT", "", 1); // TODO + setenv("REMOTE_HOST", addrs, 1); + setenv("REMOTE_ADDR", addrs, 1); + + const SSL_CIPHER *cipher = SSL_get_current_cipher(client->ssl); + setenv("TLS_CIPHER", SSL_CIPHER_get_name(cipher), 1); + setenv("TLS_VERSION", SSL_CIPHER_get_version(cipher), 1); + + // TODO: Client certificate details + + execlp(path, path, NULL); + server_error("execlp: %s", strerror(errno)); + _exit(1); + } else { + close(pfd[1]); + FILE *f = fdopen(pfd[0], "r"); + client_submit_response(client, GEMINI_STATUS_SUCCESS, "(cgi)", f); + client->state = CLIENT_STATE_BODY; // The CGI script sends meta + client->bufix = client->bufln = 0; + } +} + static bool route_match(struct gmnisrv_route *route, const char *path, const char **revised) { @@ -219,6 +297,11 @@ serve_request(struct gmnisrv_client *client) } } + if (route->cgi) { + serve_cgi(client, path); + return; + } + FILE *body = fopen(path, "r"); if (!body) { if (errno == ENOENT) {