#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mimes.h" #include "opts.h" #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 status(const int code, const char *fmt, ...) { 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 uridecode(char *uri) { int n = 0; char c = '\0'; long l = 0; char *pos = NULL; if ((pos = strchr(uri, '%')) == NULL) return n; while ((pos = strchr(pos, '%')) != NULL) { if (strlen(pos) < 3) return n; char hex[3] = {'\0'}; for (size_t i = 0; i < 2; i++) hex[i] = tolower(pos[i + 1]); errno = 0; l = strtol(hex, 0, 16); if (errno == ERANGE && (l == LONG_MAX || l == LONG_MIN)) continue; /* conversion failed */ c = (char)l; pos[0] = c; /* rewind of two char to remove %hex */ memmove(pos + 1, pos + 3, strlen(pos + 3) + 1); /* +1 for \0 */ n++; pos++; /* avoid infinite loop */ } return n; } void drop_privileges(const char *user) { 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 */ /* 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) { /* chroot worked? */ 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); } /* 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 */ eunveil(capsule_dir, "r"); /* 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 (*cgi_dir) epledge("stdio rpath exec proc", NULL); else epledge("stdio rpath", NULL); #endif echdir(capsule_dir); /* move to the gemini data directory */ } ssize_t display_file(const char *path) { FILE *fd = NULL; const char *file_mime; /* * special case : path ends with "/". The user requested a dir */ if ((path[strlen(path)-1] == '/') && (doautoidx)) { /* no index.gmi, so display autoindex if enabled */ _datasiz += autoindex(path); return _datasiz; } /* open the file requested */ if ((fd = fopen(path, "r")) != NULL) { file_mime = get_file_mime(path, default_mime); if (strcmp(file_mime, "text/gemini") == 0) status(20, "%s; %s", file_mime, lang); else status(20, "%s", file_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"); } return _datasiz; } int do_cgi(const char *rel_cgi_dir, const char *path, const char *hostname, const char *query) { 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; /* get beginning of path */ /* path_dir is initialized so there is an \0 at the end */ memcpy(path_dir, path, strlen(rel_cgi_dir)); if (stat(rel_cgi_dir, &sbcgi) + stat(path_dir, &sbpath) != 0) goto nocgi; /* 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 * 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 + strlen(rel_cgi_dir) + 1 (skip '/') */ /* cgi file to execute */ esnprintf(cgifp, sizeof(cgifp), "%s", path + strlen(rel_cgi_dir) + 1); if (!(*cgifp)) /* problem with cgi file, abort */ goto nocgi; /* 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(rel_cgi_dir); cgi(cgifp); return 0; nocgi: return 1; } ssize_t autoindex(const char *path) { /* display list of files in path + a link to parent (..) */ int n = 0; struct dirent **namelist; /* this must be freed at last */ size_t bs = 0; /* use alphasort to always have the same order on every system */ if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) { status(50, "Can't scan %s", path); } else { status(20, "text/gemini"); 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) || (strcmp(namelist[j]->d_name, "..") == 0)) { continue; } /* add "/" at the end of a directory path */ 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) { int fildes[2] = {0}; int retcode = 0; pid_t pid = 0; FILE *output = NULL; if (pipe(fildes) != 0) goto cgierr; if ((pid = fork()) < 0) goto cgierr; if (pid > 0) { /* parent */ close(fildes[1]); /* make sure entry is closed to get EOF */ if ((output = fdopen(fildes[0], "r")) == NULL) goto cgierr; _datasiz += print_file(output); close(fildes[0]); fclose(output); waitpid(pid, &retcode, 0); stop(EXIT_SUCCESS, "cgi ran with exit code %d", status); } else { /* child */ /* set pipe output equal to stdout & stderr */ dup2(fildes[1], STDOUT_FILENO); close(fildes[1]); /* no longer required */ execl(cgicmd, cgicmd, NULL); } cgierr: /* if execl is ok, this will never be reached */ close(fildes[0]); close(fildes[1]); status(42, "error when trying run cgi"); stop(EXIT_FAILURE, "error when trying to execl %s", cgicmd); } char * read_request(char *request) { /* 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'; /* save request for logs */ esnprintf(_request, sizeof(_request), "%s", request); return request; } void remove_double_dot(char *request) { char *pos = NULL; /* remove all "/.." for safety reasons */ while ((pos = strstr(request, "/..")) != NULL) memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */ } void check_path(char *path, size_t pathsiz) { 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] != '/') { esnprintf(tmp, sizeof(tmp), "/%s/", path); status(31, "%s", tmp); stop(EXIT_SUCCESS, NULL); } /* check if DEFAULT_INDEX exists in directory */ esnprintf(tmp, sizeof(tmp), "%s/%s", path, DEFAULT_INDEX); if (stat(tmp, &sb) == 0) esnprintf(path, pathsiz, "%s", tmp); } } void split_request(const char *request, char *hostname, char *path, char *query) { size_t nmatch = SE_MAX; /* 3 "()" + 1 for whole match */ char buf[BUFSIZ] = {'\0'}; /* to handle error messages */ int ret = 0; regex_t greg; /* compiled gemini regex */ regmatch_t match[SE_MAX]; /* matches founds */ ret = regcomp(&greg, _gemini_regex, REG_EXTENDED); if (ret != 0) { regerror(ret, &greg, buf, sizeof(buf)); regfree(&greg); status(50, "Internal server error"); stop(EXIT_FAILURE, "%s", buf); } ret = regexec(&greg, request, nmatch, match, 0); if (ret != 0) { regerror(ret, &greg, buf, sizeof(buf)); regfree(&greg); status(59, "Malformed request"); stop(EXIT_FAILURE, "Malformed request, error:%s", buf); } /* one may want to check the return of getsubexp * and change memcpy to strlcpy * to make sure we didn't try to copy too long * and that string isn't trunkated. * It is unlikely to happen since dest string are as long as request */ getsubexp(request, match[1], hostname); getsubexp(request, match[2], path); getsubexp(request, match[3], query); regfree(&greg); }