src/nntpcache.c
/* [<][>][^][v][top][bottom][index][help] */
DEFINITIONS
This source file includes following functions.
- bigToStr
- settaskinfo
- task_info_init
- task_info_new
- task_info_free
- sigterm
- sigint
- sigsegv
- sigusr1
- sigusr2
- sighup
- sigalrm
- set_client_sigset
- check_child
- sigchld
- sigpipe
- ncExit
- make_vm_proc
- retire_vm_proc
- load_config
- decomment
- load_groups
- set_cfg_shm
- load_servers
- findScfg
- perform_chroot
- usage
- drop_priv
- drop_idle_servers
- emit_banner
- relay_unknown
- client_cmd
- client_cmd_loop
- client_handler
- createPort
- master_loop
- detach
- main
- calloc_dummy
/* $Id: nntpcache.c,v 1.19 2002/04/04 11:09:27 proff Exp $
* $Copyright$
*/
#include "nglobal.h"
#include "network.h"
#include "dbz.h"
#include "mmalloc.h"
#include "acc.h"
#include "article.h"
#include "authinfo.h"
#include "authinfo_radius.h"
#include "build_history.h"
#include "date.h"
#include "debug.h"
#include "expire.h"
#include "group.h"
#include "http.h"
#include "ihave.h"
#include "ipc.h"
#include "mmap.h"
#include "newgroups.h"
#include "newnews.h"
#include "next.h"
#include "nocem.h"
#include "post.h"
#include "xover.h"
#include "xpath.h"
#include "nntpcache.h"
extern char *optarg;
EXPORT bool volatile HoldForks = FALSE;
EXPORT bool volatile HoldForksClient = FALSE;
EXPORT bool volatile HoldForksHttp = FALSE;
EXPORT bool volatile HoldForksUpdate = FALSE;
EXPORT bool volatile HoldForksNocem = FALSE;
EXPORT bool SwapWithChild = FALSE;
EXPORT struct nnconf *con = &nnconf;
EXPORT struct strStack *slaveClient;
EXPORT bool f_cleanSlate = TRUE;
EXPORT time_t ClientTimeStarted;
EXPORT struct newsgroup *CurrentGroupNode = {0};
EXPORT char CurrentDir[MAX_PATH];
EXPORT int CurrentGroupArtNum;
EXPORT int CurrentGroupArtRead;
EXPORT bool GroupNextNoCache = FALSE; /* set by post.c to force a one-shot cache miss on the next GROUP command */
EXPORT bool CurrentGroupXoverIsFilt;
EXPORT bool CurrentGroupNocem;
EXPORT struct authent *CurrentGroupAuth;
EXPORT struct authent *ConnectAuth;
EXPORT struct strList *overviewFmt;
EXPORT n_u32 overviewFmt_hash;
EXPORT struct strList *overviewFmtBozo;
EXPORT n_u32 overviewFmtDef_hash;
EXPORT big_t ClientBytes;
EXPORT char ClientHost[128 + 1 + MAX_HOST];
EXPORT char ClientHostNormal[MAX_HOST];
EXPORT char ClientHostLocal[MAX_HOST];
EXPORT char ClientHostRFC931[128 + 1 + MAX_HOST];
EXPORT char ClientHostLocalRFC931[128 + 1 + MAX_HOST];
EXPORT char ClientHostAddr[MAX_HOST];
EXPORT char ClientHostAddrRFC931[128 + 1 + MAX_HOST];
EXPORT struct sockaddr_in ClientRemoteAddr;
EXPORT char *RemoteHosts[] =
{
ClientHost, ClientHostNormal, ClientHostLocal, ClientHostRFC931, ClientHostLocalRFC931,
ClientHostAddr, ClientHostAddrRFC931, NULL
};
EXPORT char Host[MAX_HOST];
EXPORT bool ModeReader = FALSE; /* ModeReader sent */
EXPORT struct server_cfg *ServerList = NULL;
EXPORT struct group_cfg *GroupList = NULL;
EXPORT struct server_cfg *CurrentGroupScfg;
EXPORT struct server_cfg *CurrentIDScfg;
EXPORT enum auth_state AuthState = none;
EXPORT bool MakeHistory = FALSE;
EXPORT void *Mbase;
EXPORT bool mmapAnon = FALSE;
static int NNTPportFD = -1;
static int HTTPportFD = -1;
static char PidFile[MAX_PATH] = "";
EXPORT int Master_fd = -1;
EXPORT int Watch_fd = -1;
EXPORT int Debug_fd = 2;
EXPORT fd_set r_set; /* master client fd list */
static int volatile high_fd; /* highest fd in r_set */
static int ncUID;
static int ncGID;
static struct sigaction myaction;
static struct task_info dummy_task; /* this is for oneshots */
EXPORT struct task_info *Task = &dummy_task;
EXPORT struct task_info *TaskList;
static int task_max = FD_HIGH+40;
EXPORT struct command *Command;
EXPORT struct cache_stats *CS;
EXPORT char *Argv0 = "/usr/local/sbin/nntpcached";
EXPORT char *Version = VERSION;
EXPORT bool Detached = FALSE;
EXPORT struct command commands[] =
{
{"ARTICLE", c_article, "[<msgid> | artno]"},
{"AUTHINFO", c_authinfo, "USER username|PASS password"},
{"BODY", c_body, "[<msgid> | artno]"},
{"DATE", c_date, ""},
{"GROUP", c_group, "newsgroup"},
{"HEAD", c_head, "[<msgid> | artno]"},
{"HELP", c_help, ""},
{"IHAVE", c_ihave, "<msgid>"},
{"LAST", c_last, ""},
{"LIST", c_list, "[ACTIVE | ACTIVE.TIMES | NEWSGROUPS | SUBSCRIPTIONS | OVERVIEW.FMT] [pattern]"},
{"LISTGROUP", c_listgroup, "[newsgroup]"},
{"MODE", c_mode, "[READER | QUERY]"},
{"NEWGROUPS", c_newgroups, "yymmdd hhmmss [GMT] [distributions]"},
{"NEWNEWS", c_newnews, "newsgroups yymmdd hhmmss [GMT] [distributions]"},
{"NEXT", c_next, ""},
{"NOOP", c_noop, ""},
{"POST", c_post, ""},
{"QUIT", c_quit, ""},
{"SLAVE", c_slave, ""},
{"STAT", c_stat, "[<msgid> | artno}"},
{"XGTITLE", c_xgtitle, "[pattern]"},
{"XHDR", c_xhdr, "header [<msgid> | range]"},
{"XOVER", c_xover, "[<msgid> | range]"},
{"XPATH", c_xpath, "[<msgid | artno]"},
{NULL, c_none}
};
EXPORT char *task_desc[] = /* keep in-sync with enum task_state! */
{
"none",
"master",
"client",
"update",
"expire",
"nocem",
"oneshot",
"http",
"watch",
NULL /* nc_last */
};
/* bit length resiliant binary to decimal converter */
EXPORT char *bigToStr(big_t big)
/* [<][>][^][v][top][bottom][index][help] */
{
int n;
bool neg;
char *p;
#define RING_NUM 64 /* > max number of bigToStr's ever used as concurrent function arguments */
static char *ring[RING_NUM];
static int ring_idx;
char buf[128];
buf[sizeof(buf)-1] = '\0';
n=sizeof(buf)-2;
if (big<0)
{
big *=-1;
neg = TRUE;
}
else
neg = FALSE;
do
{
buf[n] = big%10 + '0';
big/=10;
} while (--n > 0 && big >=1);
if (neg)
buf[n--] = neg;
if ((p=ring[ring_idx]))
free(p);
p = ring[ring_idx] = Sstrdup(&buf[n+1]);
if (++ring_idx >= RING_NUM)
ring_idx = 0;
return p;
}
EXPORT void settaskinfo (char *fmt, ...)
/* [<][>][^][v][top][bottom][index][help] */
{
va_list ap;
char buf[MAX_LINE];
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
setproctitle("%s", buf);
strncpy(Task->ti_status_line, buf, sizeof(Task->ti_status_line)-1);
Task->ti_status_line[sizeof(Task->ti_status_line)-1] = '\0';
va_end(ap);
}
static void task_info_init ()
/* [<][>][^][v][top][bottom][index][help] */
{
TaskList = XMcalloc(sizeof(struct task_info), task_max);
return;
}
/*
* name MUST be in the text segment or permanetly in the shared data segment.
*/
static struct task_info *task_info_new (enum task_state state, char *name)
/* [<][>][^][v][top][bottom][index][help] */
{
int n;
struct task_info *t;
for (n=0; n<task_max; n++)
if (TaskList[n].ti_state == nc_none)
goto found;
loge (("task_info_new() no more tasks (max = %d)", task_max));
return NULL;
found:
t = &TaskList[n];
memset(t, 0, sizeof *t);
t->ti_started = time(NULL);
t->ti_state = state;
t->ti_pid = getpid();
t->ti_name = name;
t->ti_idx = n;
Stats->task_stats[state].invocations++;
if (Stats->task_high<n)
Stats->task_high = n;
if (state == nc_client)
Stats->clientsActive++;
return t;
}
static void task_info_free (int n)
/* [<][>][^][v][top][bottom][index][help] */
{
struct task_info *p = &TaskList[n];
if (p->ti_state == nc_none)
return;
if (p->ti_state == nc_client)
Stats->clientsActive--;
if (Stats->task_high<n)
{
for (n=Stats->task_high;n>=0 && TaskList[n].ti_state == nc_none; n--) {}
Stats->task_high=n;
}
p->ti_state = nc_none;
}
static RETSIGTYPE sigterm (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
signal (SIGTERM, sigterm);
if (Task->ti_state == nc_master)
{
errno = 0;
logw (("caught SIGTERM: syncing database, syncing disks, exiting"));
}
retire_vm_proc (0);
}
static RETSIGTYPE sigint (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
static struct nnconf *nn_orig, nn_new;
signal (SIGINT, sigint);
if (nn_orig)
{
log(("Caught SIGINT - logging set normal"));
con = nn_orig;
nn_orig = NULL;
} else
{
nn_orig = con;
nn_new = *con;
con = &nn_new;
con->logFromClient = TRUE;
con->logToClient = TRUE;
con->logFromServer = TRUE;
con->logToServer = TRUE;
con->logDebug = TRUE;
con->logInfo = TRUE;
con->logWarnings = TRUE;
con->logErrors = TRUE;
con->logListMerge = TRUE;
con->logListMergeCorrelation = TRUE;
con->logInn = TRUE;
log(("Caught SIGINT - full debug logging set"));
}
}
static RETSIGTYPE sigsegv (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
signal (SIGSEGV, SIG_DFL);
loge (("page error"));
Exit (1);
}
static RETSIGTYPE sigusr1 (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
signal (SIGUSR1, SIG_IGN);
updateDaemon (TRUE);
}
static RETSIGTYPE sigusr2 (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
signal (SIGUSR2, SIG_IGN);
expire (TRUE);
}
static int sig_hup = FALSE;
static RETSIGTYPE sighup (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
sig_hup = TRUE;
signal (SIGHUP, sighup);
}
static RETSIGTYPE sigalrm (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
emitf ("%d Timeout after %s, closing connection.\r\n", NNTP_TEMPERR_VAL, nnitod(con->idleTimeout));
log (("timeout after %s", nnitod(con->idleTimeout)));
loginn (("%s timeout", ClientHostNormal));
retire_vm_proc (0);
}
static void set_client_sigset ()
/* [<][>][^][v][top][bottom][index][help] */
{
sigemptyset(&myaction.sa_mask);
sigaddset(&myaction.sa_mask, SIGALRM);
sigaddset(&myaction.sa_mask, SIGTERM);
sigaddset(&myaction.sa_mask, SIGPIPE);
myaction.sa_flags = 0;
myaction.sa_handler = sigalrm;
sigaction(SIGALRM, &myaction, NULL);
}
static void check_child ()
/* [<][>][^][v][top][bottom][index][help] */
{
int n;
#ifdef HAVE_WAIT3
int pid = wait3 (NULL, WNOHANG, NULL);
#else
#ifdef HAVE_WAITPID
int pid = waitpid ((pid_t) - 1, NULL, WNOHANG);
#else
#error no wait3 or waitpid for this system
#endif
#endif
if (pid<1)
return;
for (n=0; n<=Stats->task_high; n++)
if (TaskList[n].ti_state != nc_none && TaskList[n].ti_pid == pid)
goto found;
loge (("check_child() returned unknow pid (%d)", pid));
return;
found:
task_info_free(n);
if (pid == UpdateDaemonPid)
{
UpdateDaemonPid = 0;
f_cleanSlate = FALSE;
signal (SIGUSR1, sigusr1);
} else if (pid == ExpireDaemonPid)
{
ExpireDaemonPid = 0;
signal (SIGUSR2, sigusr2);
} else if (pid == NocemDaemonPid)
{
NocemDaemonPid = 0;
}
statsUpdateMaster();
}
static RETSIGTYPE sigchld (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
check_child ();
signal (SIGCHLD, sigchld);
}
static RETSIGTYPE sigpipe (int sig)
/* [<][>][^][v][top][bottom][index][help] */
{
signal (SIGPIPE, SIG_IGN); /* syslog can cause a SIGPIPE ! */
logd (("client disconnected unexpectedly"));
retire_vm_proc (0);
}
EXPORT void ncExit (int code)
/* [<][>][^][v][top][bottom][index][help] */
{
sigset_t set;
struct sigaction action;
/* ignore anyone interrupting us */
alarm(0);
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
action.sa_handler = SIG_IGN;
sigaction(SIGTERM, &action, NULL);
sigaction(SIGPIPE, &action, NULL);
if (code != 0)
debugSelf();
if (Task->ti_state == nc_master)
{
chdir(con->cacheDir);
writeMmapBase();
dbzsync ();
dbmclose ();
if (Stats)
saveStats (con->statsFile);
unlink (PidFile);
sync ();
} else
{
if (Task->ti_state == nc_client)
{
struct tms buffer;
n_u32 u, s;
if (CurrentGroupArtRead && CurrentGroupScfg && CurrentGroupScfg->group)
{
loginn (("%s group %s %d", ClientHostNormal,
CurrentGroupScfg->group, CurrentGroupArtRead));
}
loginn (("%s exit articles %d groups %d bytes %s", ClientHostNormal,
ArtRead, GroupsEntered, bigToStr(ClientBytes)));
if (PostsReceived>0 || PostsRejected>0)
loginn (("%s posts received %d rejected %d", ClientHostNormal, PostsReceived, PostsRejected));
times(&buffer);
u = buffer.tms_utime;
s = buffer.tms_stime;
loginn (("%s times user %.2f system %.2f elapsed %d.00",
ClientHostNormal,
(double) u/ CLK_TCK,
(double) s/ CLK_TCK,
(int)(time(NULL) - ClientTimeStarted)));
}
}
if (Master_fd >= 0)
close (Master_fd);
if (Debug_fd >= 0)
close (Debug_fd);
if (Watch_fd >= 0)
close (Watch_fd);
#ifdef MMALLOC
if (Mbase)
mmalloc_detach (Mbase);
#endif
/* restore default SIGALRM behavior (ie, crash us out) */
/* sigemptyset(&action.sa_mask); already done above */
/* sigaddset(&action.sa_mask, SIGALRM); can't remember why I did this in this 1st place */
/* action.sa_flags = 0; already done above */
action.sa_handler = SIG_DFL;
sigaction(SIGALRM, &action, NULL);
/* unblock, since we might be in a SIGALRM handler right now! */
sigemptyset(&set);
sigaddset(&set, SIGALRM);
sigprocmask (SIG_UNBLOCK, &set, NULL);
alarm(60);
flush (); /* better finish this in 60 seconds, toots. */
if (code == 0 || code == 2)
{
log (("clean shutdown"));
closelog ();
exit (code);
}
log (("clean shutdown with error %d. dumping core for debug analysis", code));
chdir(con->cacheDir);
abort ();
}
EXPORT int make_vm_proc (enum task_state state, int clientfd, char *name)
/* [<][>][^][v][top][bottom][index][help] */
{
int i[2];
int pid;
struct task_info *task;
logd (("starting %s task", name));
if (socketpair (AF_UNIX, SOCK_STREAM, 0, i) == -1)
{
loge (("socketpair() failed"));
return -1;
}
task = task_info_new(state, name);
pid = fork ();
if (pid == -1)
{
loge (("couldn't fork()"));
task_info_free(task->ti_idx); /* -an */
if (clientfd>=0)
close (clientfd);
close (i[1]);
close (i[0]);
return -1;
}
if (pid == 0)
while (HoldForks) {}
if (SwapWithChild? pid != 0 : pid == 0)
{
int n;
int yes = 1;
struct server_cfg *scfg;
char buf[MAX_SYSLOG];
char *id;
int ni = 0;
Task = task;
Task->ti_pid = getpid();
switch (state)
{
case nc_master: ni = con->niceMaster; break;
case nc_client: ni = con->niceClient; break;
case nc_update: ni = con->niceUpdate; break;
case nc_expire: ni = con->niceExpire; break;
case nc_nocem: ni = con->niceNoCem; break;
case nc_http: ni = con->niceHTTP; break;
default:
break;
}
if (ni>0)
{
#ifdef HAVE_SETPRIORITY
ni += getpriority(PRIO_PROCESS, 0);
setpriority(PRIO_PROCESS, 0, ni);
#endif
}
Master_fd = i[1];
settaskinfo("starting %s task", name);
dbzcancel ();
dbmclose ();
signal (SIGCHLD, SIG_DFL);
signal (SIGHUP, SIG_DFL);
sigemptyset(&myaction.sa_mask);
sigaddset(&myaction.sa_mask, SIGTERM);
sigaddset(&myaction.sa_mask, SIGALRM);
if (clientfd>=0)
{
sigaddset(&myaction.sa_mask, SIGPIPE);
myaction.sa_flags = 0;
myaction.sa_handler = sigpipe;
sigaction(SIGPIPE, &myaction, NULL);
}
closelog ();
for (scfg = ServerList; scfg; scfg=scfg->next)
if (scfg->fd >= 0 &&
scfg->fd != clientfd)
{
close (scfg->fd);
scfg->fd=-1;
}
if (clientfd >= 0)
{
#ifdef SO_SNDBUF
setsockopt (clientfd, SOL_SOCKET, SO_SNDBUF, (char *)&con->outputBufferSize, sizeof con->outputBufferSize);
#endif
#ifdef SO_KEEPALIVE
if (setsockopt (clientfd, SOL_SOCKET, SO_KEEPALIVE, (char*) &yes, sizeof yes))
logd(("keepalive setsockopt failed for %s", name));
#endif
clientin = fdopen (clientfd, "r");
clientout = fdopen (clientfd, "w");
#ifdef CRASHES_UNDER_LINUX
setvbuf(clientout, io_buf, _IOFBF, IO_BUF_LEN);
#endif
}
close (i[0]);
/* close sockets to siblings */
for (n = 0; n <= high_fd; n++)
{
if (FD_ISSET (n, &r_set))
{
close (n);
FD_CLR(n, &r_set);
}
}
FD_SET(Master_fd, &r_set);
sprintf (buf, "nntpcache-%.80s", name);
id = Sstrdup(buf);
openlog (id, LOG_PID|LOG_NDELAY, LOG_NEWS);
log (("%s task awakening", name));
ClientTimeStarted = time (NULL);
ClientBytes = 0;
}
else /* parent */
{
if (clientfd >= 0)
close (clientfd);
close (i[1]);
FD_SET (i[0], &r_set);
if (i[0] > high_fd)
high_fd = i[0];
}
return pid;
}
EXPORT void retire_vm_proc (int err)
/* [<][>][^][v][top][bottom][index][help] */
{
struct tms tms;
struct task_stats *ts;
char *name;
if (Task)
{
if (Stats && Task->ti_state != nc_master)
{
times(&tms);
ts = &Stats->task_stats[Task->ti_state];
ts->cpu_user += tms.tms_utime + tms.tms_cutime;
ts->cpu_system += tms.tms_stime + tms.tms_cstime;
ts->elapsed += time(NULL) - Task->ti_started;
}
name = task_desc[Task->ti_state];
}
else
name = "unspecified";
log (("%s task retiring", name));
Exit (err);
}
static bool load_config (char *file)
/* [<][>][^][v][top][bottom][index][help] */
{
FILE *fp;
char *msg;
if ((fp = fopen (file, "r")) == NULL)
{
loge (("couldn't load config %s", file));
return FALSE;
}
msg = confused (fp, "", nnconf_idx);
fclose (fp);
if (msg)
{
logen (("error in config file %s: %s", file, msg));
return FALSE;
}
return TRUE;
}
static int decomment(char *buf, int comment_depth)
/* [<][>][^][v][top][bottom][index][help] */
{
char *p;
for (p = buf; *p; p++)
{
if (*p == '/' && p[1] == '*')
{
comment_depth++;
p++;
continue;
}
if (comment_depth && *p == '*' && p[1] == '/')
{
comment_depth--;
p++;
continue;
}
if (!comment_depth)
*buf++ = *p;
}
*buf = *p;
return comment_depth;
}
static bool load_groups(char *file, FILE *fp)
/* [<][>][^][v][top][bottom][index][help] */
{
char buf[MAX_LINE];
int n;
struct group_cfg *list=NULL;
int comment_depth=0;
for (n = 0; fgets(buf, sizeof(buf), fp); ++n)
{
char host[MAX_HOST], group_pat[MAX_HOST];
char *p;
comment_depth = decomment(buf, comment_depth);
if (!buf[0] || buf[0] == '\n' || buf[0] == '#')
continue;
if (sscanf(buf, "%127s %127s", group_pat, host) != 2)
{
loge (("invalid config line %s:%d: %s", file, n, buf));
continue;
}
for (p=strtok(group_pat, ","); p; (p=strtok(NULL, ",")))
{
if (!list)
{
list = Scalloc (1, sizeof *list);
list->head = list;
list->next = NULL;
} else
{
struct group_cfg *head = list->head;
list->next = Scalloc (1, sizeof *list);
list = list->next;
list->next = NULL;
list->head = head;
}
list->server_cfg = findScfg(host);
list->group_pat = Sstrdup (group_pat);
}
}
if (ferror(fp))
{
loge (("error reading groups config from %s", file));
Exit(1);
}
if (!list)
{
loge (("group file %s contains no group/server tuples!", file));
return FALSE;
}
GroupList = list->head;
return TRUE;
}
static void set_cfg_shm()
/* [<][>][^][v][top][bottom][index][help] */
{
struct server_cfg *l;
for (l=ServerList; l; l=l->next)
{
if (!l->share)
l->share = XMcalloc(1, sizeof *l->share);
}
}
/*
* XXX this code needs to be put into the general form
*/
static bool load_servers(char *file)
/* [<][>][^][v][top][bottom][index][help] */
{
FILE *fp;
char buf[MAX_LINE];
int n;
struct server_cfg *list=NULL;
int comment_depth=0;
if ((fp = fopen(file, "r")) == NULL) {
loge(("couldn't load servers file %s", file));
return FALSE;
}
for (n = 0; fgets(buf, sizeof(buf), fp); ++n)
{
char host[MAX_HOST], us[MAX_HOST], active_timeoutS[32], active_times_timeoutS[32], newsgroups_timeoutS[32], group_timeoutS[32], xover_timeoutS[32], article_timeoutS[32], *username, *password, *hostname;
int active_timeout, active_times_timeout, newsgroups_timeout, group_timeout, xover_timeout, article_timeout;
if (!buf[0] || buf[0] == '\n')
continue;
comment_depth = decomment(buf, comment_depth);
if (!buf[0] || buf[0] == '#' || buf[0] == '\n')
continue;
strStripEOL(buf);
if (!buf[0])
continue;
if (strCaseEq(buf, "%BeginGroups"))
break;
if (sscanf(buf, "%127s %127s %31s %31s %31s %31s %31s %31[^\t\r\n ]s", host, us, active_timeoutS, active_times_timeoutS, newsgroups_timeoutS, group_timeoutS, xover_timeoutS, article_timeoutS) != 8)
{
loge (("invalid config line %s:%d: %s", file, n, buf));
continue;
}
if ((active_timeout = nndtoi (active_timeoutS)) < 0)
{
loge (("invalid active file timeout %s:%d: %s", file, n, buf));
continue;
}
if ((active_times_timeout = nndtoi (active_times_timeoutS)) < 0)
{
loge (("invalid active.times file timeout %s:%d: %s", file, n, buf));
continue;
}
if ((newsgroups_timeout = nndtoi (newsgroups_timeoutS)) < 0)
{
loge (("invalid newsgroups timeout %s:%d: %s", file, n, buf));
continue;
}
if ((group_timeout = nndtoi (group_timeoutS)) < 0)
{
loge (("invalid group timeout %s:%d: %s", file, n, buf));
continue;
}
if ((xover_timeout = nndtoi (xover_timeoutS)) < 0)
{
loge (("invalid xover timeout %s:%d: %s", file, n, buf));
continue;
}
if ((article_timeout = nndtoi (article_timeoutS)) < 0)
{
loge (("invalid article timeout %s:%d: %s", file, n, buf));
continue;
}
if (!list)
{
list = Scalloc (1, sizeof *list);
list->head = list;
} else
{
struct server_cfg *head = list->head;
list->next = Scalloc (1, sizeof *list);
list = list->next;
list->head = head;
}
/* if there's a @ in host there's a user and password */
hostname = username = password = NULL;
if (strchr(host, '@') != NULL)
{
if ((password = strrchr(host, ':')) != NULL)
*(password++) = '\0';
else
{
loge (("missing password in %s:%d: %s", file, n, buf));
continue;
}
if ((hostname = strrchr(password-2, '@')) != NULL) {
*(hostname++) = '\0';
username = host;
list->user = Sstrdup (username);
list->pass = Sstrdup (password);
}
} else {
hostname = host;
}
list->host = Sstrdup (hostname);
list->us = Sstrdup (us);
list->active_timeout = active_timeout;
list->active_times_timeout = active_times_timeout;
list->newsgroups_timeout = newsgroups_timeout;
list->group_timeout = group_timeout;
list->listgroup_timeout = group_timeout;
list->xover_timeout = xover_timeout;
list->article_timeout = article_timeout;
list->overview_fmt_timeout = con->overviewFmtTimeout;
list->fd=-1;
}
if (ferror(fp))
{
loge (("error reading servers config from %s", file));
Exit(1);
}
if (!list)
{
loge (("servers file %s contains no servers!", file));
fclose(fp);
return FALSE;
}
ServerList = list->head;
load_groups(file, fp);
fclose (fp);
return TRUE;
}
EXPORT struct server_cfg *findScfg(char *name)
/* [<][>][^][v][top][bottom][index][help] */
{
struct server_cfg *l=ServerList;
for (;l;l=l->next)
{
if (strCaseEq(l->host, name))
return l;
}
return NULL;
}
static void perform_chroot()
/* [<][>][^][v][top][bottom][index][help] */
{
#ifdef HAVE_CHROOT
if (chdir (con->chrootDir) != 0 || chroot (".") != 0)
{
loge (("unable to chroot(\"%s\")", con->chrootDir));
Exit(2);
}
#else
loge (("no chroot() call available on this system"));
Exit(2);
#endif
}
static void usage (char *argv0)
/* [<][>][^][v][top][bottom][index][help] */
{
fprintf (stderr, "usage: %s [ehinrs] [-b addr:port] [-c config_file]\n", argv0);
exit (1);
}
static void drop_priv(int uid, int gid)
/* [<][>][^][v][top][bottom][index][help] */
{
/* Can't drop priviledges if we're not root. */
if (geteuid() != 0)
return;
if (setgid (gid) == -1)
{
loge (("unable to set gid to %d", gid));
#ifndef DEBUG
Exit (2);
#endif
}
if (setuid (uid) == -1)
{
loge (("unable to set uid to %d", uid));
#ifndef DEBUG
Exit (2);
#endif
}
}
static void drop_idle_servers()
/* [<][>][^][v][top][bottom][index][help] */
{
struct server_cfg *p;
time_t ti = time(NULL);
for (p=ServerList; p; p=p->next)
if (p->last_active_time &&
ti - p->last_active_time > con->remoteIdleTimeout)
detachServer(p);
}
static void emit_banner(bool post_ok)
/* [<][>][^][v][top][bottom][index][help] */
{
emitf ("%d %s NNTPCache server V%s [see www.nntpcache.com] "
" (c) 1996-2002 Julian Assange <proff@iq.org> %s ready"
" (posting %s, %d groups available).\r\n",
post_ok? NNTP_POSTOK_VAL: NNTP_NOPOSTOK_VAL,
Host,
VERSION,
__DATE__,
post_ok? "ok": "not permitted",
(int)Stats->list_stats[l_active].entries
);
}
static bool relay_unknown (char *buf)
/* [<][>][^][v][top][bottom][index][help] */
{
Cemit (buf);
Cflush (buf);
if (!Cget (buf, sizeof buf))
{
CurrentScfg->share->relay_fail++;
emitrn (NNTP_SERVERDOWN);
return FALSE;
}
CurrentScfg->share->relay_good++;
emit (buf);
switch (strToi(buf))
{
case NNTP_HELPOK_VAL:
case NNTP_LIST_FOLLOWS_VAL:
case NNTP_ARTICLE_FOLLOWS_VAL:
case NNTP_HEAD_FOLLOWS_VAL:
case NNTP_BODY_FOLLOWS_VAL:
case NNTP_OVERVIEW_FOLLOWS_VAL:
case 230:
case NNTP_NEWGROUPS_FOLLOWS_VAL:
case NNTP_XGTITLE_OK_VAL:
getArt (CurrentScfg, clientout);
break;
case NNTP_GOODBYE_ACK_VAL:
retire_vm_proc (0);
break;
case NNTP_GROUPOK_VAL:
/* case NNTP_NOTHING_FOLLOWS_VAL: */
if (strlen (buf) > (size_t)5 && !isdigit (buf[5]))
{
getArt (CurrentScfg, clientout);
}
break;
case NNTP_AUTH_NEEDED_VAL:
case NNTP_AUTH_NEXT_VAL:
case NNTP_AUTH_OK_VAL:
break;
default:
break;
}
return TRUE; /* XXX dubious */
}
static bool client_cmd (char *buf)
/* [<][>][^][v][top][bottom][index][help] */
{
struct command *cmd;
bool ret = FALSE;
char a0[32]="", a1[501]="";
int i;
sscanf(buf, "%31[^\r\n\t ]%*[\r\n\t ]%480[^\r\n]", a0, a1);
if (a0[0] == '\0')
return FALSE;
settaskinfo("%s [%s]: %.32s %.32s", ClientHost, (*CurrentGroup && Task->ti_state == nc_client && con->taskInfoPrivacy)? "private": CurrentGroup, a0, a1);
for (cmd = commands; cmd->cmd; cmd++)
if (strCaseEq(a0, cmd->cmd))
break;
if (CurrentGroupScfg)
CurrentScfg = CurrentGroupScfg;
CS = &Stats->cache_stats[cmd->val];
CS->requests++;
CS->clientFromBytes+=strlen(buf);
Command = cmd;
alarm(con->idleTimeout);
if (ConnectAuth && ConnectAuth->authinfo && ConnectAuth->authinfo->type != AUTHINFO_NONE && authinfo_ok_cmd(cmd->val) == 0) {
emitf("%d Authentication required for command\r\n", NNTP_AUTH_NEEDED_VAL );
return FALSE;
}
switch(cmd->val)
{
case c_post:
ModeReader = TRUE;
ret = CMDpost ();
break;
case c_ihave:
ret = CMDihave (buf);
break;
case c_listgroup:
ModeReader = TRUE;
ret = CMDlistgroup (buf);
break;
case c_newnews:
ModeReader = TRUE;
ret = CMDnewnews(buf);
break;
case c_newgroups:
ModeReader = TRUE;
ret = CMDnewgroups(buf);
break;
case c_xgtitle:
sprintf(buf, "list %.120s %.120s", a0, a1);
/* FALL-THOUGH */
case c_list:
ret = CMDlist (buf);
break;
case c_xover:
ModeReader = TRUE;
ret = CMDxover (buf);
break;
case c_xhdr:
ModeReader = TRUE;
ret = CMDxhdr (buf);
break;
case c_group:
ModeReader = TRUE;
ret = CMDgroup (buf);
break;
case c_date:
ret = CMDdate (buf);
break;
case c_help:
emitrn(NNTP_HELP_FOLLOWS);
for (i = 0; commands[i].cmd; i++)
emitf (" %s %s\r\n", commands[i].cmd, commands[i].desc);
emitf ("Report local configuration problems to <%s> or NNTPCache specific problems to <nntpcache@nntpcache.com>\r\n", con->adminEmail);
emitrn (".");
break;
case c_article:
case c_head:
case c_body:
case c_stat:
ModeReader = TRUE;
ret = CMDarticle (cmd, buf, FALSE);
break;
case c_next:
ModeReader = TRUE;
ret = CMDnext (buf);
break;
case c_last:
ModeReader = TRUE;
ret = CMDlast (buf);
break;
case c_slave:
emitrn ("202 Unsupported");
ret = FALSE;
break;
case c_noop:
emitrn ("500 noop");
break;
case c_xpath:
ModeReader = TRUE;
ret = CMDxpath (buf);
break;
case c_quit:
{
settaskinfo("%s QUITing", ClientHost);
sigemptyset(&myaction.sa_mask);
myaction.sa_flags = 0;
myaction.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &myaction, NULL);
emitrn (NNTP_GOODBYE_ACK);
CS->requests_good++;
retire_vm_proc (0);
NOTREACHED;
}
case c_authinfo:
ret = CMDauthinfo (buf);
break;
case c_mode:
if (strnCaseEq(a1, "reader", 6) ||
strnCaseEq(a1, "query", 5))
{
ModeReader = TRUE;
emit_banner(ConnectAuth->post);
break;
}
/* FALL-THROUGH */
default:
if (con->relayUnknowns)
ret = relay_unknown (buf);
else
{
strStripEOL(buf);
loginn (("%s unrecognized command %.128s", ClientHostNormal, buf));
emitrn (NNTP_BAD_COMMAND);
ret = FALSE;
}
}
if (ret)
CS->requests_good++;
else
CS->requests_failed++;
return ret;
}
static void client_cmd_loop ()
/* [<][>][^][v][top][bottom][index][help] */
{
for (;;)
{
char buf [MAX_CMD];
alarm(con->idleTimeout);
flush (); /* required! */
settaskinfo("%s [%s]: waiting for input", ClientHost, (*CurrentGroup && Task->ti_state == nc_client && con->taskInfoPrivacy)? "private": CurrentGroup);
drop_idle_servers();
if (!Get (buf, sizeof buf))
{
char *p = strerror(errno);
logd (("client '%s' diconnected before QUIT", ClientHost));
loginn (("%s cant read %s", ClientHostNormal, p));
settaskinfo("%s diconnected before QUIT", ClientHost);
retire_vm_proc (0);
}
client_cmd (buf);
}
}
static bool client_handler ()
/* [<][>][^][v][top][bottom][index][help] */
{
while (HoldForksClient) {}
Stats->clientConnects++;
Stats->clientConnectsFailed++; /* presume failure */
alarm(con->idleTimeout);
settaskinfo("authenticating client");
if (!fillAuth(fileno(clientin), "<nntp>"))
{
emitf ("%d NNTPCache-%s access denied <%s>, you do not have connect permissions in the %s file.\r\n", NNTP_ACCESS_VAL, VERSION, ClientHost, con->accessFile);
log (("refused connect from %s (%s)", ClientHost, ClientHostAddr));
loginn (("%s no_access", ClientHostNormal));
return FALSE;
}
if (Stats->clientsActive > con->maxReaders)
{
emitf ("%d NNTPCache-%s too many concurrent reader sessions already (%d). try again later\r\n", NNTP_TEMPERR_VAL, VERSION, Stats->clientsActive);
log (("%s romance refused with %s (%s) due to too many users (%d)", ClientHostNormal, ClientHost, ClientHostAddr, Stats->clientsActive));
return FALSE;
}
if ((f_cleanSlate && UpdateDaemonPid) ||
con->minActive > Stats->list_stats[l_active].entries)
{
int togo = con->minActive - Stats->list_stats[l_active].entries;
emitf ("%d NNTPCache-%s %s server rebuild in progress (%d groups complete, at least %d groups to go. Please try again later. If you think this is an error, get your news admin to check minGroups (in %s), %s and the nntpcache web-server output\r\n",
NNTP_SERVERDOWN_VAL,
VERSION,
(f_cleanSlate && UpdateDaemonPid)? "Initial": "Failing",
(int)Stats->list_stats[l_active].entries,
(togo>0)? togo: 0,
con->configFile,
con->serversFile);
log (("romance refused with %s (%s) due to %s server rebuild in progress (%d groups complete, at least %d groups to go)",
ClientHost,
ClientHostAddr,
(f_cleanSlate && UpdateDaemonPid)? "initial": "failing",
(int)Stats->list_stats[l_active].entries,
(togo>0)? togo: 0));
return FALSE;
}
if (con->contentFilters)
setenv ("CLIENTHOST", ClientHost, 1);
log (("%s connect from %s (%s)", ClientHostNormal, ClientHostRFC931, ClientHostAddr));
loginn (("%s connect", ClientHostNormal));
settaskinfo("%s: starting", ClientHost);
emit_banner(ConnectAuth->post);
CurrentScfg = ServerList->head;
Stats->clientConnectsFailed--;
client_cmd_loop ();
NOTREACHED;
}
int createPort (char *addr)
/* [<][>][^][v][top][bottom][index][help] */
{
int fd;
struct sockaddr_in *in;
int yes = 1;
fd = socket (AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
loge (("couldn't get socket()"));
retire_vm_proc (1);
}
setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, (char*) &yes, sizeof yes);
in = getHostAddr (addr);
if (!in || bind (fd, (struct sockaddr *) in, sizeof *in) == -1)
{
loge (("couldn't bind %s", addr));
retire_vm_proc (1);
}
listen (fd, 50);
FD_SET (fd, &r_set);
if (fd > high_fd)
high_fd = fd;
return fd;
}
/*
* return's TRUE for reload
*/
static bool master_loop ()
/* [<][>][^][v][top][bottom][index][help] */
{
#ifdef CRASHES_UNDER_LINUX
char *io_buf;
#endif
fd_set rt_set, et_set;
if (!sig_hup)
{
FILE *fh;
FD_ZERO (&r_set);
FD_ZERO (&rt_set);
FD_ZERO (&et_set);
NNTPportFD = createPort (con->bindAddr);
if (con->httpServer)
HTTPportFD = createPort (con->httpBindAddr);
signal (SIGCHLD, sigchld);
signal (SIGHUP, sighup);
signal (SIGPIPE, SIG_IGN);
signal (SIGUSR1, sigusr1);
signal (SIGUSR2, sigusr2);
open_mmap();
task_info_init ();
loadStats (con->statsFile);
Task = task_info_new (nc_master, "master");
watchInit();
drop_priv(ncUID, ncGID);
sprintf (PidFile, "%.127s.%.164s", con->pidFile, con->bindAddr);
if (!(fh = fopen (PidFile, "w")))
logw (("couldn't open pid file '%s'", PidFile));
else
{
fprintf (fh, "%d\n", (int) getpid ());
fclose (fh);
}
}
sig_hup = FALSE;
overviewFmt = overviewFmtGen(NULL, con->overviewFmtInternal, &overviewFmt_hash);
set_cfg_shm();
expire (FALSE);
updateDaemon (TRUE);
#ifdef CRASHES_UNDER_LINUX
io_buf = Smalloc(con->outputBufferSize);
#endif
log (("waiting for NTTP connections on %s", con->bindAddr));
if (con->httpServer)
log (("waiting for HTTP connections on %s", con->httpBindAddr));
for (;;)
{
struct sockaddr_in remote;
int remlen = sizeof remote;
int client;
int sel;
int n;
int s_errno;
settaskinfo("waiting for connections");
rt_set = et_set = r_set;
sel = select (high_fd + 1, &rt_set, NULL, &et_set, NULL);
s_errno = errno;
check_child ();
if (sel < 1) /* TODO: fix signal race window */
{
if (s_errno != EINTR)
{
errno = s_errno;
logw (("main daemon select() failed"));
continue;
}
if (sig_hup)
{
errno = 0;
logw (("caught SIGHUP, restarting..."));
return TRUE;
}
continue;
}
if (FD_ISSET (NNTPportFD, &rt_set))
{
if ((client = accept (NNTPportFD, (struct sockaddr *) &remote, &remlen)) == -1)
{
if (errno == EINTR)
{
if (sig_hup)
{
logw (("caught SIGHUP, restarting..."));
return TRUE;
}
} else
loge (("accept() failed"));
goto play_with_children;
}
if (sig_hup)
{
errno = 0;
logw (("caught SIGHUP, restarting..."));
return TRUE;
}
if (make_vm_proc (nc_client, client, "client") == 0) /* child == 0 */
{
set_client_sigset ();
client_handler ();
retire_vm_proc (0);
}
updateDaemon (FALSE);
if (!f_cleanSlate || !UpdateDaemonPid)
{
nocemDaemon ();
expire (FALSE);
}
}
if (con->httpServer && FD_ISSET (HTTPportFD, &rt_set))
{
int http_client;
if ((http_client = accept (HTTPportFD, (struct sockaddr *) &remote, &remlen)) == -1)
{
if (errno == EINTR)
{
if (sig_hup)
{
logw (("caught SIGHUP, restarting..."));
return TRUE;
}
} else
loge (("accept() failed"));
goto play_with_children;
}
if (sig_hup)
{
errno = 0;
logw (("caught SIGHUP, restarting..."));
return TRUE;
}
if (make_vm_proc (nc_http, http_client, "http") == 0) /* child == 0 */
{
set_client_sigset ();
httpHandler ();
retire_vm_proc (0);
}
}
play_with_children:
for (n = high_fd; n > MAX(NNTPportFD, HTTPportFD); n--)
{
if (FD_ISSET (n, &rt_set) || FD_ISSET (n, &et_set))
{
if (!DoIPC (n) || FD_ISSET(n, &et_set))
{
FD_CLR (n, &r_set);
close (n);
if (n == high_fd)
high_fd--;
}
}
}
}
NOTREACHED;
}
void
detach()
/* [<][>][^][v][top][bottom][index][help] */
{
#ifndef HAVE_DAEMON
int fd;
#endif
int n =
#ifndef IDIOTIC_BUGS_IN_LINUX_OPENLOG_NOT_PRESENT
3;
#else
# ifdef HAVE_DTABLESIZE
getdtablesize ();
# else
256;
# endif
#endif
logd (("detaching from tty"));
while (n)
close (--n);
#ifdef HAVE_DAEMON
daemon (1, 0);
#else
if (fork ())
exit (0);
if ((fd = open ("/dev/null", O_RDWR)) != -1)
{
if (fd !=0 )
dup2 (fd, 0);
dup2 (fd, 1);
dup2 (fd, 2);
}
# ifdef TIOCNOTTY
fd = open ("/dev/tty", O_RDWR | O_NOCTTY);
if (fd != -1)
{
ioctl (0, TIOCNOTTY, NULL);
close (fd);
}
# else
setsid ();
# endif
#endif
Detached = TRUE;
}
int main (int argc, char **argv, char **envp)
/* [<][>][^][v][top][bottom][index][help] */
{
char buf[MAX_CMD];
int c;
struct passwd *pw;
struct group *gr;
int nodetach = FALSE;
int expireonly = FALSE;
int zorch = FALSE;
char *config_file = con->configFile;
char *access_file = con->accessFile;
char *bindAddr = NULL;
struct hostent *hp;
enum task_state task;
char *p = NULL;
bool reloading = FALSE;
openlog ("nntpcached", LOG_PID|LOG_NDELAY, LOG_NEWS);
enableCoreDump();
fprintf (stderr, "\
NNTPCache-" VERSION "\n\
Copyright (c) 1996-2002 Julian Assange <proff@iq.org>\n\
Copyright (c) 1998-2002 Australian National Cognitive Facility\n\
See the files \"COPYING\", \"FAQ\", \"LICENSING\" and \n\
http://www.nntpcache.com/ for copyright details\n");
fflush(stderr);
Argv0 = Sstrdup(argv[0]);
/* start a master task unless told otherwise */
assert(task_desc[nc_last] == NULL);
task = nc_master;
while ((c = getopt (argc, argv, "ef:hnb:rc:s")) != -1)
switch (c)
{
case 'a':
access_file = Sstrdup(optarg);
break;
case 'e':
expireonly = TRUE;
task = nc_oneshot;
break;
case 'f':
for (p = optarg; *p; p++)
switch (*p)
{
case 'a':
HoldForks = TRUE;
break;
case 'c':
HoldForksClient = TRUE;
break;
case 'h':
HoldForksHttp = TRUE;
break;
case 'n':
HoldForksNocem = TRUE;
break;
case 'u':
HoldForksUpdate = TRUE;
break;
}
break;
case 'c':
config_file = Sstrdup(optarg);
break;
case 'n':
nodetach = TRUE;
break;
case 'b':
bindAddr = Sstrdup(optarg);
break;
case 's':
SwapWithChild = TRUE;
break;
case 'h':
task = nc_oneshot;
MakeHistory = TRUE;
break;
case 'z':
task = nc_oneshot;
zorch = TRUE;
break;
default:
usage (argv[0]);
}
Task->ti_state = nc_master;
initsetproctitle(argc, argv, envp);
settaskinfo("initialising");
umask (022);
gethostname (Host, sizeof (Host));
if ((hp = gethostbyname (Host)))
strncpy (Host, hp->h_name, sizeof Host);
reload:
if (chdir (con->configDir) == -1)
{
loge (("couldn't set cwd to %s", con->configDir));
Exit (2);
}
if (!load_config (config_file))
Exit (2);
umask (con->umask);
safeGroupInit (con->safeGroup);
if (!nocemInit ())
con->nocem = FALSE;
if (!postInit())
Exit (1);
if (zorch && !reloading)
{
printf ("zorching %s/{cache.mmap,%s.{dir,pag}}!\n", con->cacheDir, con->historyFile);
chdir (con->cacheDir);
unlink (con->mmapFile);
unlink (con->mmapBaseFile);
unlink (con->historyFile);
sprintf (buf, "%.127s.pag", con->historyFile);
unlink (buf);
sprintf (buf, "%.127s.dir", con->historyFile);
unlink (buf);
zorch = FALSE; /* don't zorch again on reload */
}
if (expireonly && !reloading)
{
puts ("Running expire (only)...");
if (chdir (con->cacheDir) != 0)
{
perror (con->cacheDir);
exit (1);
}
open_mmap();
task_info_init ();
loadStats (con->statsFile);
expire (TRUE);
exit (0);
}
if (bindAddr)
{
if (con->bindAddr) free(con->bindAddr);
con->bindAddr = Sstrdup(bindAddr);
}
if (chdir (con->configDir) == -1)
{
loge (("couldn't set cwd to %s", con->configDir));
Exit (2);
}
logd(("cwd now %s", con->configDir));
if (!load_servers (con->serversFile))
Exit (2);
CurrentScfg = ServerList;
if (!authReadConfig (con->accessFile))
Exit (2);
if (!(pw = getpwnam (con->user)))
{
loge (("configuration error: no such user '%s'", con->user));
Exit (2);
}
ncUID = pw->pw_uid;
if (!(gr = getgrnam (con->group)))
{
loge (("configuration error: no such group '%s'", con->group));
Exit (2);
}
ncGID = gr->gr_gid;
if (con->chroot && !reloading)
perform_chroot();
if (Task->ti_state != nc_master && !reloading)
drop_priv(ncUID, ncGID);
if (chdir (con->cacheDir) == -1)
{
loge (("couldn't set cwd to %s", con->cacheDir));
Exit (2);
}
logd(("cwd now %s", con->cacheDir));
if (!nodetach && !reloading)
{
detach();
Debug_fd = -1;
}
if (!reloading && nodetach)
{
Debug_fd = dup(2);
}
#ifdef AUTHINFO_RADIUS
if (!authinfo_radius_init())
Exit (1);
#endif
sigemptyset(&myaction.sa_mask);
sigaddset(&myaction.sa_mask, SIGTERM);
sigaddset(&myaction.sa_mask, SIGALRM);
sigaddset(&myaction.sa_mask, SIGPIPE);
myaction.sa_flags = 0;
myaction.sa_handler = sigterm;
sigaction(SIGTERM, &myaction, NULL);
signal (SIGINT, sigint);
signal (SIGSEGV, sigsegv);
signal (SIGFPE, SIG_IGN);
if (MakeHistory && !reloading)
{
settaskinfo("rebuilding history file");
build_history();
}
if (Task->ti_state != nc_master)
Exit (0);
if (master_loop ())
{
reloading = TRUE;
goto reload;
}
NOTREACHED;
}
/*
* Without the following, our custom calloc may not get linked resulting
* in a version mismatch.
*/
void calloc_dummy() {
/* [<][>][^][v][top][bottom][index][help] */
calloc(0, 0);
}