commit - c8b74339185123feebb6164b91f500f1930e45ff
commit + 252908e6bb335c42249a3d5fe6ecaa4daf5a3e3e
blob - b7f05157b697387c34a2aa1d1ea441f66636fa41
blob + 1626a63f07560366fb17f7f63161ce309d4aeaf2
--- ChangeLog
+++ ChangeLog
2021-01-24 Omar Polo <op@omarpolo.com>
+ * server.c (open_dir): add directory listing (disabled by default)
+
* parse.y (vhost): added support for location blocks
* server.c (send_dir): make the directory index customizable
blob - 70ece216eb65b5deaaf34bce45113e2314463af6
blob + ad0aeba97f7232a1d19b9386ae486e802cb90884
--- gmid.1
+++ gmid.1
Set the directory index file.
If not specified, it defaults to
.Pa index.gmi
+.It Ic auto Ic index Ar bool
+If no index file is found, automatically generate a directory listing.
+It's disabled by default.
.It Ic location Pa path Brq ...
Specify server configuration rules for a specific location.
The
blob - 5bccefcb33564b967270e493acfb8d9daa978382
blob + 3e1ed338c0fcbfc4f686ebd27bc3d48414985053
--- gmid.c
+++ gmid.c
{
size_t i;
+ if (prefix == NULL)
+ return 0;
+
for (i = 0; prefix[i] != '\0'; ++i)
if (str[i] != prefix[i])
return 0;
blob - 139338e0f1048ebcbffaa6b2200d1cf1cc2f7647
blob + 2364e7c7bc0cbda5e0eeb965b11ca858c6c21db1
--- gmid.h
+++ gmid.h
#ifndef GMID_H
#define GMID_H
+#include <sys/socket.h>
+#include <sys/types.h>
+
#include <arpa/inet.h>
#include <netinet/in.h>
-#include <sys/socket.h>
+#include <dirent.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
char *lang;
char *default_mime;
char *index;
+ int auto_index; /* 0 auto, -1 off, 1 on */
};
struct vhost {
S_OPEN,
S_INITIALIZING,
S_SENDING_FILE,
+ S_SENDING_DIR,
S_SENDING_CGI,
S_CLOSING,
};
char sbuf[1024]; /* static buffer */
void *buf, *i; /* mmap buffer */
ssize_t len, off; /* mmap/static buffer */
+ DIR *dir;
struct sockaddr_storage addr;
struct vhost *host; /* host she's talking to */
};
const char *vhost_lang(struct vhost*, const char*);
const char *vhost_default_mime(struct vhost*, const char*);
const char *vhost_index(struct vhost*, const char*);
+int vhost_auto_index(struct vhost*, const char*);
int check_path(struct client*, const char*, int*);
void open_file(struct pollfd*, struct client*);
+void load_file(struct pollfd*, struct client*);
void check_for_cgi(char *, char*, struct pollfd*, struct client*);
void mark_nonblock(int);
void handle_handshake(struct pollfd*, struct client*);
void start_reply(struct pollfd*, struct client*, int, const char*);
void start_cgi(const char*, const char*, const char*, struct pollfd*, struct client*);
void send_file(struct pollfd*, struct client*);
-void send_dir(struct pollfd*, struct client*);
+void open_dir(struct pollfd*, struct client*);
+void redirect_canonical_dir(struct pollfd*, struct client*);
+int read_next_dir_entry(struct client*);
+void send_directory_listing(struct pollfd*, struct client*);
void cgi_poll_on_child(struct pollfd*, struct client*);
void cgi_poll_on_client(struct pollfd*, struct client*);
void handle_cgi(struct pollfd*, struct client*);
blob - b785796f7bf9fb2390aa06179bed58b0cd242d52
blob + 240f7c466e7c1551d21d5664d4aaab6974aba9d3
--- lex.l
+++ lex.l
cgi return TCGI;
lang return TLANG;
index return TINDEX;
+auto return TAUTO;
[{}] return *yytext;
blob - e7883a9d1705124c010c75d6c852d3090d06c8a2
blob + 5e7cb216241e588d84ac594ed7b7a2ef4a4671cc
--- parse.y
+++ parse.y
}
%token TDAEMON TIPV6 TPORT TPROTOCOLS TMIME TDEFAULT TTYPE TSERVER
-%token TLOCATION TCERT TKEY TROOT TCGI TLANG
+%token TLOCATION TCERT TKEY TROOT TCGI TLANG TINDEX TAUTO
%token TERR
%token <str> TSTRING
free(loc->index);
loc->index = $2;
}
+ | TAUTO TINDEX TBOOL { loc->auto_index = $3 ? 1 : -1; }
;
blob - 8cf2afb1ee11f9c4d717dd411e42e23e37d4c7fb
blob + ca87701f35435c53dcc1f64a02f7f06b21ff1509
--- regress/runtime
+++ regress/runtime
eq "$(head /dir/hello)" "20 text/plain" "Unexpected head for /"
echo OK GET /dir/hello with location and default type
-eq "$(head /dir/)" "20 text/plain" "Unexpected head for /dir"
+eq "$(head /dir/)" "20 text/plain" "Unexpected head for /dir/"
eq "$(get /dir/|tail -1)" 'echo "# hello world"' "Unexpected body for /dir/"
echo OK GET /dir/ with location and custom index
check "should be running"
quit
+
+config '' 'location "/dir/" { auto index on }'
+checkconf
+run
+
+eq "$(head /)" "20 text/gemini" "Unexpected head for /"
+eq "$(get /)" "# hello world$ln" "Unexpected body for /"
+echo OK GET / with auto index
+
+eq "$(head /dir)" "30 /dir/" "Unexpected head for /dir"
+eq "$(head /dir/)" "20 text/gemini" "Unexpected head for /dir/"
+eq "$(get /dir/|wc -l|xargs)" "3" "Unexpected body for /dir/"
+echo OK GET /dir/ with auto index on
+
+check "should be running"
+quit
blob - d9bf46fbb00a0a25bea51cbaf4e8403f1302fd9c
blob + 490bd2c97b6aea7a9f2efc48f9236d0a8bf82dcb
--- sample.conf
+++ sample.conf
key "/path/to/key.pem"
root "/var/gemini/example.com"
- # enable CGI scripts in /cgi-bin/
+ # enable CGI scripts in /cgi-bin/
cgi "/cgi-bin/"
- # optional
- lang "it"
+ # optional
+ lang "it"
}
+
+# a server block with a location
+server "foo.com" {
+ cert "..."
+ key "..."
+ root "..."
+
+ location "/it/" {
+ lang "it"
+ }
+
+ location "/files" {
+ lang "en"
+ auto index on
+ }
+}
blob - 48a7701556fc2f134906f2d8c9ec6f7c73ca42b2
blob + 3c6b6e73a05d1b1b260837946773df372ee609e3
--- server.c
+++ server.c
}
int
+vhost_auto_index(struct vhost *v, const char *path)
+{
+ struct location *loc;
+ int auto_index = 0;
+
+ for (loc = v->locations; loc->match != NULL; ++loc) {
+ if (!fnmatch(loc->match, path, 0)) {
+ if (loc->auto_index)
+ auto_index = loc->auto_index;
+ }
+ }
+
+ return auto_index == 1;
+}
+
+int
check_path(struct client *c, const char *path, int *fd)
{
struct stat sb;
const char *p;
+ int flags;
assert(path != NULL);
else
p = path;
- if ((*fd = openat(c->host->dirfd, p, O_RDONLY | O_NOFOLLOW)) == -1) {
+ flags = O_RDONLY | O_NOFOLLOW;
+
+ if (*fd == -1 && (*fd = openat(c->host->dirfd, p, flags)) == -1)
return FILE_MISSING;
- }
if (fstat(*fd, &sb) == -1) {
LOGN(c, "failed stat for %s: %s", path, strerror(errno));
{
switch (check_path(c, c->iri.path, &c->fd)) {
case FILE_EXECUTABLE:
- if (c->host->cgi != NULL && starts_with(c->iri.path, c->host->cgi)) {
+ if (starts_with(c->iri.path, c->host->cgi)) {
start_cgi(c->iri.path, "", c->iri.query, fds, c);
return;
}
/* fallthrough */
case FILE_EXISTS:
- if ((c->len = filesize(c->fd)) == -1) {
- LOGE(c, "failed to get file size for %s", c->iri.path);
- close_conn(fds, c);
- return;
- }
-
- if ((c->buf = mmap(NULL, c->len, PROT_READ, MAP_PRIVATE,
- c->fd, 0)) == MAP_FAILED) {
- LOGW(c, "mmap: %s: %s", c->iri.path, strerror(errno));
- close_conn(fds, c);
- return;
- }
- c->i = c->buf;
- c->next = S_SENDING_FILE;
- start_reply(fds, c, SUCCESS, mime(c->host, c->iri.path));
+ load_file(fds, c);
return;
case FILE_DIRECTORY:
- close(c->fd);
- c->fd = -1;
- send_dir(fds, c);
+ open_dir(fds, c);
return;
case FILE_MISSING:
}
}
+void
+load_file(struct pollfd *fds, struct client *c)
+{
+ if ((c->len = filesize(c->fd)) == -1) {
+ LOGE(c, "failed to get file size for %s", c->iri.path);
+ start_reply(fds, c, TEMP_FAILURE, "internal server error");
+ return;
+ }
+ if ((c->buf = mmap(NULL, c->len, PROT_READ, MAP_PRIVATE,
+ c->fd, 0)) == MAP_FAILED) {
+ LOGW(c, "mmap: %s: %s", c->iri.path, strerror(errno));
+ start_reply(fds, c, TEMP_FAILURE, "internal server error");
+ return;
+ }
+ c->i = c->buf;
+ c->next = S_SENDING_FILE;
+ start_reply(fds, c, SUCCESS, mime(c->host, c->iri.path));
+}
+
/*
* the inverse of this algorithm, i.e. starting from the start of the
* path + strlen(cgi), and checking if each component, should be
}
void
-send_dir(struct pollfd *fds, struct client *c)
+open_dir(struct pollfd *fds, struct client *c)
{
size_t len;
-
- /* guard against a re-entrant call: open_file -> send_dir ->
- * open_file -> send_dir. This can happen only if:
- *
- * - user requested a dir, say foo/
- * - we try to serve foo/$INDEX
- * - foo/$INDEX is a directory.
- */
- if (c->iri.path == c->sbuf) {
- start_reply(fds, c, TEMP_REDIRECT, c->sbuf);
- return;
- }
+ int dirfd;
+ char *before_file;
- strlcpy(c->sbuf, "/", sizeof(c->sbuf));
-
len = strlen(c->iri.path);
- if (len > 0 && c->iri.path[len-1] != '/') {
- /* redirect to url with the trailing / */
- strlcat(c->sbuf, c->iri.path, sizeof(c->sbuf));
- strlcat(c->sbuf, "/", sizeof(c->sbuf));
- start_reply(fds, c, TEMP_REDIRECT, c->sbuf);
+ if (len > 0 && !ends_with(c->iri.path, "/")) {
+ redirect_canonical_dir(fds, c);
return;
}
+ strlcpy(c->sbuf, "/", sizeof(c->sbuf));
strlcat(c->sbuf, c->iri.path, sizeof(c->sbuf));
-
if (!ends_with(c->sbuf, "/"))
strlcat(c->sbuf, "/", sizeof(c->sbuf));
-
+ before_file = strchr(c->sbuf, '\0');
len = strlcat(c->sbuf, vhost_index(c->host, c->iri.path),
sizeof(c->sbuf));
+ if (len >= sizeof(c->sbuf)) {
+ start_reply(fds, c, TEMP_FAILURE, "internal server error");
+ return;
+ }
+ c->iri.path = c->sbuf;
+
+ /* close later unless we have to generate the dir listing */
+ dirfd = c->fd;
+ c->fd = -1;
+
+ switch (check_path(c, c->iri.path, &c->fd)) {
+ case FILE_EXECUTABLE:
+ if (starts_with(c->iri.path, c->host->cgi)) {
+ start_cgi(c->iri.path, "", c->iri.query, fds, c);
+ break;
+ }
+
+ /* fallthrough */
+
+ case FILE_EXISTS:
+ load_file(fds, c);
+ break;
+
+ case FILE_DIRECTORY:
+ start_reply(fds, c, TEMP_REDIRECT, c->sbuf);
+ break;
+
+ case FILE_MISSING:
+ *before_file = '\0';
+
+ if (!vhost_auto_index(c->host, c->iri.path)) {
+ start_reply(fds, c, NOT_FOUND, "not found");
+ break;
+ }
+
+ c->fd = dirfd;
+ c->next = S_SENDING_DIR;
+
+ if ((c->dir = fdopendir(c->fd)) == NULL) {
+ LOGE(c, "can't fdopendir(%d) (vhost:%s) %s: %s",
+ c->fd, c->host->domain, c->iri.path, strerror(errno));
+ start_reply(fds, c, TEMP_FAILURE, "internal server error");
+ return;
+ }
+ c->off = 0;
+
+ start_reply(fds, c, SUCCESS, "text/gemini");
+ return;
+
+ default:
+ /* unreachable */
+ abort();
+ }
+
+ close(dirfd);
+}
+
+void
+redirect_canonical_dir(struct pollfd *fds, struct client *c)
+{
+ size_t len;
+
+ strlcpy(c->sbuf, "/", sizeof(c->sbuf));
+ strlcat(c->sbuf, c->iri.path, sizeof(c->sbuf));
+ len = strlcat(c->sbuf, "/", sizeof(c->sbuf));
+
if (len >= sizeof(c->sbuf)) {
start_reply(fds, c, TEMP_FAILURE, "internal server error");
return;
}
- close(c->fd);
- c->iri.path = c->sbuf;
- open_file(fds, c);
+ start_reply(fds, c, TEMP_REDIRECT, c->sbuf);
}
+int
+read_next_dir_entry(struct client *c)
+{
+ struct dirent *d;
+
+ do {
+ errno = 0;
+ if ((d = readdir(c->dir)) == NULL) {
+ if (errno != 0)
+ LOGE(c, "readdir: %s", strerror(errno));
+ return 0;
+ }
+ } while (!strcmp(d->d_name, "."));
+
+ /* XXX: url escape */
+ snprintf(c->sbuf, sizeof(c->sbuf), "=> %s %s\n",
+ d->d_name, d->d_name);
+ c->len = strlen(c->sbuf);
+ c->off = 0;
+
+ return 1;
+}
+
+void
+send_directory_listing(struct pollfd *fds, struct client *c)
+{
+ ssize_t r;
+
+ while (1) {
+ if (c->len == 0) {
+ if (!read_next_dir_entry(c))
+ goto end;
+ }
+
+ while (c->len > 0) {
+ switch (r = tls_write(c->ctx, c->sbuf + c->off, c->len)) {
+ case -1:
+ goto end;
+
+ case TLS_WANT_POLLOUT:
+ fds->events = POLLOUT;
+ return;
+
+ case TLS_WANT_POLLIN:
+ fds->events = POLLIN;
+ return;
+
+ default:
+ c->off += r;
+ c->len -= r;
+ break;
+ }
+ }
+ }
+
+end:
+ close_conn(fds, c);
+}
+
void
cgi_poll_on_child(struct pollfd *fds, struct client *c)
{
if (c->fd != -1)
close(c->fd);
+ if (c->dir != NULL)
+ closedir(c->dir);
+
close(pfd->fd);
pfd->fd = -1;
}
clients[i].fd = -1;
clients[i].waiting_on_child = 0;
clients[i].buf = MAP_FAILED;
+ clients[i].dir = NULL;
clients[i].addr = addr;
connected_clients++;
send_file(fds, client);
break;
+ case S_SENDING_DIR:
+ send_directory_listing(fds, client);
+ break;
+
case S_SENDING_CGI:
handle_cgi(fds, client);
break;