diff --git a/main.c b/main.c index 2c0786c..a850822 100644 --- a/main.c +++ b/main.c @@ -8,7 +8,10 @@ main(int argc, char **argv) char hostname[GEMINI_REQUEST_MAX] = {'\0'}; char path[GEMINI_REQUEST_MAX] = {'\0'}; char query[GEMINI_REQUEST_MAX] = {'\0'}; + char cgi_dir[PATH_MAX] = {'\0'}; + char rel_cgi_dir[PATH_MAX] = {'\0'}; char chroot_dir[PATH_MAX] = DEFAULT_CHROOT; + char tmp[PATH_MAX] = {'\0'}; int option = 0; int virtualhost = 0; @@ -34,7 +37,11 @@ main(int argc, char **argv) esnprintf(user, sizeof(user), "%s", optarg); break; case 'c': - esnprintf(cgi_dir, sizeof(cgi_dir), "%s", optarg); + esnprintf(rel_cgi_dir, sizeof(rel_cgi_dir), "%s", optarg); + /* remove leading / */ + while (*rel_cgi_dir == '/') + memmove(rel_cgi_dir, rel_cgi_dir+1, + strlen(rel_cgi_dir)); // strlen +1-1 break; case 'v': virtualhost = 1; @@ -45,29 +52,43 @@ main(int argc, char **argv) } } - /* - * do chroot if an user is supplied - */ - drop_privileges(user, chroot_dir, cgi_dir); - read_request(request); split_request(request, hostname, path, query); - set_path(path, sizeof(path), virtualhost, hostname); - /* percent decode */ + /* do chroot if an user is supplied */ + if (*user) + drop_privileges(user); + + /* set actual chroot_dir */ + if (virtualhost) { + esnprintf(tmp, sizeof(tmp), "%s/%s", chroot_dir, hostname); + esnprintf(chroot_dir, sizeof(chroot_dir), "%s", tmp); + } + + /* cgi_dir is in chroot_dir */ + if (*rel_cgi_dir) + esnprintf(cgi_dir, sizeof(cgi_dir), + "%s/%s", chroot_dir, rel_cgi_dir); + + set_rootdir(chroot_dir, cgi_dir, user); + + if (strlen(path) == 0) { /* this is root dir */ + esnprintf(path, sizeof(path), "./"); + } else { + uridecode(path); + remove_double_dot(path); + } + uridecode(query); - uridecode(path); - - remove_double_dot(path); /* is it cgi ? */ if (*cgi_dir) - if (do_cgi(chroot_dir, cgi_dir, path, hostname, query) == 0) + if (do_cgi(rel_cgi_dir, path, hostname, query) == 0) stop(EXIT_SUCCESS, NULL); /* *** from here, cgi didn't run *** */ /* check if path available */ - check_path(path, sizeof(path), virtualhost, strlen(hostname)); + check_path(path, sizeof(path)); /* regular file to stdout */ display_file(path); diff --git a/opts.h b/opts.h index d09f024..ec1a400 100644 --- a/opts.h +++ b/opts.h @@ -14,5 +14,3 @@ static char default_mime[64] = DEFAULT_MIME; static char lang[16] = DEFAULT_LANG; static unsigned int doautoidx = DEFAULT_AUTOIDX; -static char cgi_dir[PATH_MAX] = {'\0'}; -static int chrooted = 0; diff --git a/tests/test.sh b/tests/test.sh index 9ee0bf5..45781fe 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -80,23 +80,23 @@ OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | tee 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) +OUT=$(printf "gemini://host.name/cgi-bin/test.cgi\r\n" | ../vger -d var/gemini/ -c /cgi-bin | tee /dev/stderr | MD5) if ! [ $OUT = "666e48200f90018b5e96c2cf974882dc" ] ; then echo "error" ; exit 1 ; fi # cgi with use of variables -OUT=$(printf "gemini://host.name/cgi-bin/who.cgi?user=jean-mi\r\n" | ../vger -d var/gemini/ -c var/gemini/cgi-bin | tee /dev/stderr | MD5) +OUT=$(printf "gemini://host.name/cgi-bin/who.cgi?user=jean-mi\r\n" | ../vger -d var/gemini/ -c /cgi-bin | tee /dev/stderr | MD5) if ! [ $OUT = "fa065a67d1f7c973501d4a9e3ca2ea57" ] ; then echo "error" ; exit 1 ; fi # 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) +OUT=$(printf "gemini://host.name/cgi-bin/nope\r\n" | ../vger -d var/gemini/ -c /cgi-bin | tee /dev/stderr | MD5) 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) +OUT=$(printf "gemini://host.name/cgi-bin/test.cgi/path/info\r\n" | ../vger -d var/gemini -c /cgi-bin | tee /dev/stderr | MD5) if ! [ $OUT = "ec64da76dc578ffb479fbfb23e3a7a5b" ] ; then echo "error" ; exit 1 ; fi # virtualhost + cgi -OUT=$(printf "gemini://perso.pw/cgi-bin/test.cgi\r\n" | ../vger -v -d var/gemini/ -c var/gemini/perso.pw/cgi-bin | tee /dev/stderr | MD5) +OUT=$(printf "gemini://perso.pw/cgi-bin/test.cgi\r\n" | ../vger -v -d var/gemini/ -c /cgi-bin | tee /dev/stderr | MD5) if ! [ $OUT = "666e48200f90018b5e96c2cf974882dc" ] ; then echo "error" ; exit 1 ; fi # percent-decoding diff --git a/vger.8 b/vger.8 index dde7509..b46dc7c 100644 --- a/vger.8 +++ b/vger.8 @@ -45,11 +45,10 @@ will read the file /var/gemini/hostname.example/file.gmi Enable CGI support. .Ar cgi_path files will be executed as a cgi script instead of returning their content. -.Ar cgi_path must not end with '/'. -If using virtualhost, you must insert the virtualhost directory in the cgi path. +.Ar cgi_path should be relative to chroot so cgi can be called for different virtualhosts. As example, for a request gemini://hostname.example/cgi-bin/hello.cgi, one must set: .Bd -literal -offset indent -vger -c /var/gemini/hostname.example/cgi-bin/hello.cgi +vger -c cgi-bin .Ed .Pp In this case, diff --git a/vger.c b/vger.c index 79bf09e..a253501 100644 --- a/vger.c +++ b/vger.c @@ -99,7 +99,7 @@ uridecode(char *uri) } void -drop_privileges(const char *user, const char *chroot_dir, const char *cgi_dir) +drop_privileges(const char *user) { struct passwd *pw; @@ -107,22 +107,38 @@ drop_privileges(const char *user, const char *chroot_dir, const char *cgi_dir) * use chroot() if an user is specified requires root user to be * running the program to run chroot() and then drop privileges */ + + /* is 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) { + status(41, "privileges issue, see logs"); + stop(EXIT_FAILURE, + "the user %s can't be found on the system", user); + } + + /* drop privileges */ + 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)) { + status(41, "privileges issue, see logs"); + stop(EXIT_FAILURE, + "dropping privileges to user %s (uid=%i) failed", \ + user, pw->pw_uid); + } +} + +void +set_rootdir(const char *chroot_dir, const char *cgi_dir, const char *user) +{ + char capsule_dir[PATH_MAX] = {'\0'}; + if (*user) { - - /* is 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) { - status(41, "privileges issue, see logs"); - stop(EXIT_FAILURE, - "the user %s can't be found on the system", user); - } - /* chroot worked? */ if (chroot(chroot_dir) != 0) { status(41, "privileges issue, see logs"); @@ -130,26 +146,18 @@ drop_privileges(const char *user, const char *chroot_dir, const char *cgi_dir) "the chroot_dir %s can't be used for chroot", chroot_dir); } - chrooted = 1; - echdir("/"); - /* drop privileges */ - 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)) { - status(41, "privileges issue, see logs"); - stop(EXIT_FAILURE, - "dropping privileges to user %s (uid=%i) failed", \ - user, pw->pw_uid); - } + /* now chroot_dir is / */ + esnprintf(capsule_dir, sizeof(capsule_dir), "%s", "/"); + + } else { + esnprintf(capsule_dir, sizeof(capsule_dir), "%s", chroot_dir); } + #ifdef __OpenBSD__ /* * prevent access to files other than the one in chroot_dir */ - if (chrooted) - eunveil("/", "r"); - else - eunveil(chroot_dir, "r"); + eunveil(capsule_dir, "r"); /* permission to execute what's inside cgi_dir */ if (*cgi_dir) @@ -163,8 +171,7 @@ drop_privileges(const char *user, const char *chroot_dir, const char *cgi_dir) else epledge("stdio rpath", NULL); #endif - if (!chrooted) - echdir(chroot_dir); /* move to the gemini data directory */ + echdir(capsule_dir); /* move to the gemini data directory */ } ssize_t @@ -204,7 +211,7 @@ display_file(const char *path) } int -do_cgi(const char *chroot_dir, const char *cgi_dir, const char *path, const char *hostname, const char *query) +do_cgi(const char *rel_cgi_dir, const char *path, const char *hostname, const char *query) { /* WARNING : this function is fragile since it @@ -213,21 +220,11 @@ do_cgi(const char *chroot_dir, const char *cgi_dir, const char *path, const char * 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 - * cgi_dir + strlen(chrootdir) (skip chrootdir) - */ - - esnprintf(cgirp, sizeof(cgirp), "%s", cgi_dir + strlen(chroot_dir)); - /* ensure there is no leading / if user didn't end chrootdir with */ - while (*cgirp == '/') - memmove(cgirp, cgirp+1, strlen(cgirp+1)+1); - - if (strncmp(cgirp, path, strlen(cgirp)) != 0) + /* check if path starts with rel_cgi_dir */ + if (strncmp(rel_cgi_dir, path, strlen(rel_cgi_dir)) != 0) return 1; /* not in cgi_dir, go to display_file */ /* set env variables for CGI @@ -248,11 +245,11 @@ do_cgi(const char *chroot_dir, const char *cgi_dir, const char *path, const char */ /* find next item after cgi_dir in path: - * path + strlen(cgirp) + 1 (skip '/') + * path + strlen(rel_cgi_dir) + 1 (skip '/') */ /* cgi file to execute */ - esnprintf(cgifp, sizeof(cgifp), "%s", path + strlen(cgirp) + 1); + esnprintf(cgifp, sizeof(cgifp), "%s", path + strlen(rel_cgi_dir) + 1); if (!(*cgifp)) /* problem with cgi file, abort */ return 1; @@ -267,7 +264,7 @@ do_cgi(const char *chroot_dir, const char *cgi_dir, const char *path, const char esetenv("SCRIPT_NAME", cgifp, 1); esetenv("SERVER_NAME", hostname, 1); - echdir(cgirp); + echdir(rel_cgi_dir); cgi(cgifp); return 0; @@ -394,25 +391,8 @@ remove_double_dot(char *request) memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */ } -char * -set_path(char *path, size_t pathsiz, int virtualhost, const char *hostname) -{ - /* path is in a subdir named hostname */ - if (virtualhost) { - char tmp[GEMINI_REQUEST_MAX] = {'\0'}; - esnprintf(tmp, sizeof(tmp), "%s/%s", hostname, path); - esnprintf(path, pathsiz, "%s", tmp); - } - - if (strlen(path) == 0) /* this is root dir */ - esnprintf(path, pathsiz, "./"); - - - return path; -} - void -check_path(char *path, size_t pathsiz, int virtualhost, size_t hstnm_o) +check_path(char *path, size_t pathsiz) { struct stat sb = {0}; char tmp[PATH_MAX] = {'\0'}; @@ -431,11 +411,7 @@ check_path(char *path, size_t pathsiz, int virtualhost, size_t hstnm_o) if (S_ISDIR(sb.st_mode)) { /* check if dir path end with "/" */ if (path[strlen(path) - 1] != '/') { - /* redirect to the dir with appropriate ending '/' */ - if (virtualhost) /* skip hostname */ - esnprintf(tmp, sizeof(tmp), "/%s/", path+hstnm_o+1); - else - esnprintf(tmp, sizeof(tmp), "/%s/", path); + esnprintf(tmp, sizeof(tmp), "/%s/", path); status(31, "%s", tmp); stop(EXIT_SUCCESS, NULL); } diff --git a/vger.h b/vger.h index 4a4c40c..d3a687b 100644 --- a/vger.h +++ b/vger.h @@ -42,12 +42,12 @@ static char _request[GEMINI_REQUEST_MAX] = {'\0'}; ssize_t autoindex(const char *); void cgi(const char *); char * read_request(char *); -void check_path(char *, size_t, int, size_t); +void check_path(char *, size_t); 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 *); +int do_cgi(const char *, const char *, const char *, const char *); +void drop_privileges(const char *); +void set_rootdir(const char *, const char *, const char *); void remove_double_dot(char *); -char * set_path(char *, size_t, int, const char *); void split_request(const char *, char *, char *, char *); void status(const int, const char *, ...); void stop(const int, const char *, ...);