From f9733519459db6997f41c3bba671d6be624b8132 Mon Sep 17 00:00:00 2001 From: prx Date: Mon, 8 Aug 2022 22:57:03 +0200 Subject: [PATCH] reformat as much as possible. Now main() is much simpler. Removed all goto. Less variables in main. Simplified status_ to status(). Use a stop() function to log, send messages to stderr if necessary and close vger. Minor fixes with defaults, mimes There is still work to do to compare path using stat(). --- main.c | 202 ++++---------------- mimes.c | 18 +- opts.h | 2 +- tests.c | 6 +- tests/test.sh | 12 +- utils.c | 68 ++++--- utils.h | 14 +- vger.c | 496 +++++++++++++++++++++++++++++++++++--------------- vger.h | 30 ++- 9 files changed, 475 insertions(+), 373 deletions(-) diff --git a/main.c b/main.c index 5860649..fae3d9e 100644 --- a/main.c +++ b/main.c @@ -3,17 +3,16 @@ int main(int argc, char **argv) { - char request [GEMINI_REQUEST_MAX] = {'\0'}; - char user [_SC_LOGIN_NAME_MAX] = ""; - char hostname [GEMINI_REQUEST_MAX] = {'\0'}; - char query [PATH_MAX] = {'\0'}; - char chroot_dir[PATH_MAX] = DEFAULT_CHROOT; - char file [FILENAME_MAX] = DEFAULT_INDEX; - char dir [PATH_MAX] = {'\0'}; - char *pos = NULL; - int option = 0; - int virtualhost = 0; - int docgi = 0; + char request[GEMINI_REQUEST_MAX] = {'\0'}; + char user[_SC_LOGIN_NAME_MAX] = {'\0'}; + char hostname[GEMINI_REQUEST_MAX] = {'\0'}; + char query[PATH_MAX] = {'\0'}; + char path[PATH_MAX] = {'\0'}; + char chroot_dir[PATH_MAX] = DEFAULT_CHROOT; + char file[FILENAME_MAX] = DEFAULT_INDEX; + char dir[PATH_MAX] = {'\0'}; + int option = 0; + int virtualhost = 0; /* * request : contain the whole request from client : gemini://...\r\n @@ -41,8 +40,7 @@ main(int argc, char **argv) estrlcpy(user, optarg, sizeof(user)); break; case 'c': - estrlcpy(cgidir, optarg, sizeof(cgidir)); - docgi = 1; + estrlcpy(cgi_dir, optarg, sizeof(cgi_dir)); break; case 'v': virtualhost = 1; @@ -56,171 +54,35 @@ main(int argc, char **argv) /* * do chroot if an user is supplied */ - drop_privileges(user, chroot_dir); + drop_privileges(user, chroot_dir, cgi_dir); - /* - * read 1024 chars from stdin - * to get the request - * (actually 1024 + \0) - */ - if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) { - /* EOF reached before reading anything */ - if (feof(stdin)) { - status(59, "request is too short and probably empty"); - errlog("request is too short and probably empty"); - - /* error before reading anything */ - } else if (ferror(stdin)) { - status(59, "Error while reading request"); - errlog("Error while reading request: %s", request); - } - } - /* check if string ends with '\n', or to long */ - if (request[strnlen(request, GEMINI_REQUEST_MAX) - 1] != '\n') { - status(59, "request is too long (1024 max)"); - errlog("request is too long (1024 max): %s", request); - } - /* remove \r\n at the end of string */ - pos = strchr(request, '\r'); - if (pos != NULL) - *pos = '\0'; - - /* - * check if the beginning of the request starts with - * gemini:// - */ - if (strncmp(request, "gemini://", GEMINI_PART) != 0) { - /* error code url malformed */ - errlog("request «%s» doesn't match gemini://", - request); - } - syslog(LOG_DAEMON, "request %s", request); - - /* remove the gemini:// part */ - memmove(request, request + GEMINI_PART, strlen(request) + 1 - GEMINI_PART); - - /* remove all "/.." for safety reasons */ - while ((pos = strstr(request, "/..")) != NULL) - memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */ - - /* look for hostname in request : first thing before first / if any */ - pos = strchr(request, '/'); - if (pos != NULL) { - /* copy what's after hostname in dir */ - estrlcpy(dir, pos, strlen(pos) + 1); - /* just keep hostname in request : stop the string with \0 */ - pos[0] = '\0'; - } - /* check if client added :port at end of hostname and remove it */ - pos = strchr(request, ':'); - if (pos != NULL) { - /* end string at : */ - pos[0] = '\0'; - } - /* copy hostname from request */ - estrlcpy(hostname, request, sizeof(hostname)); - - /* remove leading '/' in dir */ - while (dir[0] == '/') - memmove(dir, dir + 1, strlen(dir + 1) + 1); - - if (virtualhost) { - /* add hostname at the beginning of the dir path */ - char tmp [PATH_MAX] = {'\0'}; - estrlcpy(tmp, hostname, sizeof(tmp)); - estrlcat(tmp, "/", sizeof(tmp)); - estrlcat(tmp, dir, sizeof(tmp)); - estrlcpy(dir, tmp, sizeof(dir)); - } - - /* remove a query string before percent decoding */ - /* look for "?" if any to set query for cgi, remove it */ - pos = strchr(dir, '?'); - if (pos != NULL) { - estrlcpy(query, pos + 1, sizeof(query)); - uridecode(query); - pos[0] = '\0'; - } + check_request(request); + get_hostname(request, hostname, sizeof(hostname)); + get_path(request, path, sizeof(path), virtualhost, hostname); + get_query(path, query, sizeof(query)); /* percent decode */ - uridecode(dir); + uridecode(query); + uridecode(path); - /* - * split dir and filename. file is last part after last '/'. if none - * found, then requested file is actually a directory + /* is it cgi ? */ + if (*cgi_dir) + if (do_cgi(chroot_dir, cgi_dir, path, hostname, query) == 0) + stop(EXIT_SUCCESS, NULL); + + /* *** from here, cgi didn't run *** + * check if path available */ - if (strlen(dir) > 0) { - /* - * if in cgidir, only the first file after cgidir/FILE is to be executed - * the rest is PATH_INFO - */ - /* find the string of cgidir without chroot_dir prefix */ - char tmp [PATH_MAX] = {'\0'}; - char *cgipp; - strip_trailing_slash(chroot_dir); - strip_trailing_slash(cgidir); - estrlcpy(tmp, cgidir + strlen(chroot_dir), sizeof(tmp)); - cgipp = tmp; - if (tmp[0] == '/') - cgipp++; + check_path(path, sizeof(path), hostname, virtualhost); - if (docgi && strncmp(dir, cgipp, strlen(cgipp)) == 0) { - pos = strchr(dir+strlen(cgipp), '/'); - } else { - pos = strrchr(dir, '/'); - } - if (pos != NULL) { - estrlcpy(file, pos + 1, sizeof(file)); /* +1 : no leading '/' */ - pos[0] = '\0'; + /* split dir and filename */ + get_dir_file(path, dir, sizeof(dir), file, sizeof(file)); - /* change directory to requested directory */ - if (strlen(dir) > 0) - echdir(dir); - } else { - estrlcpy(file, dir, sizeof(file)); - } - } - if (docgi) { - /* check if directory is cgidir */ - char cgifp [PATH_MAX] = {'\0'}; - estrlcpy(cgifp, chroot_dir, sizeof(cgifp)); - if (cgifp[strlen(cgifp) - 1] != '/') - estrlcat(cgifp, "/", sizeof(cgifp)); + /* go to dir */ + echdir(dir); - estrlcat(cgifp, dir, sizeof(cgifp)); - - /* not cgipath, display file content */ - if (strcmp(cgifp, cgidir) != 0) - goto file_to_stdout; - - /* set env variables for CGI */ - /* - * see - * https://lists.orbitalfox.eu/archives/gemini/2020/000315.htm - * l - */ - esetenv("GATEWAY_INTERFACE", "CGI/1.1", 1); - esetenv("SERVER_PROTOCOL", "GEMINI", 1); - esetenv("SERVER_SOFTWARE", "vger/1", 1); - - if (*query) - esetenv("QUERY_STRING", query, 1); - - /* if we've got here and file contains a '/', it should be interpreted as PATH_INFO */ - pos = strchr(file, '/'); - if (pos != NULL) { - setenv("PATH_INFO", pos, 1); - pos[0] = '\0'; /* keep only script name */ - } - esetenv("SCRIPT_NAME", file, 1); - esetenv("SERVER_NAME", hostname, 1); - - cgi(file); - return 0; - } -file_to_stdout: - /* regular file to stdout */ + /* regular file to stdout */ display_file(file); - return (0); + stop(EXIT_SUCCESS, NULL); } diff --git a/mimes.c b/mimes.c index 698bd61..7ee796c 100644 --- a/mimes.c +++ b/mimes.c @@ -126,16 +126,14 @@ get_file_mime(const char *path, const char *default_mime) char *extension; /* search for extension after last '.' in path */ - if ((extension = strrchr(path, '.')) == NULL) - goto out; - - /* look for the MIME in the database */ - for (i = 0; i < nitems(database); i++) { - if (strcmp(database[i].extension, extension + 1) == 0) - return (database[i].type); + if ((extension = strrchr(path, '.')) != NULL) { + /* look for the MIME in the database */ + for (i = 0; i < nitems(database); i++) { + if (strcmp(database[i].extension, extension + 1) == 0) + return (database[i].type); + } } - out: - /* if no MIME have been found, set a default one */ - return (default_mime); + /* no MIME found, set a default one */ + return default_mime; } diff --git a/opts.h b/opts.h index c3c1957..d09f024 100644 --- a/opts.h +++ b/opts.h @@ -14,5 +14,5 @@ static char default_mime[64] = DEFAULT_MIME; static char lang[16] = DEFAULT_LANG; static unsigned int doautoidx = DEFAULT_AUTOIDX; -static char cgidir[PATH_MAX] = {'\0'}; +static char cgi_dir[PATH_MAX] = {'\0'}; static int chrooted = 0; diff --git a/tests.c b/tests.c index b07311f..b740ca9 100644 --- a/tests.c +++ b/tests.c @@ -34,9 +34,9 @@ test_status(void) void test_status_error(void) { - status_error(51, "file not found"); - status_error(50, "Forbidden path"); - status_error(50, "Internal server error"); + status(51, "file not found"); + status(50, "Forbidden path"); + status(50, "Internal server error"); } int diff --git a/tests/test.sh b/tests/test.sh index 0bdc509..9ee0bf5 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -29,11 +29,11 @@ if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 # 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 = "b0e7e20db5ca7b80918025e7c15a8b02" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "84e5e7bb3eee0dfcc8db14865dc83e77" ] ; then echo "error" ; exit 1 ; fi # redirect to uri with trailing / if directory and vhost enabled OUT=$(printf "gemini://perso.pw/cgi-bin\r\n" | ../vger -vd var/gemini | tee /dev/stderr | MD5) -if ! [ $OUT = "827eef65a3cd71e2ce805bc1e05eac44" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "e0eb3a8e31bdb30c89d92d1d2b0a1fa1" ] ; 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) @@ -60,7 +60,7 @@ OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stde 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) +OUT=$(printf "gemini://perso.pw/\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | MD5) if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi # file from local directory using virtualhosts and IRI @@ -73,11 +73,11 @@ if ! [ $OUT = "e354a1a29ea8273faaf0cdc29c1d8583" ] ; then echo "error" ; exit 1 # 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 = "5742b21d465e377074408045a71656dc" ] ; then echo "error" ; exit 1 ; fi +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 = "2d4a82fea3f10ab3e123e9f9d5dd1fbc" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "765bbbe2add810be8eb191bbde59e258" ] ; then echo "error" ; exit 1 ; fi # cgi simple script OUT=$(printf "gemini://host.name/cgi-bin/test.cgi\r\n" | ../vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5) @@ -89,7 +89,7 @@ if ! [ $OUT = "fa065a67d1f7c973501d4a9e3ca2ea57" ] ; then echo "error" ; exit 1 # cgi with error OUT=$(printf "gemini://host.name/cgi-bin/nope\r\n" | ../vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5) -if ! [ $OUT = "74ba4b36dcebec9ce9dae33033f3378a" ] ; then echo "error" ; exit 1 ; fi +if ! [ $OUT = "31b98e160402a073298c12f763d5db64" ] ; then echo "error" ; exit 1 ; fi # cgi with PATH_INFO OUT=$(printf "gemini://host.name/cgi-bin/test.cgi/path/info\r\n" | ../vger -d var/gemini -c var/gemini/cgi-bin | tee /dev/stderr | MD5) diff --git a/utils.c b/utils.c index ce8cf0c..080a60a 100644 --- a/utils.c +++ b/utils.c @@ -7,6 +7,7 @@ #include #include "utils.h" +#include "vger.h" #if defined(__OpenBSD__) || defined(__FreeBSD__) || defined( __NetBSD__) || defined(__DragonFly__) #include @@ -25,8 +26,8 @@ void eunveil(const char *path, const char *permissions) { if (unveil(path, permissions) == -1) { - syslog(LOG_DAEMON, "unveil on %s failed", path); - err(1, "unveil on %s failed", path); + status(41, "Error when unveil(), see logs"); + stop(EXIT_FAILURE, "unveil on %s failed", path); } } @@ -34,8 +35,8 @@ void epledge(const char *promises, const char *execpromises) { if (pledge(promises, execpromises) == -1) { - syslog(LOG_DAEMON, "pledge failed for: %s", promises); - err(1, "pledge failed for: %s", promises); + status(41, "Error when pledge(), see logs"); + stop(EXIT_FAILURE, "pledge failed for: %s", promises); } } #endif @@ -47,7 +48,8 @@ estrlcpy(char *dst, const char *src, size_t dstsize) n = strlcpy(dst, src, dstsize); if (n >= dstsize) { - err(1, "strlcpy failed for %s = %s", dst, src); + status(41, "strlcpy failed, see logs"); + stop(EXIT_FAILURE, "strlcpy() failed for %s = %s", dst, src); } return n; @@ -56,11 +58,13 @@ estrlcpy(char *dst, const char *src, size_t dstsize) size_t estrlcat(char *dst, const char *src, size_t dstsize) { - size_t size; - if ((size = strlcat(dst, src, dstsize)) >= dstsize) - err(1, "strlcat on %s + %s", dst, src); + size_t size; + if ((size = strlcat(dst, src, dstsize)) >= dstsize) { + status(41, "strlcat() failed, see logs"); + stop(EXIT_FAILURE, "strlcat on %s + %s", dst, src); + } - return size; + return size; } int @@ -70,25 +74,43 @@ esetenv(const char *name, const char *value, int overwrite) ret = setenv(name, value, overwrite); if (ret != 0) { - err(1, "setenv %s:%s", name, value); + status(41, "setenv() failed, see logs"); + stop(EXIT_FAILURE, "setenv() %s:%s", name, value); } return ret; } -/* send error in syslog, to stdout and die */ void -errlog(const char *format, ...) +echdir(const char *path) { - char e[1024] = {'\0'}; - va_list ap; - - fflush(stdout); /* make sure older messages are printed */ - - va_start(ap, format); - vsnprintf(e, sizeof(e), format, ap); - va_end(ap); - - syslog(LOG_DAEMON, "%s", e); - err(1, "%s", e); + if (chdir(path) == -1) { + switch (errno) { + case ENOTDIR: /* FALLTHROUGH */ + case ENOENT: + status(51, "file not found"); + break; + case EACCES: + status(50, "Forbidden path"); + break; + default: + status(50, "Internal server error"); + break; + } + stop(EXIT_FAILURE, "chdir(%s) failed", path); + } +} + +/* read the file fd byte after byte in buffer and write it to stdout + * return number of bytes read + */ +size_t +print_file(FILE *fd) +{ + ssize_t nread = 0; + char *buffer[BUFSIZ]; + + while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0) + fwrite(buffer, 1, nread, stdout); + return nread; } diff --git a/utils.h b/utils.h index 8d68748..982a3e0 100644 --- a/utils.h +++ b/utils.h @@ -1,6 +1,8 @@ -void epledge(const char *, const char *); -void errlog(const char *format, ...); -void eunveil(const char *, const char *); -int esetenv(const char *, const char *, int); -size_t estrlcat(char *, const char *, size_t); -size_t estrlcpy(char *, const char *, size_t); +void echdir (const char *); +void epledge(const char *, const char *); +void eunveil(const char *, const char *); +int esetenv(const char *, const char *, int); +size_t estrlcat(char *, const char *, size_t); +size_t estrlcpy(char *, const char *, size_t); +size_t print_file(FILE *fd); +void set_errmsg(const char *, ...); diff --git a/vger.c b/vger.c index 6292838..d02f420 100644 --- a/vger.c +++ b/vger.c @@ -21,25 +21,46 @@ #include "utils.h" #include "vger.h" +void +stop(const int r, const char *fmt, ...) +{ + va_list ap, ap2; + + fflush(stdout); /* ensure all data is sent */ + + /* log the request and retcode */ + syslog(LOG_DAEMON, "\"%s\" %i %zd", _request, _retcode, _datasiz); + + if (r != EXIT_SUCCESS) { + /* log and print error */ + va_copy(ap2, ap); + + va_start(ap, fmt); + vsyslog(LOG_ERR, fmt, ap); + va_end(ap); + + va_start(ap2, fmt); + vfprintf(stderr, fmt, ap2); + va_end(ap2); + } + + exit(r); +} void -echdir(const char *path) +status(const int code, const char *fmt, ...) { - if (chdir(path) == -1) { - switch (errno) { - case ENOTDIR: /* FALLTHROUGH */ - case ENOENT: - status_error(51, "file not found"); - break; - case EACCES: - status_error(50, "Forbidden path"); - break; - default: - status_error(50, "Internal server error"); - break; - } - errlog("failed to chdir(%s)", path); - } + va_list ap; + + _datasiz += fprintf(stdout, "%i ", code); + + va_start(ap, fmt); + _datasiz += vfprintf(stdout, fmt, ap); + va_end(ap); + + _datasiz += fprintf(stdout, "\r\n"); /* make sure status end correctly */ + + _retcode = code; /* store return code for logs */ } int @@ -77,27 +98,36 @@ uridecode(char *uri) } void -drop_privileges(const char *user, const char *path) +drop_privileges(const char *user, const char *chroot_dir, const char *cgi_dir) { - struct passwd *pw; + struct passwd *pw; /* * use chroot() if an user is specified requires root user to be * running the program to run chroot() and then drop privileges */ - if (strlen(user) > 0) { + if (*user) { /* is root? */ - if (getuid() != 0) - errlog("chroot requires program to be run as root"); + if (getuid() != 0) { + status(41, "privileges issue, see logs"); + stop(EXIT_FAILURE, "%s", + "chroot requires program to be run as root"); + } /* search user uid from name */ - if ((pw = getpwnam(user)) == NULL) - errlog("the user %s can't be found on the system", user); + if ((pw = getpwnam(user)) == NULL) { + status(41, "privileges issue, see logs"); + stop(EXIT_FAILURE, + "the user %s can't be found on the system", user); + } /* chroot worked? */ - if (chroot(path) != 0) - errlog("the chroot_dir %s can't be used for chroot", path); + if (chroot(chroot_dir) != 0) { + status(41, "privileges issue, see logs"); + stop(EXIT_FAILURE, + "the chroot_dir %s can't be used for chroot", chroot_dir); + } chrooted = 1; echdir("/"); @@ -105,158 +135,159 @@ drop_privileges(const char *user, const char *path) if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) { - errlog("dropping privileges to user %s (uid=%i) failed", - user, pw->pw_uid); + status(41, "privileges issue, see logs"); + stop(EXIT_FAILURE, + "dropping privileges to user %s (uid=%i) failed", \ + user, pw->pw_uid); } } #ifdef __OpenBSD__ /* - * prevent access to files other than the one in path + * prevent access to files other than the one in chroot_dir */ if (chrooted) - eunveil("/", "r"); + eunveil("/", "r"); else - eunveil(path, "r"); + eunveil(chroot_dir, "r"); - /* permission to execute what's inside cgidir */ - if (strlen(cgidir) > 0) - eunveil(cgidir, "rx"); + /* permission to execute what's inside cgi_dir */ + if (*cgi_dir) + eunveil(cgi_dir, "rx"); eunveil(NULL, NULL); /* no more call to unveil() */ /* promise permissions */ - if (strlen(cgidir) > 0) - epledge("stdio rpath exec", NULL); + if (*cgi_dir) + epledge("stdio rpath exec", NULL); else - epledge("stdio rpath", NULL); + epledge("stdio rpath", NULL); #endif if (!chrooted) - echdir(path); /* move to the gemini data directory */ + echdir(chroot_dir); /* move to the gemini data directory */ } -void -status(const int code, const char *file_mime) -{ - if (strcmp(file_mime, "text/gemini") == 0) - printf("%i %s; %s\r\n", code, file_mime, lang); - else - printf("%i %s\r\n", code, file_mime); -} - -void -status_redirect(const int code, const char *url) -{ - printf("%i %s\r\n", - code, url); -} - -void -status_error(const int code, const char *reason) -{ - printf("%i %s\r\n", - code, reason); -} - -void +ssize_t display_file(const char *fname) { FILE *fd = NULL; - struct stat sb = {0}; - ssize_t nread = 0; const char *file_mime; - char *buffer[BUFSIZ]; - char target[FILENAME_MAX] = {'\0'}; - char tmp[PATH_MAX] = {'\0'}; /* used to build - * temporary path */ /* - * special case : fname empty. The user requested just the directory - * name + * special case : fname empty. The user requested just a dir name */ - if (strlen(fname) == 0) { - if (stat("index.gmi", &sb) == 0) { - /* there is index.gmi in the current directory */ - display_file("index.gmi"); - return; - } else if (doautoidx) { - /* no index.gmi, so display autoindex if enabled */ - autoindex("."); - return; - } else { - goto err; - } - } - /* this is to check if path exists and obtain metadata later */ - if (stat(fname, &sb) == -1) { - /* - * check if fname is a symbolic link if so, redirect using - * its target - */ - if (lstat(fname, &sb) != -1 && S_ISLNK(sb.st_mode) == 1) - goto redirect; - else - goto err; - } - /* check if directory */ - if (S_ISDIR(sb.st_mode) != 0) { - /* no ending "/", redirect to "fname/" */ - estrlcpy(tmp, fname, sizeof(tmp)); - estrlcat(tmp, "/", sizeof(tmp)); - status_redirect(31, tmp); - return; + if ((strlen(fname) == 0) && (doautoidx)) { + /* no index.gmi, so display autoindex if enabled */ + _datasiz += autoindex("."); + return _datasiz; } + /* open the file requested */ - if ((fd = fopen(fname, "r")) == NULL) - goto err; + if ((fd = fopen(fname, "r")) != NULL) { + file_mime = get_file_mime(fname, default_mime); + if (strcmp(file_mime, "text/gemini") == 0) + status(20, "%s; %s", file_mime, lang); + else + status(20, "%s", file_mime); - file_mime = get_file_mime(fname, default_mime); + _datasiz += print_file(fd); + fclose(fd); /* close file descriptor */ + } else { + /* return an error code and no content. + * seems unlikely to happen unless the file vanished + * since we checked with stat() if it exists + */ + status(51, "%s", "file not found and may have vanished"); + } - status(20, file_mime); - - /* read the file byte after byte in buffer and write it to stdout */ - while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0) - fwrite(buffer, 1, nread, stdout); - goto closefd; /* close file descriptor */ - syslog(LOG_DAEMON, "path served %s", fname); - - return; - -err: - /* return an error code and no content */ - status_error(51, "file not found"); - syslog(LOG_DAEMON, "path invalid %s", fname); - goto closefd; - -redirect: - /* read symbolic link target to redirect */ - if (readlink(fname, target, FILENAME_MAX) == -1) - goto err; - - status_redirect(30, target); - syslog(LOG_DAEMON, "redirection from %s to %s", fname, target); - -closefd: - if (S_ISREG(sb.st_mode) != 0) - fclose(fd); + return _datasiz; } -void +int +do_cgi(const char *chroot_dir, const char *cgi_dir, const char *path, const char *hostname, const char *query) +{ + + /* WARNING : this function is fragile since it + * compares path using the string to access them. + * It would be preferable to use stat() to check + * if two path refer to the same inode + */ + + char cgirp[PATH_MAX] = {'\0'}; /* cgi dir path in chroot */ + char cgifp[PATH_MAX] = {'\0'}; /* cgi file to execute */ + char *path_info = NULL; + + /* check if path starts with cgi_dir + * compare beginning of path with cgi_dir + * path + 2 : skip "./" + * cgi_dir + strlen(chrootdir) (skip chrootdir) + */ + + estrlcpy(cgirp, cgi_dir + strlen(chroot_dir), sizeof(cgirp)); + /* ensure there is no leading / if user didn't end chrootdir with */ + while (*cgirp == '/') + estrlcpy(cgirp, cgirp+1, sizeof(cgirp)); + + if (strncmp(cgirp, path+2, strlen(cgirp)) != 0) + return 1; /* not in cgi_dir, go to display_file */ + + /* set env variables for CGI + * see + * https://lists.orbitalfox.eu/archives/gemini/2020/000315.html + */ + esetenv("GATEWAY_INTERFACE", "CGI/1.1", 1); + esetenv("SERVER_PROTOCOL", "GEMINI", 1); + esetenv("SERVER_SOFTWARE", "vger/1", 1); + + if (*query) + esetenv("QUERY_STRING", query, 1); + + /* + * if in cgi_dir, only the first file after cgi_dir/FILE + * is to be executed + * the rest is PATH_INFO + */ + + /* find next item after cgi_dir in path: + * path + 2 (skip "./") + strlen(cgirp) + 1 (skip '/') + */ + + /* cgi file to execute */ + estrlcpy(cgifp, path + 2 + strlen(cgirp) + 1, sizeof(cgifp)); + if (!(*cgifp)) /* problem with cgi file, abort */ + return 1; + + /* check if there is something after cgi file for PATH_INFO */ + path_info = strchr(cgifp, '/'); + + if (path_info != NULL) { + esetenv("PATH_INFO", path_info, 1); + *path_info = '\0'; /* stop cgifp before PATH_INFO */ + } + + esetenv("SCRIPT_NAME", cgifp, 1); + esetenv("SERVER_NAME", hostname, 1); + + echdir(cgirp); + + cgi(cgifp); + return 0; +} + +ssize_t autoindex(const char *path) { - /* display liks to files in path + a link to parent (..) */ + /* display list of files in path + a link to parent (..) */ int n = 0; struct dirent **namelist; /* this must be freed at last */ - - syslog(LOG_DAEMON, "autoindex: %s", path); + size_t bs = 0; /* use alphasort to always have the same order on every system */ if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) { - status_error(50, "Internal server error"); - errlog("Can't scan %s", path); + status(50, "Can't scan %s", path); } else { status(20, "text/gemini"); - printf("=> .. ../\n"); /* display link to parent */ + bs += fprintf(stdout, "=> .. ../\n"); /* display link to parent */ for (int j = 0; j < n; j++) { /* skip self and parent */ if ((strcmp(namelist[j]->d_name, ".") == 0) || @@ -264,25 +295,33 @@ autoindex(const char *path) continue; } /* add "/" at the end of a directory path */ - if (namelist[j]->d_type == DT_DIR) - printf("=> ./%s/ %s/\n", namelist[j]->d_name, namelist[j]->d_name); - else - printf("=> ./%s %s\n", namelist[j]->d_name, namelist[j]->d_name); + if (namelist[j]->d_type == DT_DIR) { + bs += fprintf(stdout, "=> ./%s/ %s/\n", + namelist[j]->d_name, namelist[j]->d_name); + } else { + bs += fprintf(stdout, "=> ./%s %s\n", + namelist[j]->d_name, namelist[j]->d_name); + } free(namelist[j]); } free(namelist); } + + return bs; } void cgi(const char *cgicmd) { + /* TODO? cgi currently return the wrong data size unless we switch from execl to popen */ + /* run cgicmd replacing current process */ + _datasiz = -1; /* bytes sent by cgi are unknown */ execl(cgicmd, cgicmd, NULL); /* if execl is ok, this will never be reached */ - status(42, "Couldn't execute CGI script"); - errlog("error when trying to execl %s", cgicmd); - exit(1); + status(42, "error when trying run cgi"); + stop(EXIT_FAILURE, "error when trying to execl %s", cgicmd); + } void @@ -295,3 +334,172 @@ strip_trailing_slash(char *path) while (path[end] == '/') path[end--] = '\0'; } + +char * +check_request(char *request) +{ +/* +* read the request, check for errors and sanitize the input +*/ + char *pos = NULL; + + + /* read 1024 +1 chars from stdin to get the request (1024 + \0) */ + if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) { + /* EOF reached before reading anything */ + if (feof(stdin)) { + status(59, "%s", "request is too short and probably empty"); + stop(EXIT_FAILURE, "%s", "request is too short and probably empty"); + + /* error before reading anything */ + } else if (ferror(stdin)) { + status(59, "Error while reading request: %s", request); + stop(EXIT_FAILURE, "Error while reading request: %s", request); + } + } + + /* check if string ends with '\n', or to long */ + if (request[strnlen(request, GEMINI_REQUEST_MAX) - 1] != '\n') { + status(59, "request is too long (1024 max): %s", request); + stop(EXIT_FAILURE, "request is too long (1024 max): %s", request); + } + + /* remove \r\n at the end of string */ + request[strcspn(request, "\r\n")] = '\0'; + + /* + * check if the beginning of the request starts with + * gemini:// + */ + if (strncmp(request, "gemini://", GEMINI_PART) != 0) { + /* error code url malformed */ + status(59, "request «%s» doesn't match gemini://", request); + stop(EXIT_FAILURE, "request «%s» doesn't match gemini://", request); + } + + /* save request for logs */ + estrlcpy(_request, request, sizeof(_request)); + + /* remove the gemini:// part */ + memmove(request, request + GEMINI_PART, strlen(request) + 1 - GEMINI_PART); + + /* remove all "/.." for safety reasons */ + while ((pos = strstr(request, "/..")) != NULL) + memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */ + + return request; +} + +char * +get_hostname(const char *request, char *hstnm, size_t hstnmsiz) +{ + char *pos = NULL; + + /* first make a copy of request */ + estrlcpy(hstnm, request, hstnmsiz); + + /* look for hostname : stops at first '/' if any */ + if ( (pos = strchr(hstnm, '/')) != NULL) + pos[0] = '\0'; /* end string at the end of hostname */ + + /* check if client added :port at end of hostname and remove it */ + if ( (pos = strchr(hstnm, ':')) != NULL) + pos[0] = '\0'; /* end string at : */ + + return hstnm; +} + +char * +get_path(const char *request, char *path, size_t pathsiz, int virtualhost, const char *hostname) +{ + char *pos = NULL; + + /* path must be relative to chroot */ + estrlcpy(path, "./", pathsiz); + + /* path is in a subdir named hostname */ + if (virtualhost) { + estrlcat(path, hostname, pathsiz); + estrlcat(path, "/", pathsiz); + } + + /* path is after hostname/ */ + pos = strchr(request, '/'); + if (pos != NULL) /* append the path. pos +1 to remove leading '/' */ + estrlcat(path, pos+1, pathsiz); + + return path; +} + +void +check_path(char *path, size_t pathsiz, const char *hstnm, int virtualhost) +{ + struct stat sb = {0}; + char tmp[PATH_MAX] = {'\0'}; + + if (stat(path, &sb) == -1) { + if (lstat(path, &sb) != -1 && S_ISLNK(sb.st_mode) == 1) { + if (readlink(path, tmp, sizeof(tmp)) > 0) { + status(30, "%s", tmp); + stop(EXIT_SUCCESS, NULL); + } + } + status(51, "%s", "file not found"); + stop(EXIT_SUCCESS, NULL); + } + + if (S_ISDIR(sb.st_mode)) { + /* check if dir path end with "/" */ + if (path[strlen(path) - 1] != '/') { + /* redirect to the dir with appropriate ending '/' */ + + /* remove leading '.' for redirection*/ + if (virtualhost) /* remove ./host.name */ + memmove(path, path+2+strlen(hstnm), + strlen(path + 2) + strlen(hstnm) + 1); + else + memmove(path, path+1, + strlen(path + 1) + 1); /* +1 for \0 */ + + estrlcat(path, "/", pathsiz); + status(31, "%s", path); + stop(EXIT_SUCCESS, NULL); + } + /* check if DEFAULT_INDEX exists in directory */ + estrlcpy(tmp, path, sizeof(tmp)); + estrlcat(tmp, "/", sizeof(tmp)); + estrlcat(tmp, DEFAULT_INDEX, sizeof(tmp)); + if (stat(tmp, &sb) == 0) + estrlcpy(path, tmp, pathsiz); + } +} + +void +get_dir_file(char *path, char *dir, size_t dirsiz, char *file, size_t filesiz) +{ + char *pos = NULL; + + pos = strrchr(path, '/'); + if (pos != NULL) { + estrlcpy(file, pos+1, filesiz); /* +1 : not heading / */ + pos[0] = '\0'; /* stop path at file */ + estrlcpy(dir, path, dirsiz); + } else { + estrlcpy(file, path, filesiz); + } +} + +char * +get_query(char *path, char *query, size_t querysiz) +{ + char *pos = NULL; + + /* remove a query string before percent decoding */ + /* look for "?" if any to set query for cgi, remove it */ + pos = strchr(path, '?'); + if (pos != NULL) { + estrlcpy(query, pos + 1, querysiz); + pos[0] = '\0'; /* path end where query begins */ + } + return query; +} diff --git a/vger.h b/vger.h index a3d0ba2..0585e4a 100644 --- a/vger.h +++ b/vger.h @@ -11,17 +11,27 @@ */ #define GEMINI_REQUEST_MAX 1025 -void autoindex(const char *); -void cgi (const char *cgicmd); -void display_file(const char *); -void drop_privileges(const char *, const char *); -void echdir (const char *); -void status (const int, const char *); -void status_redirect(const int, const char *); -void status_error(const int, const char *); -void strip_trailing_slash(char *path); -int uridecode (char *); +/* global vars */ +static int _retcode = 0; +static ssize_t _datasiz = 0; +static char _request[GEMINI_REQUEST_MAX] = {'\0'}; +/* functions */ +ssize_t autoindex(const char *); +void cgi(const char *); +char * check_request(char *); +void check_path(char *, size_t, const char *, int); +ssize_t display_file(const char *); +int do_cgi(const char *, const char *, const char *, const char *, const char *); +void drop_privileges(const char *, const char *, const char *); +void get_dir_file(char *, char *, size_t, char *, size_t); +char * get_hostname(const char *, char *, size_t); +char * get_path(const char *, char *, size_t, int, const char *); +char * get_query(char *, char *, size_t); +void status(const int, const char *, ...); +void strip_trailing_slash(char *); +int uridecode (char *); +void stop(const int, const char *, ...); #endif // vger_h_INCLUDED