1
0
forked from aniani/gmnisrv

Initial support for CGI scripts

This commit is contained in:
Drew DeVault 2020-10-25 23:16:50 -04:00
parent 8baeb5a51c
commit cc1bd152e3
4 changed files with 146 additions and 0 deletions

View File

@ -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 "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 list of files in the requested directory when an index file cannot be
found. Off by default. 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.

View File

@ -27,6 +27,7 @@ struct gmnisrv_route {
char *root; char *root;
char *index; char *index;
bool autoindex; bool autoindex;
bool cgi;
struct gmnisrv_route *next; struct gmnisrv_route *next;
}; };

View File

@ -210,6 +210,7 @@ conf_ini_handler(void *user, const char *section,
bool *value; bool *value;
} route_bvars[] = { } route_bvars[] = {
{ "autoindex", &route->autoindex }, { "autoindex", &route->autoindex },
{ "cgi", &route->cgi },
}; };
for (size_t i = 0; i < sizeof(route_strvars) / sizeof(route_strvars[0]); ++i) { for (size_t i = 0; i < sizeof(route_strvars) / sizeof(route_strvars[0]); ++i) {

View File

@ -1,3 +1,4 @@
#include <arpa/inet.h>
#include <assert.h> #include <assert.h>
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
@ -6,6 +7,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include "config.h" #include "config.h"
#include "gemini.h" #include "gemini.h"
@ -117,6 +119,82 @@ internal_error:
goto exit; 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 static bool
route_match(struct gmnisrv_route *route, const char *path, const char **revised) 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"); FILE *body = fopen(path, "r");
if (!body) { if (!body) {
if (errno == ENOENT) { if (errno == ENOENT) {