From f60ea88c8d52714770e3b419450126bf630dc851 Mon Sep 17 00:00:00 2001 From: prx Date: Sun, 10 Jan 2021 09:30:35 +0100 Subject: [PATCH] add mimetype and autoindex option + minor changes * follow style(9) for prototypes * move first most used extension for more effeciciency when looking for mime * add opts.h to deal with options * remove lang=en by default * add option to set default mimetype * add option to autoindex if no index.gmi found * redirect if ending "/" is missing * send appropriate status code if request too long * edit manpage and README for new options --- Makefile | 2 +- README.md | 7 +- main.c | 205 ++++++++++++++++++----------- mimes.c | 9 +- mimes.h | 2 +- opts.h | 12 ++ tests/test.sh | 36 +++-- tests/var/gemini/autoidx/here.txt | 0 tests/var/gemini/autoidx/index.txt | 0 tests/var/gemini/autoidx/no.txt | 0 vger.8 | 15 ++- 11 files changed, 191 insertions(+), 97 deletions(-) create mode 100644 opts.h create mode 100644 tests/var/gemini/autoidx/here.txt create mode 100644 tests/var/gemini/autoidx/index.txt create mode 100644 tests/var/gemini/autoidx/no.txt diff --git a/Makefile b/Makefile index 5225483..c577aeb 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ all: vger clean: rm -f vger *.core *.o -vger: main.o mimes.o +vger: main.o mimes.o opts.h ${CC} -o vger main.o mimes.o install: vger diff --git a/README.md b/README.md index 8e1b4c6..aa3b96e 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,12 @@ without a `-d` parameter. **Vger** has a few parameters you can use in inetd configuration. - `-d PATH`: use `PATH` as the data directory to serve files from. Default is `/var/gemini` -- `-l LANG`: change the language in the status return code. Default is `en` +- `-l LANG`: change the language in the status return code. Default is no language specified. - `-v`: enable virtualhost support, the hostname in the query will be considered as a directory name. - `-u username`: enable chroot to the data directory and drop privileges to `username`. +- `-m MIME` : use MIME as default instead of "application/octet-stream". +- `-i` : Enable auto index if no "index.gmi" file is found in a directory. + # How to configure Vger using relayd and inetd @@ -94,4 +97,4 @@ On OpenBSD, enable inetd and relayd and start them: Don't forget to open the TCP port 1965 in your firewall. -Vger will serve files named `index.gmi` if no explicite filename is given. +Vger will serve files named `index.gmi` if no explicit filename is given. diff --git a/main.c b/main.c index ad392ba..9b73b05 100644 --- a/main.c +++ b/main.c @@ -1,5 +1,7 @@ #include +#include +#include #include #include #include @@ -12,21 +14,21 @@ #include #include "mimes.h" +#include "opts.h" #define GEMINI_PART 9 -#define DEFAULT_LANG "en" -#define DEFAULT_CHROOT "/var/gemini" #define GEMINI_REQUEST_MAX 1024 /* see https://gemini.circumlunar.space/docs/specification.html */ -void display_file(const char *, const char *); -void status(const int, const char *, const char *); -void status_redirect(const int code, const char *url); +void autoindex(const char *); +void display_file(const char *); +void status(const int, const char *); +void status_redirect(const int, const char *); void drop_privileges(const char *, const char *); -void eunveil(const char *path, const char *permissions); -size_t estrlcat(char *dst, const char *src, size_t dstsize); -size_t estrlcpy(char *dst, const char *src, size_t dstsize); +void eunveil(const char *, const char *); +size_t estrlcat(char *, const char *, size_t); +size_t estrlcpy(char *, const char *, size_t); void eunveil(const char *path, const char *permissions) @@ -44,7 +46,7 @@ estrlcpy(char *dst, const char *src, size_t dstsize) n = strlcpy(dst, src, dstsize); if (n >= dstsize) { - err(1, "estrlcpy failed"); + err(1, "strlcyp failed for %s = %s", dst, src); } return n; @@ -55,7 +57,7 @@ estrlcat(char *dst, const char *src, size_t dstsize) { size_t size; if ((size = strlcat(dst, src, dstsize)) >= dstsize) - err(1, "strlcat"); + err(1, "strlcat on %s + %s", dst, src); return size; } @@ -122,9 +124,9 @@ drop_privileges(const char *user, const char *path) } void -status(const int code, const char *file_mime, const char *lang) +status(const int code, const char *file_mime) { - printf("%i %s; lang=%s\r\n", + printf("%i %s; %s\r\n", code, file_mime, lang); } @@ -136,72 +138,123 @@ status_redirect(const int code, const char *url) } void -display_file(const char *path, const char *lang) +display_file(const char *uri) { FILE *fd = NULL; struct stat sb = {0}; ssize_t nread = 0; - char *buffer[BUFSIZ]; const char *file_mime; - char target[FILENAME_MAX] = ""; + char *buffer[BUFSIZ]; + char target[FILENAME_MAX] = {'\0'}; + char fp[PATH_MAX] = {'\0'}; + + /* build file path inside chroot */ + estrlcpy(fp, chroot_dir, sizeof(fp)); + estrlcat(fp, uri, sizeof(fp)); /* this is to check if path exists and obtain metadata later */ - if (stat(path, &sb) == -1) { + if (stat(fp, &sb) == -1) { - /* check if path is a symbolic link - * if so, redirect using its target */ - if (lstat(path, &sb) != -1 && S_ISLNK(sb.st_mode) == 1) - goto redirect; - else - goto err; + /* check if fp is a symbolic link + * if so, redirect using its target */ + if (lstat(fp, &sb) != -1 && S_ISLNK(sb.st_mode) == 1) + goto redirect; + else + goto err; } - /* check if directory */ if (S_ISDIR(sb.st_mode) != 0) { - /* look for index.gmi inside dir */ - char index_path[GEMINI_REQUEST_MAX] = {'\0'}; - estrlcpy(index_path, path, sizeof(index_path)); - estrlcat(index_path, "/index.gmi", sizeof(index_path)); - display_file(index_path, lang); + if (fp[strlen(fp) -1 ] != '/') { + /* no ending "/", redirect to "path/" */ + char new_uri[PATH_MAX] = {'\0'}; + estrlcpy(new_uri, uri, sizeof(fp)); + estrlcat(new_uri, "/", sizeof(fp)); + status_redirect(31, new_uri); + return; - } else { + } else { + /* there is a leading "/", display index.gmi */ + char index_path[PATH_MAX] = {'\0'}; + estrlcpy(index_path, fp, sizeof(index_path)); + estrlcat(index_path, "index.gmi", sizeof(index_path)); + + /* check if index.gmi exists or show autoindex */ + if (stat(index_path, &sb) == 0) { + estrlcpy(fp, index_path, sizeof(fp)); + } else if (doautoidx != 0) { + autoindex(fp); + return; + } else { + goto err; + } + } + } /* open the file requested */ - if ((fd = fopen(path, "r")) == NULL) { goto err; } + if ((fd = fopen(fp, "r")) == NULL) { goto err; } - file_mime = get_file_mime(path); + file_mime = get_file_mime(fp, default_mime); - status(20, file_mime, lang); + status(20, file_mime); - /* read the file and write it to stdout */ - while ((nread = fread(buffer, sizeof(char), sizeof(buffer), fd)) != 0) - fwrite(buffer, sizeof(char), nread, stdout); - goto closefd; - syslog(LOG_DAEMON, "path served %s", path); - } + /* read the file and write it to stdout */ + while ((nread = fread(buffer, sizeof(char), sizeof(buffer), fd)) != 0) + fwrite(buffer, sizeof(char), nread, stdout); + goto closefd; + syslog(LOG_DAEMON, "path served %s", fp); return; err: /* return an error code and no content */ - status(51, "text/gemini", lang); - syslog(LOG_DAEMON, "path invalid %s", path); + status(51, "text/gemini"); + syslog(LOG_DAEMON, "path invalid %s", fp); goto closefd; redirect: - /* read symbolic link target to redirect */ - if (readlink(path, target, FILENAME_MAX) == -1) { - goto err; + /* read symbolic link target to redirect */ + if (readlink(fp, target, FILENAME_MAX) == -1) { + goto err; } - status_redirect(30, target); - syslog(LOG_DAEMON, "redirection from %s to %s", path, target); + status_redirect(30, target); + syslog(LOG_DAEMON, "redirection from %s to %s", fp, target); closefd: - if (S_ISREG(sb.st_mode) != 0) { - fclose(fd); - } + if (S_ISREG(sb.st_mode) != 0) { + fclose(fd); + } +} + +void +autoindex(const char *path) +{ + struct dirent *dp; + DIR *fd; + + + if (!(fd = opendir(path))) { + err(1,"opendir '%s':", path); + } + + syslog(LOG_DAEMON, "autoindex: %s", path); + + status(20, "text/gemini"); + + /* TODO : add ending / in name if directory */ + while ((dp = readdir(fd))) { + /* skip self */ + if (!strcmp(dp->d_name, ".")) { + continue; + } + if (dp->d_type == DT_DIR) { + printf("=> ./%s/ %s/\n", dp->d_name, dp->d_name); + } else { + printf("=> ./%s %s\n", dp->d_name, dp->d_name); + } + } + closedir(fd); } int @@ -209,29 +262,32 @@ main(int argc, char **argv) { char request [GEMINI_REQUEST_MAX] = {'\0'}; char hostname [GEMINI_REQUEST_MAX] = {'\0'}; - char file [GEMINI_REQUEST_MAX] = {'\0'}; - char path [GEMINI_REQUEST_MAX] = DEFAULT_CHROOT; - char lang [3] = DEFAULT_LANG; + char uri [PATH_MAX] = {'\0'}; char user [_SC_LOGIN_NAME_MAX] = ""; int virtualhost = 0; int option = 0; - int chroot = 0; char *pos = NULL; - while ((option = getopt(argc, argv, ":d:l:u:v")) != -1) { + while ((option = getopt(argc, argv, ":d:l:m:u:vi")) != -1) { switch (option) { case 'd': - estrlcpy(path, optarg, sizeof(path)); + estrlcpy(chroot_dir, optarg, sizeof(chroot_dir)); + break; + case 'l': + estrlcpy(lang, "lang=", sizeof(lang)); + estrlcat(lang, optarg, sizeof(lang)); + break; + case 'm': + estrlcpy(default_mime, optarg, sizeof(default_mime)); + break; + case 'u': + estrlcpy(user, optarg, sizeof(user)); break; case 'v': virtualhost = 1; break; - case 'l': - estrlcpy(lang, optarg, sizeof(lang)); - break; - case 'u': - estrlcpy(user, optarg, sizeof(user)); - chroot = 1; + case 'i': + doautoidx = 1; break; } } @@ -239,18 +295,14 @@ main(int argc, char **argv) /* * do chroot if an user is supplied run pledge/unveil if OpenBSD */ - drop_privileges(user, path); - - /* change basedir to / to build the filepath if we use chroot */ - if (chroot == 1) - estrlcpy(path, "/", sizeof(path)); + drop_privileges(user, chroot_dir); /* * read 1024 chars from stdin * to get the request */ - if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL){ - /*TODO : add error code 5x */ + if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) { + status(59, "request is too long (1024 max)"); syslog(LOG_DAEMON, "request is too long (1024 max): %s", request); exit(1); } @@ -284,7 +336,7 @@ main(int argc, char **argv) if (pos != NULL) { /* if there is a / found */ /* separate hostname and uri */ - estrlcpy(file, pos, strlen(pos)+1); + estrlcpy(uri, pos, strlen(pos)+1); /* just keep hostname in request */ pos[0] = '\0'; } @@ -298,19 +350,22 @@ main(int argc, char **argv) estrlcpy(hostname, request, sizeof(hostname)); /* - * if virtualhost feature is actived looking under the default path + + * if virtualhost feature is actived looking under the chroot_path + * hostname directory gemini://foobar/hello will look for - * path/foobar/hello + * chroot_path/foobar/hello */ if (virtualhost) { - estrlcat(path, hostname, sizeof(path)); - estrlcat(path, "/", sizeof(path)); + if (strlen(uri) == 0) { + estrlcpy(uri, "/index.gmi", sizeof(uri)); + } + char new_uri[PATH_MAX] = {'\0'}; + estrlcpy(new_uri, hostname, sizeof(new_uri)); + estrlcat(new_uri, uri, sizeof(new_uri)); + estrlcpy(uri, new_uri, sizeof(uri)); } - /* add the base dir to the file requested */ - estrlcat(path, file, sizeof(path)); /* open file and send it to stdout */ - display_file(path, lang); + display_file(uri); return (0); } diff --git a/mimes.c b/mimes.c index 2293551..62dd542 100644 --- a/mimes.c +++ b/mimes.c @@ -3,11 +3,14 @@ #include #include "mimes.h" +#include "opts.h" static const struct { const char *extension; const char *type; } database[] = { + {"gmi", "text/gemini"}, + {"gemini", "text/gemini"}, {"7z", "application/x-7z-compressed"}, {"atom", "application/atom+xml"}, {"avi", "video/x-msvideo"}, @@ -24,9 +27,7 @@ static const struct { {"exe", "application/octet-stream"}, {"flv", "video/x-flv"}, {"fs", "application/octet-stream"}, - {"gemini", "text/gemini"}, {"gif", "image/gif"}, - {"gmi", "text/gemini"}, {"hqx", "application/mac-binhex40"}, {"htc", "text/x-component"}, {"html", "text/html"}, @@ -118,7 +119,7 @@ static const struct { #endif const char * -get_file_mime(const char *path) +get_file_mime(const char *path, const char *default_mime) { size_t i; char *extension; @@ -134,5 +135,5 @@ get_file_mime(const char *path) out: /* if no MIME have been found, set a default one */ - return ("text/gemini"); + return (default_mime); } diff --git a/mimes.h b/mimes.h index 22bd192..d6ae520 100644 --- a/mimes.h +++ b/mimes.h @@ -1 +1 @@ -const char *get_file_mime(const char *); +const char *get_file_mime(const char *, const char *); diff --git a/opts.h b/opts.h new file mode 100644 index 0000000..60974fc --- /dev/null +++ b/opts.h @@ -0,0 +1,12 @@ +#include /* PATH_MAX */ + +#define DEFAULT_MIME "application/octet-stream" +#define DEFAULT_LANG "" +#define DEFAULT_CHROOT "/var/gemini" +#define DEFAULT_AUTOIDX 0 + + /* longest is 56 so 64 should be enough */ +static char default_mime[64] = DEFAULT_MIME; +static char chroot_dir[PATH_MAX] = DEFAULT_CHROOT; +static char lang[16] = DEFAULT_LANG; +static unsigned int doautoidx = DEFAULT_AUTOIDX; diff --git a/tests/test.sh b/tests/test.sh index 5e3f0dd..94d108c 100644 --- a/tests/test.sh +++ b/tests/test.sh @@ -12,31 +12,35 @@ fi # serving a file OUT=$(printf "gemini://host.name/main.gmi\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "d11e0c0ff074f5627f2d2af72fd07104" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "c7e352d6aae4ee7e7604548f7874fb9d" ] ; then echo "error" ; exit 1 ; fi # default index.gmi file OUT=$(printf "gemini://host.name\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "3edd48286850d386592403956aec770f" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi # default index.gmi file when using a trailing slash OUT=$(printf "gemini://host.name/\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "3edd48286850d386592403956aec770f" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi # default index.gmi file when client specify port OUT=$(printf "gemini://host.name:1965\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "3edd48286850d386592403956aec770f" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi -# serving index.gmi automatically in a sub directory ending without "/" +# redirect to uri with trailing / if directory OUT=$(printf "gemini://host.name/subdir\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "d11e0c0ff074f5627f2d2af72fd07104" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "84e5e7bb3eee0dfcc8db14865dc83e77" ] ; then echo "error" ; exit 1 ; fi # file from local directory with lang=fr and markdown MIME type OUT=$(printf "gemini://perso.pw/file.md\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5) if ! [ $OUT = "e663f17730d5ddc24010c14a238e1e78" ] ; then echo "error" ; exit 1 ; fi -# file from local directory with lang=fr and unknwon MIME type (default to text/gemini) +# file from local directory with lang=fr and unknown MIME type (default to application/octet-stream) OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5) -if ! [ $OUT = "649a2e224632b679fd7599eafb13c001" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "a23b0053d759863a45da4afbffd847d2" ] ; then echo "error" ; exit 1 ; fi + +# file from local directory and unknown MIME type, default forced to text/plain +OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -m text/plain | tee /dev/stderr | $MD5) +if ! [ $OUT = "383a5a5ddb7bb30e3553ecb666378ebc" ] ; then echo "error" ; exit 1 ; fi # redirect file OUT=$(printf "gemini://perso.pw/old_location\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5) @@ -44,11 +48,11 @@ if ! [ $OUT = "cb4597b6fcc82cbc366ac9002fb60dac" ] ; then echo "error" ; exit 1 # file from local directory using virtualhosts OUT=$(printf "gemini://perso.pw/index.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "0d36a423a4e8be813fda4022f08b3844" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi # file from local directory using virtualhosts without specifying a file OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "0d36a423a4e8be813fda4022f08b3844" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi # file from local directory using virtualhosts without specifying a file using lang = fr OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | $MD5) @@ -56,11 +60,19 @@ if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 # file from local directory using virtualhosts and IRI OUT=$(printf "gemini://virtualhoßt/é è.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "bd30e2bb2dc2d7d18b5a3cb1af872c70" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "282cee071d3bd20dbb6e6af38f217a29" ] ; then echo "error" ; exit 1 ; fi # file from local directory using virtualhosts and IRI both with emojis OUT=$(printf "gemini://⛴//❤️.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5) -if ! [ $OUT = "9bed6f0c92d9c86cb46a32e845fb7161" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "e354a1a29ea8273faaf0cdc29c1d8583" ] ; then echo "error" ; exit 1 ; fi + +# auto index in directory without index.gmi must redirect +OUT=$(printf "gemini://host.name/autoidx\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | $MD5) +if ! [ $OUT = "874f5e1af67eff6b93bedf8ac8033066" ] ; then echo "error" ; exit 1 ; fi + +# auto index in directory +OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | $MD5) +if ! [ $OUT = "770a987b8f5cf7169e6bc3c6563e1570" ] ; then echo "error" ; exit 1 ; fi # must fail only on OpenBSD ! # try to escape from unveil diff --git a/tests/var/gemini/autoidx/here.txt b/tests/var/gemini/autoidx/here.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/var/gemini/autoidx/index.txt b/tests/var/gemini/autoidx/index.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/var/gemini/autoidx/no.txt b/tests/var/gemini/autoidx/no.txt new file mode 100644 index 0000000..e69de29 diff --git a/vger.8 b/vger.8 index 888a321..dbfd601 100644 --- a/vger.8 +++ b/vger.8 @@ -8,8 +8,10 @@ .Nm vger .Op Fl l Ar lang .Op Fl v +.Op Fl i .Op Fl d Ar path .Op Fl u Ar username +.Op Fl m Ar mimetype .Sh DESCRIPTION .Nm is a secure gemini server that is meant to be run on @@ -27,13 +29,22 @@ containing the new file location. .Bl -tag -width Ds .It Op Fl l Ar lang Set the default lang in the return code to -.Ar lang -instead of "en". +.Ar lang . +A list can be specified, i.e "-l en,fr" +will send "lang=en,fr". + +Default is no lang metadata. +.It Op Fl i +Enable auto index if no index.gmi is found in a directory. .It Op Fl v Enable virtualhost support, the hostname in the query will be considered as a directory name. As example, for request gemini://hostname.example/file.gmi .Nm will read the file /var/gemini/hostname.example/file.gmi +.It Op Fl m Ar mimetype +Use +.Ar mimetype +instead of the default "application/octet-stream" as MIME for files without or unrecognized extension. .It Op Fl d Ar path Use .Ar path