From 6bc9c4deb90e8daa228d792b23a3e61b7bebdb78 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sat, 26 Sep 2020 15:51:28 -0400 Subject: [PATCH] Implement autoindex option --- include/config.h | 5 ++ include/server.h | 4 +- src/config.c | 20 ++++++ src/serve.c | 163 +++++++++++++++++++++++++++++++++++++++++------ src/server.c | 11 ++-- 5 files changed, 176 insertions(+), 27 deletions(-) diff --git a/include/config.h b/include/config.h index 83253f7..1a0b17c 100644 --- a/include/config.h +++ b/include/config.h @@ -2,6 +2,7 @@ #define GMNISRV_CONFIG #include #include +#include struct gmnisrv_tls { char *store; @@ -13,8 +14,12 @@ struct gmnisrv_tls { struct gmnisrv_host { char *hostname; char *root; + char *index; + bool autoindex; + X509 *x509; EVP_PKEY *pkey; + struct gmnisrv_host *next; }; diff --git a/include/server.h b/include/server.h index 884f210..c6f4a38 100644 --- a/include/server.h +++ b/include/server.h @@ -33,7 +33,7 @@ struct gmnisrv_client { enum response_state state; enum gemini_status status; char *meta; - int bodyfd; + FILE *body; size_t bbytes; struct gmnisrv_host *host; @@ -70,7 +70,7 @@ void disconnect_client(struct gmnisrv_server *server, void serve_request(struct gmnisrv_client *client); bool request_validate(struct gmnisrv_client *client, char **path); void client_submit_response(struct gmnisrv_client *client, - enum gemini_status status, const char *meta, int bodyfd); + enum gemini_status status, const char *meta, FILE *body); void client_oom(struct gmnisrv_client *client); #endif diff --git a/src/config.c b/src/config.c index f3172d2..f146aa0 100644 --- a/src/config.c +++ b/src/config.c @@ -1,8 +1,10 @@ #include #include +#include #include #include #include +#include #include "config.h" #include "ini.h" @@ -146,6 +148,13 @@ conf_ini_handler(void *user, const char *section, char **value; } host_strvars[] = { { "root", &host->root }, + { "index", &host->index }, + }; + struct { + char *name; + bool *value; + } host_bvars[] = { + { "autoindex", &host->autoindex }, }; for (size_t i = 0; i < sizeof(host_strvars) / sizeof(host_strvars[0]); ++i) { @@ -156,6 +165,17 @@ conf_ini_handler(void *user, const char *section, return 1; } + for (size_t i = 0; i < sizeof(host_bvars) / sizeof(host_bvars[0]); ++i) { + if (strcmp(host_bvars[i].name, name) != 0) { + continue; + } + *host_bvars[i].value = + strcasecmp(value, "yes") == 0 || + strcasecmp(value, "true") == 0 || + strcasecmp(value, "on") == 0; + return 1; + } + fprintf(stderr, "Unknown config option [%s]%s\n", section, name); return 0; } diff --git a/src/serve.c b/src/serve.c index 00d51b5..a856bc2 100644 --- a/src/serve.c +++ b/src/serve.c @@ -1,9 +1,12 @@ #include +#include #include #include #include #include #include +#include +#include #include "config.h" #include "gemini.h" #include "log.h" @@ -13,11 +16,11 @@ void client_submit_response(struct gmnisrv_client *client, - enum gemini_status status, const char *meta, int bodyfd) + enum gemini_status status, const char *meta, FILE *body) { client->status = status; client->meta = strdup(meta); - client->bodyfd = bodyfd; + client->body = body; client->pollfd->events = POLLOUT; } @@ -26,7 +29,92 @@ client_oom(struct gmnisrv_client *client) { const char *error = "Out of memory"; client_submit_response(client, - GEMINI_STATUS_TEMPORARY_FAILURE, error, -1); + 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]; + strcpy(fpath, path); + strncat(fpath, ent->d_name, sizeof(fpath)); + + 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; } void @@ -40,32 +128,69 @@ serve_request(struct gmnisrv_client *client) 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, - "Request path exceeds PATH_MAX", -1); + "Request path exceeds PATH_MAX", NULL); return; } - if (path[strlen(path) - 1] == '/') { - // TODO: Let user configure index file name? - strncat(path, "index.gmi", sizeof(path) - 1); + 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; + } } - int fd = open(path, O_RDONLY); - if (fd == -1) { + FILE *body = fopen(path, "r"); + if (!body) { if (errno == ENOENT) { - client_submit_response(client, GEMINI_STATUS_NOT_FOUND, - "Not found", -1); + 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, - "Internal server error", -1); + "Internal server error", NULL); return; } } const char *meta = gmnisrv_mimetype_for_path(path); - client_submit_response(client, GEMINI_STATUS_SUCCESS, meta, fd); + client_submit_response(client, GEMINI_STATUS_SUCCESS, meta, body); } bool @@ -79,7 +204,7 @@ request_validate(struct gmnisrv_client *client, char **path) if (curl_url_set(url, CURLUPART_URL, client->buf, 0) != CURLUE_OK) { const char *error = "Protocol error: invalid URL"; client_submit_response(client, - GEMINI_STATUS_BAD_REQUEST, error, -1); + GEMINI_STATUS_BAD_REQUEST, error, NULL); goto exit; } @@ -87,13 +212,13 @@ request_validate(struct gmnisrv_client *client, char **path) if (curl_url_get(url, CURLUPART_SCHEME, &part, 0) != CURLUE_OK) { const char *error = "Protocol error: invalid URL (expected scheme)"; client_submit_response(client, - GEMINI_STATUS_BAD_REQUEST, error, -1); + 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, - GEMINI_STATUS_PROXY_REQUEST_REFUSED, error, -1); + GEMINI_STATUS_PROXY_REQUEST_REFUSED, error, NULL); goto exit; } free(part); @@ -101,13 +226,13 @@ request_validate(struct gmnisrv_client *client, char **path) if (curl_url_get(url, CURLUPART_HOST, &part, 0) != CURLUE_OK) { const char *error = "Protocol error: invalid URL (expected host)"; client_submit_response(client, - GEMINI_STATUS_BAD_REQUEST, error, -1); + 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, - GEMINI_STATUS_BAD_REQUEST, error, -1); + GEMINI_STATUS_BAD_REQUEST, error, NULL); goto exit; } free(part); @@ -115,7 +240,7 @@ request_validate(struct gmnisrv_client *client, char **path) if (curl_url_get(url, CURLUPART_PATH, &part, 0) != CURLUE_OK) { const char *error = "Protocol error: invalid URL (expected path)"; client_submit_response(client, - GEMINI_STATUS_BAD_REQUEST, error, -1); + GEMINI_STATUS_BAD_REQUEST, error, NULL); goto exit; } // NOTE: curl_url_set(..., CURLUPART_URL, ..., 0) will consoldate .. and diff --git a/src/server.c b/src/server.c index e69e381..c818fbd 100644 --- a/src/server.c +++ b/src/server.c @@ -150,7 +150,6 @@ accept_client(struct gmnisrv_server *server, int fd) client->pollfd = pollfd; client->addrlen = addrlen; client->server = server; - client->bodyfd = -1; clock_gettime(CLOCK_MONOTONIC, &client->ctime); memcpy(&client->addr, &addr, sizeof(addr)); @@ -240,7 +239,7 @@ client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client) if (!client->host) { const char *error = "This server requires clients to support the TLS SNI (server name identification) extension"; client_submit_response(client, - GEMINI_STATUS_BAD_REQUEST, error, -1); + GEMINI_STATUS_BAD_REQUEST, error, NULL); return; } @@ -261,7 +260,7 @@ client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client) if (!newline) { const char *error = "Protocol error: malformed request"; client_submit_response(client, - GEMINI_STATUS_BAD_REQUEST, error, -1); + GEMINI_STATUS_BAD_REQUEST, error, NULL); return; } *newline = 0; @@ -304,7 +303,7 @@ client_writable(struct gmnisrv_server *server, struct gmnisrv_client *client) } client->bufix += r; if (client->bufix >= client->bufln) { - if (client->bodyfd == -1) { + if (!client->body) { disconnect_client(server, client); } else { client->state = RESPOND_BODY; @@ -315,8 +314,8 @@ client_writable(struct gmnisrv_server *server, struct gmnisrv_client *client) break; case RESPOND_BODY: if (client->bufix >= client->bufln) { - n = read(client->bodyfd, - client->buf, sizeof(client->buf)); + n = fread(client->buf, 1, + sizeof(client->buf), client->body); if (n == -1) { client_error(&client->addr, "Error reading response body: %s",