Merge pull request 'relative_cgi' (#10) from relative_cgi into master

Reviewed-on: #10
master 2.0.0
solene 2 weeks ago
commit cad05817d3
  1. 47
      main.c
  2. 2
      opts.h
  3. 10
      tests/test.sh
  4. 5
      vger.8
  5. 139
      vger.c
  6. 8
      vger.h

@ -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 */
uridecode(query);
uridecode(path);
/* 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);
}
remove_double_dot(path);
/* 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);
/* 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);

@ -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;

@ -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

@ -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,

139
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
*/
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");
}
/* 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);
}
/* 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) {
/* 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,31 +211,25 @@ 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
* 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;
struct stat sbcgi = {0};
struct stat sbpath = {0};
char cgifp[PATH_MAX] = {'\0'}; /* cgi file to execute */
char path_dir[PATH_MAX] = {'\0'};
char *path_info = NULL;
/* check if path starts with cgi_dir
* compare beginning of path with cgi_dir
* cgi_dir + strlen(chrootdir) (skip chrootdir)
*/
/* get beginning of path */
/* path_dir is initialized so there is an \0 at the end */
memcpy(path_dir, path, strlen(rel_cgi_dir));
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 (stat(rel_cgi_dir, &sbcgi) + stat(path_dir, &sbpath) != 0)
goto nocgi;
if (strncmp(cgirp, path, strlen(cgirp)) != 0)
return 1; /* not in cgi_dir, go to display_file */
/* compare inodes */
if (sbcgi.st_ino != sbpath.st_ino)
goto nocgi; /* not in cgi_dir, go to display_file */
/* set env variables for CGI
* see
@ -248,13 +249,13 @@ 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;
goto nocgi;
/* check if there is something after cgi file for PATH_INFO */
path_info = strchr(cgifp, '/');
@ -267,10 +268,13 @@ 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;
nocgi:
return 1;
}
ssize_t
@ -394,25 +398,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 +418,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);
}

@ -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 *, ...);

Loading…
Cancel
Save