/* * Gophernicus * * Copyright (c) 2009-2018 Kim Holviala * Copyright (c) 2019 Gophernicus Developers * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "gophernicus.h" /* * Alphabetic folders first sort for sortdir() */ static int foldersort(const void *a, const void *b) { mode_t amode; mode_t bmode; amode = (*(sdirent *) a).mode & S_IFMT; bmode = (*(sdirent *) b).mode & S_IFMT; if (amode == S_IFDIR && bmode != S_IFDIR) return -1; if (amode != S_IFDIR && bmode == S_IFDIR) return 1; return strcmp((*(sdirent *) a).name, (*(sdirent *) b).name); } /* * Date sort for userlist() */ int datesort(const void *a, const void *b) { if (((user_date *)a)->mtime > ((user_date *)b)->mtime) return -1; else return 1; } /* * Scan, stat and sort a directory folders first (scandir replacement) */ static int sortdir(char *path, sdirent *list, int max) { DIR *dp; struct dirent *d; struct stat s; char buf[BUFSIZE]; int i; /* Try to open the dir */ if ((dp = opendir(path)) == NULL) return 0; i = 0; /* Loop through the directory & stat() everything */ while (max--) { if ((d = readdir(dp)) == NULL) break; snprintf(buf, sizeof(buf), "%s/%s", path, d->d_name); if (stat(buf, &s) == ERROR) continue; if (strlen(d->d_name) > sizeof(list[i].name)) continue; sstrlcpy(list[i].name, d->d_name); list[i].mode = s.st_mode; list[i].uid = s.st_uid; list[i].gid = s.st_gid; list[i].size = s.st_size; list[i].mtime = s.st_mtime; i++; } closedir(dp); /* Sort the entries */ if (i > 1) qsort(list, i, sizeof(sdirent), foldersort); /* Return number of entries found */ return i; } /* * Print a list of users with ~/public_gopher */ #ifdef HAVE_PASSWD static void userlist(state *st) { struct passwd *pwd; struct stat dir; char buf[BUFSIZE]; user_date users[MAX_USERS]; struct tm *ltime; char timestr[20]; int width; /* Width of filenames for fancy listing */ width = st->out_width - DATE_WIDTH - 15; /* Loop through all users */ setpwent(); int i = 0; while ((pwd = getpwent())) { /* Skip too small uids */ if (pwd->pw_uid < PASSWD_MIN_UID) continue; /* Look for a world-readable user-owned ~/public_gopher */ snprintf(buf, sizeof(buf), "%s/%s", pwd->pw_dir, st->user_dir); if (stat(buf, &dir) == ERROR) continue; if ((dir.st_mode & S_IROTH) == 0) continue; if (dir.st_uid != pwd->pw_uid) continue; /* Found one */ strcpy(users[i].user, pwd->pw_name); users[i].mtime = dir.st_mtime; i++; } /* Sort by date */ int true_length = 0; while((users[true_length].user[0] != '\0') && (true_length < MAX_USERS)) true_length++; qsort((void*)users, true_length, sizeof(users[0]), datesort); /* Loop over the found users */ for( i = 0; ((i < MAX_USERS) && (users[i].user[0] != '\0')); i++) { /* Format the user string */ snprintf(buf, sizeof(buf), USERDIR_FORMAT); /* Output */ if (st->opt_date) { ltime = localtime(&users[i].mtime); strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); printf("1%-*.*s %s - \t/~%s/\t%s\t%i" CRLF, width, width, buf, timestr, users[i].user, st->server_host, st->server_port); } else { printf("1%.*s\t/~%s/\t%s\t%i" CRLF, st->out_width, buf, users[i].user, st->server_host_default, st->server_port); } } endpwent(); } #endif /* * Print a list of available virtual hosts */ static void vhostlist(state *st) { sdirent dir[MAX_SDIRENT]; struct tm *ltime; char timestr[20]; char buf[BUFSIZE]; int width; int num; int i; /* Scan the root dir for vhost dirs */ num = sortdir(st->server_root, dir, MAX_SDIRENT); if (num < 0) die(st, ERR_NOTFOUND, "WTF?"); /* Width of filenames for fancy listing */ width = st->out_width - DATE_WIDTH - 15; /* Loop through the directory entries */ for (i = 0; i < num; i++) { /* Skip dotfiles */ if (dir[i].name[0] == '.') continue; /* Require FQDN */ if (!strchr(dir[i].name, '.')) continue; /* We only want world-readable directories */ if ((dir[i].mode & S_IROTH) == 0) continue; if ((dir[i].mode & S_IFMT) != S_IFDIR) continue; /* Generate display string for vhost */ snprintf(buf, sizeof(buf), VHOST_FORMAT, dir[i].name); /* Fancy listing */ if (st->opt_date) { ltime = localtime(&dir[i].mtime); strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); printf("1%-*.*s %s - \t/;%s\t%s\t%i" CRLF, width, width, buf, timestr, dir[i].name, dir[i].name, st->server_port); } /* Teh boring version */ else { printf("1%.*s\t/;%s\t%s\t%i" CRLF, st->out_width, buf, dir[i].name, dir[i].name, st->server_port); } } } /* * Return gopher filetype for a file */ char gopher_filetype(state *st, char *file, char magic) { FILE *fp; char buf[BUFSIZE]; char *c; int i; /* If it ends with an slash it's a menu */ if (!*file) return st->default_filetype; if (strlast(file) == '/') return TYPE_MENU; /* Get file suffix */ if ((c = strrchr(file, '.'))) { c++; /* Loop through the filetype array looking for a match*/ for (i = 0; i < st->filetype_count; i++) if (strcasecmp(st->filetype[i].suffix, c) == MATCH) return st->filetype[i].type; } /* Are we allowed to look inside files? */ if (!magic) return st->default_filetype; /* Read data from the file */ if ((fp = fopen(file , "r")) == NULL) return st->default_filetype; i = fread(buf, 1, sizeof(buf) - 1, fp); buf[i] = '\0'; fclose(fp); /* GIF images */ if (sstrncmp(buf, "GIF89a") == MATCH || sstrncmp(buf, "GIF87a") == MATCH) return TYPE_GIF; /* JPEG images */ if (sstrncmp(buf, "\377\330\377") == MATCH) return TYPE_IMAGE; /* PNG images */ if (sstrncmp(buf, "\211PNG") == MATCH) return TYPE_IMAGE; /* mbox */ if (strstr(buf, "\nFrom: ") && strstr(buf, "\nSubject: ")) return TYPE_MIME; /* MIME */ if (strstr(buf, "\nContent-Type: ")) return TYPE_MIME; /* HTML files */ if (buf[0] == '<' && (strstr(buf, "default_filetype; } /* * Handle gophermaps */ static int gophermap(state *st, char *mapfile, int depth) { FILE *fp; struct stat file; char line[BUFSIZE]; #ifdef HAVE_POPEN char command[BUFSIZE]; #endif char *selector; char *name; char *host; char *c; char type; int port; int exe; int return_val = QUIT; /* Prevent include loops */ if (depth > 4) return OK; /* Try to figure out whether the map is executable */ if (stat(mapfile, &file) == OK) { if ((file.st_mode & S_IXOTH)) { #ifdef HAVE_POPEN /* Quote the command in case path has spaces */ snprintf(command, sizeof(command), "'%s'", mapfile); #endif exe = TRUE; } else exe = FALSE; } /* This must be a shell include */ else { #ifdef HAVE_POPEN /* Let's assume the shell command runs as is without quoting */ sstrlcpy(command, mapfile); #endif exe = TRUE; } log_debug("parsing %s gophermap \"%s\"%s", exe ? "executable" : "static", mapfile, exe && !st->opt_exec ? ": forbidden by `-nx'" : ""); /* Try to execute or open the mapfile */ if (exe & st->opt_exec) { #ifdef HAVE_POPEN setenv_cgi(st, mapfile); if ((fp = popen(command, "r")) == NULL) return OK; #else return OK; #endif } else if ((fp = fopen(mapfile, "r")) == NULL) return OK; /* Read lines one by one */ while (fgets(line, sizeof(line) - 1, fp)) { /* Parse type & name */ chomp(line); type = line[0]; name = line + 1; /* Ignore #comments */ if (type == '#') continue; /* Stop handling gophermap? */ if (type == '*') { return_val = OK; goto CLOSE_FP; } if (type == '.') goto CLOSE_FP; /* Print a list of users with public_gopher */ if (type == '~' && st->opt_personal_spaces) { #ifdef HAVE_PASSWD userlist(st); #endif continue; } /* Print a list of available virtual hosts */ if (type == '%') { if (st->opt_vhost) vhostlist(st); continue; } /* Hide files in menus */ if (type == '-') { if (st->hidden_count < MAX_HIDDEN) sstrlcpy(st->hidden[st->hidden_count++], name); continue; } /* Override filetype mappings */ if (type == ':') { add_ftype_mapping(st, name); continue; } /* Include gophermap or shell exec */ if (type == '=') { gophermap(st, name, depth + 1); continue; } /* Title resource */ if (type == TYPE_TITLE) { info(st, name, TYPE_TITLE); continue; } /* Print out non-resources as info text */ if (!strchr(line, '\t')) { info(st, line, TYPE_INFO); continue; } /* Parse selector */ selector = EMPTY; if ((c = strchr(name, '\t'))) { *c = '\0'; selector = c + 1; } if (!*selector) selector = name; /* Parse host */ host = st->server_host; if ((c = strchr(selector, '\t'))) { *c = '\0'; host = c + 1; } /* Parse port */ port = st->server_port; if ((c = strchr(host, '\t'))) { *c = '\0'; port = atoi(c + 1); } /* Handle remote, absolute and hURL gopher resources */ if (sstrncmp(selector, "URL:") == MATCH || selector[0] == '/' || host != st->server_host) { printf("%c%s\t%s\t%s\t%i" CRLF, type, name, selector, host, port); } /* Handle relative resources */ else { printf("%c%s\t%s%s\t%s\t%i" CRLF, type, name, st->req_selector, selector, host, port); /* Automatically hide manually defined selectors */ #ifdef ENABLE_AUTOHIDING if (st->hidden_count < MAX_HIDDEN) sstrlcpy(st->hidden[st->hidden_count++], selector); #endif } } CLOSE_FP: #ifdef HAVE_POPEN if (exe & st->opt_exec) pclose(fp); else #endif fclose(fp); return return_val; } /* * Handle gopher menus */ void gopher_menu(state *st) { FILE *fp; sdirent dir[MAX_SDIRENT]; struct tm *ltime; struct stat file; char buf[BUFSIZE]; char pathname[BUFSIZE]; char displayname[BUFSIZE]; char encodedname[BUFSIZE]; char timestr[20]; char sizestr[20]; char *parent; char *c; char type; int width; int num; int i; int n; /* Check for a gophermap */ snprintf(pathname, sizeof(pathname), "%s/%s", st->req_realpath, st->map_file); if (stat(pathname, &file) == OK && (file.st_mode & S_IFMT) == S_IFREG) { /* Parse gophermap */ if (gophermap(st, pathname, 0) == QUIT) { footer(st); return; } } else { /* Check for a gophertag */ snprintf(pathname, sizeof(pathname), "%s/%s", st->req_realpath, st->tag_file); if (stat(pathname, &file) == OK && (file.st_mode & S_IFMT) == S_IFREG) { /* Read & output gophertag */ if ((fp = fopen(pathname , "r"))) { if (fgets(buf, sizeof(buf), fp) == NULL) strclear(buf); chomp(buf); info(st, buf, TYPE_TITLE); info(st, EMPTY, TYPE_INFO); fclose(fp); } } /* No gophermap or tag found - print default header */ else if (st->opt_header) { /* Use the selector as menu title */ sstrlcpy(displayname, st->req_selector); /* Shorten too long titles */ while (strlen(displayname) > (st->out_width - sizeof(HEADER_FORMAT))) { if ((c = strchr(displayname, '/')) == NULL) break; if (!*++c) break; sstrlcpy(displayname, c); } /* Output menu title */ snprintf(buf, sizeof(buf), HEADER_FORMAT, displayname); info(st, buf, TYPE_TITLE); info(st, EMPTY, TYPE_INFO); } } /* Scan the directory */ num = sortdir(st->req_realpath, dir, MAX_SDIRENT); if (num < 0) die(st, ERR_NOTFOUND, "WTF?"); /* Create link to parent directory */ if (st->opt_parent) { sstrlcpy(buf, st->req_selector); parent = dirname(buf); /* Root has no parent */ if (strcmp(st->req_selector, ROOT) != MATCH) { /* Prevent double-slash */ if (strcmp(parent, ROOT) == MATCH) parent++; /* Print link */ printf("1%-*s\t%s/\t%s\t%i" CRLF, st->opt_date ? (st->out_width - 1) : (int) strlen(PARENT), PARENT, parent, st->server_host, st->server_port); } } /* Width of filenames for fancy listing */ width = st->out_width - DATE_WIDTH - 15; /* Loop through the directory entries */ for (i = 0; i < num; i++) { /* Get full path+name */ snprintf(pathname, sizeof(pathname), "%s/%s", st->req_realpath, dir[i].name); /* Skip dotfiles and non world-readables */ if (dir[i].name[0] == '.') continue; if ((dir[i].mode & S_IROTH) == 0) continue; /* Skip gophermaps and tags (but not dirs) */ if ((dir[i].mode & S_IFMT) != S_IFDIR) { if (strcmp(dir[i].name, st->map_file) == MATCH) continue; if (strcmp(dir[i].name, st->tag_file) == MATCH) continue; } /* Skip files marked for hiding */ for (n = 0; n < st->hidden_count; n++) if (strcmp(dir[i].name, st->hidden[n]) == MATCH) break; if (n < st->hidden_count) continue; /* Cruel hack... */ /* Generate display name with correct output charset */ if (st->opt_iconv) sstrniconv(st->out_charset, displayname, dir[i].name); else sstrlcpy(displayname, dir[i].name); /* #OCT-encode filename */ strnencode(encodedname, dir[i].name, sizeof(encodedname)); /* Handle inline .gophermap */ if (strstr(displayname, st->map_file) > displayname) { gophermap(st, pathname, 0); continue; } /* Handle directories */ if ((dir[i].mode & S_IFMT) == S_IFDIR) { /* Check for a gophertag */ snprintf(buf, sizeof(buf), "%s/%s", pathname, st->tag_file); if (stat(buf, &file) == OK && (file.st_mode & S_IFMT) == S_IFREG) { /* Use the gophertag as displayname */ if ((fp = fopen(buf , "r"))) { if (fgets(buf, sizeof(buf), fp) == NULL) strclear(buf); chomp(buf); fclose(fp); /* Skip empty gophertags */ if (*buf) { /* Convert to output charset */ if (st->opt_iconv) sstrniconv(st->out_charset, displayname, buf); else sstrlcpy(displayname, buf); } } } /* Dir listing with dates */ if (st->opt_date) { ltime = localtime(&dir[i].mtime); strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); /* Hack to get around UTF-8 byte != char */ n = width - strcut(displayname, width); strrepeat(buf, ' ', n); printf("1%s%s %s --------\t%s%s/\t%s\t%i" CRLF, displayname, buf, timestr, st->req_selector, encodedname, st->server_host, st->server_port); } /* Regular dir listing */ else { strcut(displayname, st->out_width); printf("1%s\t%s%s/\t%s\t%i" CRLF, displayname, st->req_selector, encodedname, st->server_host, st->server_port); } continue; } /* Skip special files (sockets, fifos etc) */ if ((dir[i].mode & S_IFMT) != S_IFREG) continue; /* Get file type */ type = gopher_filetype(st, pathname, st->opt_magic); /* File listing with dates & sizes */ if (st->opt_date) { ltime = localtime(&dir[i].mtime); strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); strfsize(sizestr, dir[i].size, sizeof(sizestr)); /* Hack to get around UTF-8 byte != char */ n = width - strcut(displayname, width); strrepeat(buf, ' ', n); printf("%c%s%s %s %s\t%s%s\t%s\t%i" CRLF, type, displayname, buf, timestr, sizestr, st->req_selector, encodedname, st->server_host, st->server_port); } /* Regular file listing */ else { strcut(displayname, st->out_width); printf("%c%s\t%s%s\t%s\t%i" CRLF, type, displayname, st->req_selector, encodedname, st->server_host, st->server_port); } } /* Print footer */ footer(st); }