1
0
mirror of https://github.com/gophernicus/gophernicus.git synced 2024-11-03 04:27:17 -05:00
gophernicus/gophernicus.c

781 lines
20 KiB
C

/*
* Gophernicus - Copyright (c) 2009-2017 Kim Holviala <kim@holviala.com>
* 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"
/*
* Print gopher menu line
*/
void info(state *st, char *str, char type)
{
char buf[BUFSIZE];
char selector[16];
/* Convert string to output charset */
if (st->opt_iconv) sstrniconv(st->out_charset, buf, str);
else sstrlcpy(buf, str);
/* Handle gopher title resources */
strclear(selector);
if (type == TYPE_TITLE) {
sstrlcpy(selector, "TITLE");
type = TYPE_INFO;
}
/* Output info line */
strcut(buf, st->out_width);
printf("%c%s\t%s\t%s" CRLF,
type, buf, selector, DUMMY_HOST);
}
/*
* Print footer
*/
void footer(state *st)
{
char line[BUFSIZE];
char buf[BUFSIZE];
char msg[BUFSIZE];
if (!st->opt_footer) {
#ifndef ENABLE_STRICT_RFC1436
if (st->req_filetype == TYPE_MENU || st->req_filetype == TYPE_QUERY)
#endif
printf("." CRLF);
return;
}
/* Create horizontal line */
strrepeat(line, '_', st->out_width);
/* Create right-aligned footer message */
snprintf(buf, sizeof(buf), FOOTER_FORMAT, st->server_platform);
snprintf(msg, sizeof(msg), "%*s", st->out_width - 1, buf);
/* Menu footer? */
if (st->req_filetype == TYPE_MENU || st->req_filetype == TYPE_QUERY) {
info(st, line, TYPE_INFO);
info(st, msg, TYPE_INFO);
printf("." CRLF);
}
/* Plain text footer */
else {
printf("%s" CRLF, line);
printf("%s" CRLF, msg);
#ifdef ENABLE_STRICT_RFC1436
printf("." CRLF);
#endif
}
}
/*
* Print error message & exit
*/
void die(state *st, char *message, char *description)
{
int en = errno;
static const char error_gif[] = ERROR_GIF;
/* Handle NULL description */
if (description == NULL) description = strerror(en);
/* Log the error */
if (st->opt_syslog) {
syslog(LOG_ERR, "error \"%s\" for request \"%s\" from %s",
description, st->req_selector, st->req_remote_addr);
}
log_combined(st, HTTP_404);
/* Handle menu errors */
if (st->req_filetype == TYPE_MENU || st->req_filetype == TYPE_QUERY) {
printf("3" ERROR_PREFIX "%s\tTITLE\t" DUMMY_HOST CRLF, message);
footer(st);
}
/* Handle image errors */
else if (st->req_filetype == TYPE_GIF || st->req_filetype == TYPE_IMAGE) {
fwrite(error_gif, sizeof(error_gif), 1, stdout);
}
/* Handle HTML errors */
else if (st->req_filetype == TYPE_HTML) {
printf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"
"<HTML>\n<HEAD>\n"
" <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html;charset=iso-8859-1\">\n"
" <TITLE>" ERROR_PREFIX "%1$s</TITLE>\n"
"</HEAD>\n<BODY>\n"
"<STRONG>" ERROR_PREFIX "%1$s</STRONG>\n"
"<PRE>\n", message);
footer(st);
printf("</PRE>\n</BODY>\n</HTML>\n");
}
/* Use plain text error for other filetypes */
else {
printf(ERROR_PREFIX "%s" CRLF, message);
footer(st);
}
/* Quit */
exit(EXIT_FAILURE);
}
/*
* Apache-compatible combined logging
*/
void log_combined(state *st, int status)
{
FILE *fp;
struct tm *ltime;
char timestr[64];
time_t now;
/* Try to open the logfile for appending */
if (!*st->log_file) return;
if ((fp = fopen(st->log_file , "a")) == NULL) return;
/* Format time */
now = time(NULL);
ltime = localtime(&now);
strftime(timestr, sizeof(timestr), HTTP_DATE, ltime);
/* Generate log entry */
fprintf(fp, "%s %s:%i - [%s] \"GET %c%s HTTP/1.0\" %i %li \"%s\" \"" HTTP_USERAGENT "\"\n",
st->req_remote_addr,
st->server_host,
st->server_port,
timestr,
st->req_filetype,
st->req_selector,
status,
(long) st->req_filesize,
st->req_referrer);
fclose(fp);
}
/*
* Convert gopher selector to an absolute path
*/
void selector_to_path(state *st)
{
DIR *dp;
struct dirent *dir;
struct stat file;
#ifdef HAVE_PASSWD
struct passwd *pwd;
char *path = EMPTY;
char *c;
#endif
char buf[BUFSIZE];
int i;
/* Handle selector rewriting */
for (i = 0; i < st->rewrite_count; i++) {
/* Match found? */
if (strstr(st->req_selector, st->rewrite[i].match) == st->req_selector) {
/* Replace match with a new string */
snprintf(buf, sizeof(buf), "%s%s",
st->rewrite[i].replace,
st->req_selector + strlen(st->rewrite[i].match));
if (st->debug) {
syslog(LOG_INFO, "rewriting selector \"%s\" -> \"%s\"",
st->req_selector, buf);
}
sstrlcpy(st->req_selector, buf);
}
}
#ifdef HAVE_PASSWD
/* Virtual userdir (~user -> /home/user/public_gopher)? */
if (*(st->user_dir) && sstrncmp(st->req_selector, "/~") == MATCH) {
/* Parse userdir login name & path */;
sstrlcpy(buf, st->req_selector + 2);
if ((c = strchr(buf, '/'))) {
*c = '\0';
path = c + 1;
}
/* Check user validity */
if ((pwd = getpwnam(buf)) == NULL)
die(st, ERR_NOTFOUND, "User not found");
if (pwd->pw_uid < PASSWD_MIN_UID)
die(st, ERR_NOTFOUND, "User found but UID too low");
/* Generate absolute path to users own gopher root */
snprintf(st->req_realpath, sizeof(st->req_realpath),
"%s/%s/%s", pwd->pw_dir, st->user_dir, path);
/* Check ~public_gopher access rights */
if (stat(st->req_realpath, &file) == ERROR)
die(st, ERR_NOTFOUND, NULL);
if ((file.st_mode & S_IROTH) == 0)
die(st, ERR_ACCESS, "~/public_gopher not world-readable");
if (file.st_uid != pwd->pw_uid)
die(st, ERR_ACCESS, "~/ and ~/public_gopher owned by different users");
/* Userdirs always come from the default vhost */
if (st->opt_vhost)
sstrlcpy(st->server_host, st->server_host_default);
return;
}
#endif
/* Virtual hosting */
if (st->opt_vhost) {
/* Try looking for the selector from the current vhost */
snprintf(st->req_realpath, sizeof(st->req_realpath), "%s/%s%s",
st->server_root, st->server_host, st->req_selector);
if (stat(st->req_realpath, &file) == OK) return;
/* Loop through all vhosts looking for the selector */
if ((dp = opendir(st->server_root)) == NULL) die(st, ERR_NOTFOUND, NULL);
while ((dir = readdir(dp))) {
/* Skip .hidden dirs and . & .. */
if (dir->d_name[0] == '.') continue;
/* Special case - skip lost+found (don't ask) */
if (sstrncmp(dir->d_name, "lost+found") == MATCH) continue;
/* Generate path to the found vhost */
snprintf(st->req_realpath, sizeof(st->req_realpath), "%s/%s%s",
st->server_root, dir->d_name, st->req_selector);
/* Did we find the selector under this vhost? */
if (stat(st->req_realpath, &file) == OK) {
/* Virtual host found - update state & return */
sstrlcpy(st->server_host, dir->d_name);
return;
}
}
closedir(dp);
}
/* Handle normal selectors */
snprintf(st->req_realpath, sizeof(st->req_realpath),
"%s%s", st->server_root, st->req_selector);
}
/*
* Get local IP address
*/
char *get_local_address(void)
{
#ifdef HAVE_IPv4
struct sockaddr_in addr;
socklen_t addrsize = sizeof(addr);
#endif
#ifdef HAVE_IPv6
struct sockaddr_in6 addr6;
socklen_t addr6size = sizeof(addr6);
static char address[INET6_ADDRSTRLEN];
#endif
char *c;
/* Try IPv4 first */
#ifdef HAVE_IPv4
if (getsockname(0, (struct sockaddr *) &addr, &addrsize) == OK) {
c = inet_ntoa(addr.sin_addr);
if (strlen(c) > 0 && *c != '0') return c;
}
#endif
/* IPv4 didn't work - try IPv6 */
#ifdef HAVE_IPv6
if (getsockname(0, (struct sockaddr *) &addr6, &addr6size) == OK) {
if (inet_ntop(AF_INET6, &addr6.sin6_addr, address, sizeof(address))) {
/* Strip ::ffff: IPv4-in-IPv6 prefix */
if (sstrncmp(address, "::ffff:") == MATCH) return (address + 7);
else return address;
}
}
#endif
/* Nothing works... I'm out of ideas */
return UNKNOWN_ADDR;
}
/*
* Get remote peer IP address
*/
char *get_peer_address(void)
{
#ifdef HAVE_IPv4
struct sockaddr_in addr;
socklen_t addrsize = sizeof(addr);
#endif
#ifdef HAVE_IPv6
struct sockaddr_in6 addr6;
socklen_t addr6size = sizeof(addr6);
static char address[INET6_ADDRSTRLEN];
#endif
char *c;
/* Are we a CGI script? */
if ((c = getenv("REMOTE_ADDR"))) return c;
/* if ((c = getenv("REMOTE_HOST"))) return c; */
/* Try IPv4 first */
#ifdef HAVE_IPv4
if (getpeername(0, (struct sockaddr *) &addr, &addrsize) == OK) {
c = inet_ntoa(addr.sin_addr);
if (strlen(c) > 0 && *c != '0') return c;
}
#endif
/* IPv4 didn't work - try IPv6 */
#ifdef HAVE_IPv6
if (getpeername(0, (struct sockaddr *) &addr6, &addr6size) == OK) {
if (inet_ntop(AF_INET6, &addr6.sin6_addr, address, sizeof(address))) {
/* Strip ::ffff: IPv4-in-IPv6 prefix */
if (sstrncmp(address, "::ffff:") == MATCH) return (address + 7);
else return address;
}
}
#endif
/* Nothing works... I'm out of ideas */
return UNKNOWN_ADDR;
}
/*
* Initialize state struct to default/empty values
*/
void init_state(state *st)
{
static const char *filetypes[] = { FILETYPES };
char buf[BUFSIZE];
char *c;
int i;
/* Request */
strclear(st->req_selector);
strclear(st->req_realpath);
strclear(st->req_query_string);
strclear(st->req_search);
strclear(st->req_referrer);
sstrlcpy(st->req_local_addr, get_local_address());
sstrlcpy(st->req_remote_addr, get_peer_address());
/* strclear(st->req_remote_host); */
st->req_filetype = DEFAULT_TYPE;
st->req_protocol = PROTO_GOPHER;
st->req_filesize = 0;
/* Output */
st->out_width = DEFAULT_WIDTH;
st->out_charset = DEFAULT_CHARSET;
/* Settings */
sstrlcpy(st->server_root, DEFAULT_ROOT);
sstrlcpy(st->server_host_default, DEFAULT_HOST);
if ((c = getenv("HOSTNAME")))
sstrlcpy(st->server_host, c);
else if ((gethostname(buf, sizeof(buf))) != ERROR)
sstrlcpy(st->server_host, buf);
st->server_port = DEFAULT_PORT;
st->server_tls_port = DEFAULT_TLS_PORT;
st->default_filetype = DEFAULT_TYPE;
sstrlcpy(st->map_file, DEFAULT_MAP);
sstrlcpy(st->tag_file, DEFAULT_TAG);
sstrlcpy(st->cgi_file, DEFAULT_CGI);
sstrlcpy(st->user_dir, DEFAULT_USERDIR);
strclear(st->log_file);
st->hidden_count = 0;
st->filetype_count = 0;
strclear(st->filter_dir);
st->rewrite_count = 0;
strclear(st->server_description);
strclear(st->server_location);
strclear(st->server_platform);
strclear(st->server_admin);
/* Session */
st->session_timeout = DEFAULT_SESSION_TIMEOUT;
st->session_max_kbytes = DEFAULT_SESSION_MAX_KBYTES;
st->session_max_hits = DEFAULT_SESSION_MAX_HITS;
/* Feature options */
st->opt_vhost = TRUE;
st->opt_parent = TRUE;
st->opt_header = TRUE;
st->opt_footer = TRUE;
st->opt_date = TRUE;
st->opt_syslog = TRUE;
st->opt_magic = TRUE;
st->opt_iconv = TRUE;
st->opt_query = TRUE;
st->opt_caps = TRUE;
st->opt_shm = TRUE;
st->opt_root = TRUE;
st->opt_proxy = TRUE;
st->debug = FALSE;
/* Load default suffix -> filetype mappings */
for (i = 0; filetypes[i]; i += 2) {
if (st->filetype_count < MAX_FILETYPES) {
sstrlcpy(st->filetype[st->filetype_count].suffix, filetypes[i]);
st->filetype[st->filetype_count].type = *filetypes[i + 1];
st->filetype_count++;
}
}
}
/*
* Main
*/
int main(int argc, char *argv[])
{
struct stat file;
state st;
char self[64];
char selector[BUFSIZE];
char buf[BUFSIZE];
char *dest;
char *c;
#ifdef HAVE_SHMEM
struct shmid_ds shm_ds;
shm_state *shm;
int shmid;
#endif
#ifdef ENABLE_HAPROXY1
char remote[BUFSIZE];
char local[BUFSIZE];
int dummy;
#endif
/* Get the name of this binary */
if ((c = strrchr(argv[0], '/'))) sstrlcpy(self, c + 1);
else sstrlcpy(self, argv[0]);
/* Initialize state */
#ifdef HAVE_LOCALES
setlocale(LC_TIME, DATE_LOCALE);
#endif
init_state(&st);
srand(time(NULL) / (getpid() + getppid()));
/* Handle command line arguments */
parse_args(&st, argc, argv);
/* Open syslog() */
if (st.opt_syslog) openlog(self, LOG_PID, LOG_DAEMON);
/* Check if TCP wrappers have something to say about this connection */
#ifdef HAVE_LIBWRAP
if (sstrncmp(st.req_remote_addr, UNKNOWN_ADDR) != MATCH &&
hosts_ctl(self, STRING_UNKNOWN, st.req_remote_addr, STRING_UNKNOWN) == WRAP_DENIED)
die(&st, ERR_ACCESS, "Refused connection");
#endif
/* Make sure the computer is turned on */
#ifdef __HAIKU__
if (is_computer_on() != TRUE)
die(&st, ERR_ACCESS, "Please turn on the computer first");
#endif
/* Refuse to run as root */
#ifdef HAVE_PASSWD
if (st.opt_root && getuid() == 0)
die(&st, ERR_ACCESS, "Cowardly refusing to run as root");
#endif
/* Try to get shared memory */
#ifdef HAVE_SHMEM
if ((shmid = shmget(SHM_KEY, sizeof(shm_state), IPC_CREAT | SHM_MODE)) == ERROR) {
/* Getting memory failed -> delete the old allocation */
shmctl(shmid, IPC_RMID, &shm_ds);
shm = NULL;
}
else {
/* Map shared memory */
if ((shm = (shm_state *) shmat(shmid, (void *) 0, 0)) == (void *) ERROR)
shm = NULL;
/* Initialize mapped shared memory */
if (shm && shm->start_time == 0) {
shm->start_time = time(NULL);
/* Keep server platform & description in shm */
platform(&st);
sstrlcpy(shm->server_platform, st.server_platform);
sstrlcpy(shm->server_description, st.server_description);
}
}
/* For debugging shared memory issues */
if (!st.opt_shm) shm = NULL;
/* Get server platform and description */
if (shm) {
sstrlcpy(st.server_platform, shm->server_platform);
if (!*st.server_description)
sstrlcpy(st.server_description, shm->server_description);
}
else
#endif
platform(&st);
/* Read selector */
get_selector:
if (fgets(selector, sizeof(selector) - 1, stdin) == NULL)
selector[0] = '\0';
/* Remove trailing CRLF */
chomp(selector);
if (st.debug) syslog(LOG_INFO, "client sent us \"%s\"", selector);
/* Handle HAproxy/Stunnel proxy protocol v1 */
#ifdef ENABLE_HAPROXY1
if (sstrncmp(selector, "PROXY TCP") == MATCH && st.opt_proxy) {
if (st.debug) syslog(LOG_INFO, "got proxy protocol header \"%s\"", selector);
sscanf(selector, "PROXY TCP%d %s %s %d %d",
&dummy, remote, local, &dummy, &st.server_port);
/* Strip ::ffff: IPv4-in-IPv6 prefix and override old addresses */
sstrlcpy(st.req_local_addr, local + ((sstrncmp(local, "::ffff:") == MATCH) ? 7 : 0));
sstrlcpy(st.req_remote_addr, remote + ((sstrncmp(remote, "::ffff:") == MATCH) ? 7 : 0));
/* My precious \o/ */
goto get_selector;
}
#endif
/* Handle hURL: redirect page */
if (sstrncmp(selector, "URL:") == MATCH) {
st.req_filetype = TYPE_HTML;
sstrlcpy(st.req_selector, selector);
url_redirect(&st);
return OK;
}
/* Handle gopher+ root requests (UMN gopher client is seriously borken) */
if (sstrncmp(selector, "\t$") == MATCH) {
printf("+-1" CRLF);
printf("+INFO: 1Main menu\t\t%s\t%i" CRLF,
st.server_host,
st.server_port);
printf("+VIEWS:" CRLF " application/gopher+-menu: <512b>" CRLF);
printf("." CRLF);
if (st.debug) syslog(LOG_INFO, "got a request for gopher+ root menu");
return OK;
}
/* Convert HTTP request to gopher (respond using headerless HTTP/0.9) */
if (sstrncmp(selector, "GET ") == MATCH ||
sstrncmp(selector, "POST ") == MATCH ) {
if ((c = strchr(selector, ' '))) sstrlcpy(selector, c + 1);
if ((c = strchr(selector, ' '))) *c = '\0';
st.req_protocol = PROTO_HTTP;
if (st.debug) syslog(LOG_INFO, "got HTTP request for \"%s\"", selector);
}
/* Save default server_host & fetch session data (including new server_host) */
sstrlcpy(st.server_host_default, st.server_host);
#ifdef HAVE_SHMEM
if (shm) get_shm_session(&st, shm);
#endif
/* Parse <tab>search from selector */
if ((c = strchr(selector, '\t'))) {
sstrlcpy(st.req_search, c + 1);
*c = '\0';
}
/* Parse ?query from selector */
if (st.opt_query && (c = strchr(selector, '?'))) {
sstrlcpy(st.req_query_string, c + 1);
*c = '\0';
}
/* Parse ;vhost from selector */
if (st.opt_vhost && (c = strchr(selector, ';'))) {
sstrlcpy(st.server_host, c + 1);
*c = '\0';
}
/* Loop through the selector, fix it & separate query_string */
dest = st.req_selector;
if (selector[0] != '/') *dest++ = '/';
for (c = selector; *c;) {
/* Skip duplicate slashes and /./ */
while (*c == '/' && *(c + 1) == '/') c++;
if (*c == '/' && *(c + 1) == '.' && *(c + 2) == '/') c += 2;
/* Copy valid char */
*dest++ = *c++;
}
*dest = '\0';
/* Main query parameters compatibility with older versions of Gophernicus */
if (*st.req_query_string && !*st.req_search) sstrlcpy(st.req_search, st.req_query_string);
if (!*st.req_query_string && *st.req_search) sstrlcpy(st.req_query_string, st.req_search);
/* Remove encodings from selector */
strndecode(st.req_selector, st.req_selector, sizeof(st.req_selector));
/* Deny requests for Slashdot and /../ hackers */
if (strstr(st.req_selector, "/."))
die(&st, ERR_ACCESS, "Refusing to serve out dotfiles");
/* Handle /server-status requests */
#ifdef HAVE_SHMEM
if (sstrncmp(st.req_selector, SERVER_STATUS) == MATCH) {
if (shm) server_status(&st, shm, shmid);
return OK;
}
#endif
/* Remove possible extra cruft from server_host */
if ((c = strchr(st.server_host, '\t'))) *c = '\0';
/* Guess request filetype so we can die() with style... */
st.req_filetype = gopher_filetype(&st, st.req_selector, FALSE);
/* Convert seletor to path & stat() */
selector_to_path(&st);
if (st.debug) syslog(LOG_INFO, "path to resource is \"%s\"", st.req_realpath);
if (stat(st.req_realpath, &file) == ERROR) {
/* Handle virtual /caps.txt requests */
if (st.opt_caps && sstrncmp(st.req_selector, CAPS_TXT) == MATCH) {
#ifdef HAVE_SHMEM
caps_txt(&st, shm);
#else
caps_txt(&st, NULL);
#endif
return OK;
}
/* Requested file not found - die() */
die(&st, ERR_NOTFOUND, NULL);
}
/* Fetch request filesize from stat() */
st.req_filesize = file.st_size;
/* Everyone must have read access but no write access */
if ((file.st_mode & S_IROTH) == 0)
die(&st, ERR_ACCESS, "File or directory not world-readable");
if ((file.st_mode & S_IWOTH) != 0)
die(&st, ERR_ACCESS, "File or directory world-writeable");
/* If stat said it was a dir then it's a menu */
if ((file.st_mode & S_IFMT) == S_IFDIR) st.req_filetype = TYPE_MENU;
/* Not a dir - let's guess the filetype again... */
else if ((file.st_mode & S_IFMT) == S_IFREG)
st.req_filetype = gopher_filetype(&st, st.req_realpath, st.opt_magic);
/* Menu selectors must end with a slash */
if (st.req_filetype == TYPE_MENU && strlast(st.req_selector) != '/')
sstrlcat(st.req_selector, "/");
/* Change directory to wherever the resource was */
sstrlcpy(buf, st.req_realpath);
if ((file.st_mode & S_IFMT) != S_IFDIR) c = dirname(buf);
else c = buf;
if (chdir(c) == ERROR) die(&st, ERR_ACCESS, NULL);
/* Keep count of hits and data transfer */
#ifdef HAVE_SHMEM
if (shm) {
shm->hits++;
shm->kbytes += st.req_filesize / 1024;
/* Update user session */
update_shm_session(&st, shm);
}
#endif
/* Log the request */
if (st.opt_syslog) {
syslog(LOG_INFO, "request for \"gopher%s://%s:%i/%c%s\" from %s",
(st.server_port == st.server_tls_port ? "s" : ""),
st.server_host,
st.server_port,
st.req_filetype,
st.req_selector,
st.req_remote_addr);
}
/* Check file type & act accordingly */
switch (file.st_mode & S_IFMT) {
case S_IFDIR:
log_combined(&st, HTTP_OK);
gopher_menu(&st);
break;
case S_IFREG:
log_combined(&st, HTTP_OK);
gopher_file(&st);
break;
default:
die(&st, ERR_ACCESS, "Refusing to serve out special files");
}
/* Clean exit */
return OK;
}