2020-09-26 15:08:25 -04:00
|
|
|
#include <assert.h>
|
2020-09-26 15:51:28 -04:00
|
|
|
#include <dirent.h>
|
2020-09-26 15:08:25 -04:00
|
|
|
#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>
|
|
|
|
#include <unistd.h>
|
2020-09-26 15:08:25 -04:00
|
|
|
#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)
|
2020-09-26 15:08:25 -04:00
|
|
|
{
|
2020-10-25 14:50:07 -04:00
|
|
|
client->state = CLIENT_STATE_HEADER;
|
2020-09-26 15:08:25 -04:00
|
|
|
client->status = status;
|
|
|
|
client->meta = strdup(meta);
|
2020-09-26 15:51:28 -04:00
|
|
|
client->body = body;
|
2020-09-26 15:08:25 -04:00
|
|
|
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];
|
2020-10-13 03:05:10 -04:00
|
|
|
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-09-26 15:08:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
serve_request(struct gmnisrv_client *client)
|
|
|
|
{
|
|
|
|
struct gmnisrv_host *host = client->host;
|
|
|
|
assert(host);
|
|
|
|
assert(host->root); // TODO: reverse proxy support
|
|
|
|
|
|
|
|
char path[PATH_MAX + 1];
|
|
|
|
int n = snprintf(path, sizeof(path), "%s%s", host->root, client->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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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 (host->autoindex) {
|
|
|
|
serve_autoindex(client, path);
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
strncat(path,
|
|
|
|
host->index ? host->index : "index.gmi",
|
|
|
|
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-09-26 15:08:25 -04:00
|
|
|
}
|
|
|
|
|
2020-09-26 15:51:28 -04:00
|
|
|
FILE *body = fopen(path, "r");
|
|
|
|
if (!body) {
|
2020-09-26 15:08:25 -04:00
|
|
|
if (errno == ENOENT) {
|
2020-09-26 15:51:28 -04:00
|
|
|
client_submit_response(client,
|
|
|
|
GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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);
|
2020-09-26 15:08:25 -04:00
|
|
|
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;
|
2020-10-12 00:35:02 -04:00
|
|
|
|
|
|
|
exit:
|
|
|
|
curl_url_cleanup(url);
|
|
|
|
return false;
|
2020-09-26 15:08:25 -04:00
|
|
|
}
|