diff options
-rw-r--r-- | ChangeLog | 2 | ||||
-rw-r--r-- | gmid.1 | 3 | ||||
-rw-r--r-- | gmid.c | 3 | ||||
-rw-r--r-- | gmid.h | 15 | ||||
-rw-r--r-- | lex.l | 1 | ||||
-rw-r--r-- | parse.y | 3 | ||||
-rwxr-xr-x | regress/runtime | 18 | ||||
-rw-r--r-- | sample.conf | 22 | ||||
-rw-r--r-- | server.c | 225 |
9 files changed, 241 insertions, 51 deletions
@@ -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 @@ -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 @@ -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; @@ -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*); @@ -67,6 +67,7 @@ root return TROOT; cgi return TCGI; lang return TLANG; index return TINDEX; +auto return TAUTO; [{}] return *yytext; @@ -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; } ; diff --git a/regress/runtime b/regress/runtime index 8cf2afb..ca87701 100755 --- a/regress/runtime +++ b/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 diff --git a/sample.conf b/sample.conf index d9bf46f..490bd2c 100644 --- a/sample.conf +++ b/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 + } } @@ -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,6 +164,25 @@ 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 @@ -434,49 +455,157 @@ 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; + int dirfd; + char *before_file; - /* 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); + len = strlen(c->iri.path); + if (len > 0 && !ends_with(c->iri.path, "/")) { + redirect_canonical_dir(fds, c); return; } 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, 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(); } - strlcat(c->sbuf, c->iri.path, sizeof(c->sbuf)); + close(dirfd); +} - if (!ends_with(c->sbuf, "/")) - strlcat(c->sbuf, "/", sizeof(c->sbuf)); +void +redirect_canonical_dir(struct pollfd *fds, struct client *c) +{ + size_t len; - len = strlcat(c->sbuf, vhost_index(c->host, c->iri.path), - sizeof(c->sbuf)); + 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 @@ -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 *fds, struct client *clients) 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; |