Commit Diff


commit - c8b74339185123feebb6164b91f500f1930e45ff
commit + 252908e6bb335c42249a3d5fe6ecaa4daf5a3e3e
blob - b7f05157b697387c34a2aa1d1ea441f66636fa41
blob + 1626a63f07560366fb17f7f63161ce309d4aeaf2
--- ChangeLog
+++ ChangeLog
@@ -1,5 +1,7 @@
 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
@@ -181,6 +181,9 @@ parameter will be added in the response.
 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
@@ -156,6 +156,9 @@ starts_with(const char *str, const char *prefix)
 {
 	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
@@ -17,10 +17,13 @@
 #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>
@@ -62,6 +65,7 @@ struct location {
 	char		*lang;
 	char		*default_mime;
 	char		*index;
+	int		 auto_index; /* 0 auto, -1 off, 1 on */
 };
 
 struct vhost {
@@ -119,6 +123,7 @@ enum {
 	S_OPEN,
 	S_INITIALIZING,
 	S_SENDING_FILE,
+	S_SENDING_DIR,
 	S_SENDING_CGI,
 	S_CLOSING,
 };
@@ -134,6 +139,7 @@ struct client {
 	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 */
 };
@@ -185,8 +191,10 @@ const char	*mime(struct vhost*, const char*);
 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*);
@@ -194,7 +202,10 @@ void		 handle_open_conn(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
@@ -67,6 +67,7 @@ root		return TROOT;
 cgi		return TCGI;
 lang		return TLANG;
 index		return TINDEX;
+auto		return TAUTO;
 
 [{}]		return *yytext;
 
blob - e7883a9d1705124c010c75d6c852d3090d06c8a2
blob + 5e7cb216241e588d84ac594ed7b7a2ef4a4671cc
--- parse.y
+++ parse.y
@@ -46,7 +46,7 @@ extern void yyerror(const char*);
 }
 
 %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
@@ -138,4 +138,5 @@ locopt		: TDEFAULT TTYPE TSTRING {
 			free(loc->index);
 			loc->index = $2;
 		}
+		| TAUTO TINDEX TBOOL	{ loc->auto_index = $3 ? 1 : -1; }
 		;
blob - 8cf2afb1ee11f9c4d717dd411e42e23e37d4c7fb
blob + ca87701f35435c53dcc1f64a02f7f06b21ff1509
--- regress/runtime
+++ regress/runtime
@@ -199,9 +199,25 @@ run
 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
@@ -19,9 +19,25 @@ server "it.example.com" {
 	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
@@ -78,10 +78,27 @@ vhost_index(struct vhost *v, const char *path)
 }
 
 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);
 
@@ -94,9 +111,10 @@ check_path(struct client *c, const char *path, int *fd
 	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));
@@ -117,7 +135,7 @@ open_file(struct pollfd *fds, struct client *c)
 {
 	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;
 		}
@@ -125,27 +143,11 @@ open_file(struct pollfd *fds, struct client *c)
 		/* 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:
@@ -162,7 +164,26 @@ open_file(struct pollfd *fds, struct client *c)
 	}
 }
 
+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
@@ -434,51 +455,159 @@ send_file(struct pollfd *fds, struct client *c)
 }
 
 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)
 {
@@ -624,6 +753,9 @@ close_conn(struct pollfd *pfd, struct client *c)
 	if (c->fd != -1)
 		close(c->fd);
 
+	if (c->dir != NULL)
+		closedir(c->dir);
+
 	close(pfd->fd);
 	pfd->fd = -1;
 }
@@ -658,6 +790,7 @@ do_accept(int sock, struct tls *ctx, struct pollfd *fd
 			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++;
@@ -688,6 +821,10 @@ handle(struct pollfd *fds, struct client *client)
 		send_file(fds, client);
 		break;
 
+	case S_SENDING_DIR:
+		send_directory_listing(fds, client);
+		break;
+
 	case S_SENDING_CGI:
 		handle_cgi(fds, client);
 		break;