#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, const char *chroot_dir, const char *cgi_dir) { 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 (*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"); stop(EXIT_FAILURE, "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); } } #ifdef __OpenBSD__ /* * prevent access to files other than the one in chroot_dir */ if (chrooted) eunveil("/", "r"); else eunveil(chroot_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", NULL); else epledge("stdio rpath", NULL); #endif if (!chrooted) echdir(chroot_dir); /* move to the gemini data directory */ } ssize_t display_file(const char *fname) { FILE *fd = NULL; const char *file_mime; /* * special case : fname empty. The user requested just a dir name */ 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) { 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); _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 *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 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) { /* 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, "error when trying run cgi"); stop(EXIT_FAILURE, "error when trying to execl %s", cgicmd); } void strip_trailing_slash(char *path) { size_t end = strlen(path); if (end == 0) return; end--; 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; }