1
0
mirror of https://git.sr.ht/~sircmpwn/gmnisrv synced 2024-11-03 06:07:17 -05:00
gmnisrv/src/serve.c

385 lines
9.3 KiB
C
Raw Normal View History

2020-10-25 23:16:50 -04:00
#include <arpa/inet.h>
#include <assert.h>
2020-09-26 15:51:28 -04:00
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
2020-09-26 15:51:28 -04:00
#include <sys/stat.h>
2020-10-25 23:16:50 -04:00
#include <sys/types.h>
2020-09-26 15:51:28 -04:00
#include <unistd.h>
#include "config.h"
#include "gemini.h"
#include "log.h"
#include "mime.h"
#include "server.h"
#include "url.h"
void
client_submit_response(struct gmnisrv_client *client,
2020-09-26 15:51:28 -04:00
enum gemini_status status, const char *meta, FILE *body)
{
client->state = CLIENT_STATE_HEADER;
client->status = status;
client->meta = strdup(meta);
2020-09-26 15:51:28 -04:00
client->body = body;
client->pollfd->events = POLLOUT;
}
void
client_oom(struct gmnisrv_client *client)
{
const char *error = "Out of memory";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_TEMPORARY_FAILURE, error, NULL);
}
static int
namecmp(const void *p1, const void *p2)
{
return strcmp(*(char *const *)p1, *(char *const *)p2);
}
void
serve_autoindex(struct gmnisrv_client *client, const char *path)
{
size_t bufsz = 0;
size_t nameln = 0, namesz = 1024;
char **names = calloc(namesz, sizeof(char *));
DIR *dirp = opendir(path);
if (!dirp) {
goto internal_error;
}
bufsz += snprintf(NULL, 0, "# Index of %s\n\n", client->path);
struct dirent *ent;
errno = 0;
while ((ent = readdir(dirp)) != NULL) {
char fpath[PATH_MAX + 1];
snprintf(fpath, sizeof(fpath), "%s/%s", path, ent->d_name);
2020-09-26 15:51:28 -04:00
struct stat st;
if (stat(fpath, &st) != 0) {
goto internal_error;
}
if ((S_ISREG(st.st_mode) || S_ISLNK(st.st_mode) || S_ISDIR(st.st_mode))
&& ent->d_name[0] != '.') {
if (nameln >= namesz) {
char **new = realloc(names, namesz * 2);
if (!new) {
goto internal_error;
}
namesz *= 2;
names = new;
}
names[nameln++] = strdup(ent->d_name);
bufsz += snprintf(NULL, 0, "=> %s\n", ent->d_name);
}
errno = 0;
}
if (errno != 0) {
goto internal_error;
}
qsort(names, nameln, sizeof(names[0]), namecmp);
FILE *buf = fmemopen(NULL, bufsz, "w+");
if (!buf) {
goto internal_error;
}
int r;
r = fprintf(buf, "# Index of %s\n\n", client->path);
assert(r > 0);
for (size_t i = 0; i < nameln; ++i) {
r = fprintf(buf, "=> %s\n", names[i]);
assert(r > 0);
}
r = fseek(buf, 0, SEEK_SET);
assert(r == 0);
client_submit_response(client, GEMINI_STATUS_SUCCESS,
"text/gemini", buf);
exit:
closedir(dirp);
for (size_t i = 0; i < nameln; ++i) {
free(names[i]);
}
free(names);
return;
internal_error:
server_error("Error reading %s: %s", path, strerror(errno));
client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE,
"Internal server error", NULL);
goto exit;
}
2020-10-25 23:16:50 -04:00
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)
{
switch (route->routing) {
case ROUTE_PATH:;
size_t l = strlen(route->path);
if (strncmp(path, route->path, l) != 0) {
return false;
}
if (route->path[l-1] != '/' && path[l] != '\0' && path[l] != '/') {
// Prevents path == "/foobar" from matching
// route == "/foo":
return false;
}
if (route->path[l-1] == '/') {
*revised = &path[l-1];
} else {
*revised = &path[l];
}
return true;
case ROUTE_REGEX:
assert(0); // TODO
}
assert(0); // Invariant
}
void
serve_request(struct gmnisrv_client *client)
{
struct gmnisrv_host *host = client->host;
assert(host);
struct gmnisrv_route *route = host->routes;
assert(route);
const char *url_path;
while (route) {
if (route_match(route, client->path, &url_path)) {
break;
}
route = route->next;
}
if (!route) {
client_submit_response(client,
GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
return;
}
assert(route->root); // TODO: reverse proxy support
char path[PATH_MAX + 1];
int n = snprintf(path, sizeof(path), "%s%s", route->root, url_path);
if ((size_t)n >= sizeof(path)) {
client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE,
2020-09-26 15:51:28 -04:00
"Request path exceeds PATH_MAX", NULL);
return;
}
2020-09-26 15:51:28 -04:00
int nlinks = 0;
struct stat st;
while (true) {
if ((n = stat(path, &st)) != 0) {
client_submit_response(client,
GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
return;
}
if (S_ISDIR(st.st_mode)) {
if (route->autoindex) {
2020-09-26 15:51:28 -04:00
serve_autoindex(client, path);
return;
} else {
strncat(path,
route->index ? route->index : "index.gmi",
2020-09-26 15:51:28 -04:00
sizeof(path) - 1);
}
} else if (S_ISLNK(st.st_mode)) {
++nlinks;
if (nlinks > 3) {
server_error("Maximum redirects exceeded for %s",
client->path);
client_submit_response(client,
GEMINI_STATUS_NOT_FOUND,
"Not found", NULL);
return;
}
char path2[PATH_MAX + 1];
ssize_t s = readlink(path, path2, sizeof(path2));
assert(s != -1);
strcpy(path, path2);
} else if (S_ISREG(st.st_mode)) {
break;
} else {
// Don't serve special files
client_submit_response(client,
GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
return;
}
}
2020-10-25 23:16:50 -04:00
if (route->cgi) {
serve_cgi(client, path);
return;
}
2020-09-26 15:51:28 -04:00
FILE *body = fopen(path, "r");
if (!body) {
if (errno == ENOENT) {
2020-09-26 15:51:28 -04:00
client_submit_response(client,
GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
return;
} else {
client_error(&client->addr, "error opening %s: %s",
path, strerror(errno));
client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE,
2020-09-26 15:51:28 -04:00
"Internal server error", NULL);
return;
}
}
const char *meta = gmnisrv_mimetype_for_path(path);
2020-09-26 15:51:28 -04:00
client_submit_response(client, GEMINI_STATUS_SUCCESS, meta, body);
}
bool
request_validate(struct gmnisrv_client *client, char **path)
{
struct Curl_URL *url = curl_url();
if (!url) {
client_oom(client);
return false;
}
if (curl_url_set(url, CURLUPART_URL, client->buf, 0) != CURLUE_OK) {
const char *error = "Protocol error: invalid URL";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_BAD_REQUEST, error, NULL);
goto exit;
}
char *part;
if (curl_url_get(url, CURLUPART_SCHEME, &part, 0) != CURLUE_OK) {
const char *error = "Protocol error: invalid URL (expected scheme)";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_BAD_REQUEST, error, NULL);
goto exit;
} else if (strcmp(part, "gemini") != 0) {
free(part);
const char *error = "Refusing proxy to non-gemini URL";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_PROXY_REQUEST_REFUSED, error, NULL);
goto exit;
}
free(part);
if (curl_url_get(url, CURLUPART_HOST, &part, 0) != CURLUE_OK) {
const char *error = "Protocol error: invalid URL (expected host)";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_BAD_REQUEST, error, NULL);
goto exit;
} else if (strcmp(part, client->host->hostname) != 0) {
free(part);
const char *error = "Protocol error: hostname does not match SNI";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_BAD_REQUEST, error, NULL);
goto exit;
}
free(part);
if (curl_url_get(url, CURLUPART_PATH, &part, 0) != CURLUE_OK) {
const char *error = "Protocol error: invalid URL (expected path)";
client_submit_response(client,
2020-09-26 15:51:28 -04:00
GEMINI_STATUS_BAD_REQUEST, error, NULL);
goto exit;
}
// NOTE: curl_url_set(..., CURLUPART_URL, ..., 0) will consoldate .. and
// . to prevent directory traversal without additional code.
*path = part;
curl_url_cleanup(url);
return true;
exit:
curl_url_cleanup(url);
return false;
}