/*  $Revision: 1.32 $
**
**  NNTP server for readers (NNRP) for InterNetNews.
**  This server doesn't do any real load-limiting, except for what has
**  proven empirically necesary (i.e., look at GRPscandir).
*/
#include <stdio.h>
#include <sys/types.h>
#include "configdata.h"
#include "clibrary.h"
#define MAINLINE
#include "nnrpd.h"
#include <netdb.h>
#include <pwd.h>
#if	defined(HPUX)
#include <sys/pstat.h>
#endif	/* defined(HPUX) */


/*
** Here is some defensive code to protect the news server from hosts,
** mostly PC's, that sometimes make a connection and then never give
** any commands.  The connection is abandoned, but we're never told
** about it.  The first time the connection is read, it will have a
** timeout value of INITIAL_TIMEOUT seconds.  All subsequent reads
** will have the standard timeout of CLIENT_TIMEOUT seconds.
*/
#if !defined(INITIAL_TIMEOUT)
#define INITIAL_TIMEOUT	10
#endif

#define CMDany		-1


typedef struct _CMDENT {
    STRING	Name;
    FUNCPTR	Function;
    BOOL	Needauth;
    int		Minac;
    int		Maxac;
    STRING	Help;
} CMDENT;


char	NOACCESS[] = NNTP_ACCESS;
char	ACTIVE[] = _PATH_ACTIVE;
char	ACTIVETIMES[] = _PATH_ACTIVETIMES;
char	HISTORY[] = _PATH_HISTORY;
char	NEWSGROUPS[] = _PATH_NEWSGROUPS;
    /* Default permission -- change with adb. */
BOOL	PERMdefault = FALSE;

STATIC double	STATstart;
STATIC double	STATfinish;
STATIC char	*PushedBack;
#if	!defined(HPUX)
STATIC char	*TITLEstart;
STATIC char	*TITLEend;
#endif	/* !defined(HPUX) */
STATIC SIGVAR	ChangeTrace;

extern FUNCTYPE	CMDauthinfo();
extern FUNCTYPE	CMDdate();
extern FUNCTYPE	CMDfetch();
extern FUNCTYPE	CMDgroup();
STATIC FUNCTYPE	CMDhelp();
extern FUNCTYPE	CMDlist();
extern FUNCTYPE	CMDmode();
extern FUNCTYPE	CMDnewgroups();
extern FUNCTYPE	CMDnewnews();
extern FUNCTYPE	CMDnextlast();
extern FUNCTYPE	CMDpost();
extern FUNCTYPE	CMDxgtitle();
extern FUNCTYPE	CMDxhdr();
extern FUNCTYPE	CMDxover();
extern FUNCTYPE	CMDxpat();
extern FUNCTYPE	CMDxpath();
extern FUNCTYPE	CMD_unimp();

STATIC char	CMDfetchhelp[] = "[MessageID|Number]";

STATIC CMDENT	CMDtable[] = {
    {	"authinfo",	CMDauthinfo,	FALSE,	3,	CMDany,
	"user Name|pass Password|generic <prog> <args>" },
    {	"article",	CMDfetch,	TRUE,	1,	2,
	CMDfetchhelp },
    {	"body",		CMDfetch,	TRUE,	1,	2,
	CMDfetchhelp },
    {	"date",		CMDdate,	FALSE,	1,	1,
	NULL },
    {	"group",	CMDgroup,	TRUE,	2,	2,
	"newsgroup" },
    {	"head",		CMDfetch,	TRUE,	1,	2,
	CMDfetchhelp },
    {	"help",		CMDhelp,	FALSE,	1,	CMDany,
	NULL },
    {	"ihave",	CMD_unimp,	TRUE,	1,	2,
	NULL },
    {	"last",		CMDnextlast,	TRUE,	1,	1,
	NULL },
    {	"list",		CMDlist,	TRUE,	1,	3,
	"[active|active.times|newsgroups|distributions|distrib.pats|overview.fmt|subscriptions]" },
    {	"listgroup",	CMDgroup,	TRUE,	1,	2,
	"newsgroup" },
    {	"mode",		CMDmode,	FALSE,	2,	2,
	"reader" },
    {	"newgroups",	CMDnewgroups,	TRUE,	3,	5,
	"yymmdd hhmmss [\"GMT\"] [<distributions>]" },
    {	"newnews",	CMDnewnews,	TRUE,	4,	6,
	"newsgroups yymmdd hhmmss [\"GMT\"] [<distributions>]" },
    {	"next",		CMDnextlast,	TRUE,	1,	1,
	NULL },
    {	"post",		CMDpost,	TRUE,	1,	1,
	NULL },
    {	"slave",	CMD_unimp,	FALSE,	1,	1,
	NULL },
    {	"stat",		CMDfetch,	TRUE,	1,	2,
	CMDfetchhelp },
    {	"xgtitle",	CMDxgtitle,	TRUE,	1,	2,
	"[group_pattern]" },
    {	"xhdr",		CMDxhdr,	TRUE,	2,	3,
	"header [range|MessageID]" },
    {	"xover",	CMDxover,	TRUE,	1,	2,
	"[range]" },
    {	"xpat",		CMDxpat,	TRUE,	4,	CMDany,
	"header range|MessageID pat [morepat...]" },
    {	"xpath",	CMDxpath,	TRUE,	2,	2,
	"MessageID" },
    {	NULL }
};


/*
**  Log a summary status message and exit.
*/
NORETURN
ExitWithStats(x)
    int			x;
{
    TIMEINFO		Now;
    double		usertime;
    double		systime;

    (void)fflush(stdout);
    (void)GetTimeInfo(&Now);
    STATfinish = TIMEINFOasDOUBLE(Now);
    if (GetResourceUsage(&usertime, &systime) < 0) {
	usertime = 0;
	systime = 0;
    }

    GRPreport();
    syslog(L_NOTICE, "%s exit articles %ld groups %ld",
	   ClientHost, ARTcount, GRPcount);
    if (POSTreceived ||  POSTrejected)
	syslog(L_NOTICE, "%s posts received %ld rejected %ld",
	   ClientHost, POSTreceived, POSTrejected);
    syslog(L_NOTICE, "%s times user %.3f system %.3f elapsed %.3f",
	ClientHost, usertime, systime, STATfinish - STATstart);
    exit(x);
}


/*
**  The "help" command.
*/
/* ARGSUSED0 */
STATIC FUNCTYPE
CMDhelp(ac, av)
    int		ac;
    char	*av[];
{
    CMDENT	*cp;

    Reply("%s\r\n", NNTP_HELP_FOLLOWS);
    for (cp = CMDtable; cp->Name; cp++)
	if (cp->Help == NULL)
	    Printf("  %s\r\n", cp->Name);
	else
	    Printf("  %s %s\r\n", cp->Name, cp->Help);
    Printf("Report problems to <%s@%s>\r\n",
	NEWSMASTER, GetConfigValue(_CONF_FROMHOST));
    Reply(".\r\n");
}


/*
**  Unimplemented catch-all.
*/
/* ARGSUSED0 */
FUNCTYPE
CMD_unimp(ac, av)
    int		ac;
    char	*av[];
{
    if (caseEQ(av[0], "ihave"))
	Reply("%d Transfer permission denied\r\n", NNTP_AUTH_NEEDED_VAL);
    else if (caseEQ(av[0], "slave"))
	/* Somebody sends us this?  I don't believe it! */
	Reply("%d Unsupported\r\n", NNTP_SLAVEOK_VAL);
    else
	Reply("%d %s not implemented; try help\r\n",
	    NNTP_BAD_COMMAND_VAL, av[0]);
}


/*
**  Overwrite the original argv so that ps will show what's going on.
*/
STATIC void
TITLEset(what)
    char		*what;
{
#if	defined(DO_HAVE_SETPROCTITLE)
    setproctitle("%s %s", ClientHost, what);
#else
#if	!defined(HPUX)
    register char	*p;
    register int	i;
    char		buff[BUFSIZ];

    /* Make ps think we're swapped out so we get "(nnrpd)" in the output. */
    p = TITLEstart;
    *p++ = '-';

    (void)sprintf(buff, "%s %s", ClientHost, what);
    i = strlen(buff);
    if (i > TITLEend - p - 2) {
	i = TITLEend - p - 2;
	buff[i] = '\0';
    }
    (void)strcpy(p, buff);
    for (p += i; p < TITLEend; )
	*p++ = ' ';
#else
    char		buff[BUFSIZ];
    union pstun un;
    
    (void)sprintf(buff, "(nnrpd) %s %s", ClientHost, what);
    (void)pstat(PSTAT_SETCMD, un, strlen(buff), 0, 0);
#endif	/* defined(HPUX) */
#endif	/* defined(DO_HAVE_SETPROCTITLE) */
}


#if	defined(DO_NNRP_GETHOSTBYADDR)
/*
**  Convert an IP address to a hostname.  Don't trust the reverse lookup,
**  since anyone can fake .in-addr.arpa entries.
*/
STATIC BOOL
Address2Name(ap, hostname, i)
    register INADDR		*ap;
    register char		*hostname;
    register int		i;
{
    register char		*p;
    register struct hostent	*hp;
#if	defined(h_addr)
    register char		**pp;
#endif

    /* Get the official hostname, store it away. */
    if ((hp = gethostbyaddr((char *)ap, sizeof *ap, AF_INET)) == NULL)
	return FALSE;
    (void)strncpy(hostname, hp->h_name, i);
    hostname[i - 1] = '\0';

    /* Get addresses for this host. */
    if ((hp = gethostbyname(hostname)) == NULL)
	return FALSE;

    /* Make sure one of those addresses is the address we got. */
#if	defined(h_addr)
    /* We have many addresses */
    for (pp = hp->h_addr_list; *pp; pp++)
	if (memcmp((POINTER)&ap->s_addr, (POINTER)*pp,
		(SIZE_T)hp->h_length) == 0)
	    break;
    if (*pp == NULL)
	return FALSE;
#else
    /* We have one address. */
    if (memcmp((POINTER)&ap->s_addr, (POINTER)hp->h_addr,
	    (SIZE_T)hp->h_length) != 0)
	return FALSE;
#endif

    /* Only needed for misconfigured YP/NIS systems. */
    if (strchr(hostname, '.') == NULL
     && (p = GetConfigValue(_CONF_DOMAIN)) != NULL) {
	(void)strcat(hostname, ".");
	(void)strcat(hostname, p);
    }

    /* Make all lowercase, for wildmat. */
    for (p = hostname; *p; p++)
	if (CTYPE(isupper, *p))
	    *p = tolower(*p);
    return TRUE;
}
#endif	/* defined(DO_NNRP_GETHOSTBYADDR) */


BOOL
PERMinfile(hp, ip, user, pass, accesslist)
    char		*hp;
    char		*ip;
    char		*user;
    char		*pass;
    char		*accesslist;
{
    static char		ACCESS[] = _PATH_NNRPACCESS;
    register FILE	*F;
    register char	*p;
    register BOOL	found;
    register int	i;
    struct passwd	*pwd;
    char		buff[BIG_BUFFER];
    char		*fields[5];

    if ((F = fopen(ACCESS, "r")) == NULL) {
	syslog(L_ERROR, "%s cant fopen %s %m", ClientHost, ACCESS);
	return FALSE;
    }

    PERMcanread = FALSE;
    PERMcanpost = FALSE;
    found = FALSE;
    accesslist[0] = '\0';
    while (fgets(buff, sizeof buff, F) != NULL) {
	if ((p = strchr(buff, '\n')) != NULL)
	    *p = '\0';
	if ((p = strchr(buff, COMMENT_CHAR)) != NULL)
	    *p = '\0';
	if (buff[0] == '\0')
	    continue;

	/* Split "host:permissions:user:pass:groups" into fields. */
	for (fields[0] = buff, i = 0, p = buff; *p; p++)
	    if (*p == ':') {
		*p = '\0';
		fields[++i] = p + 1;
	    }
	if (i != 4)
	    /* Malformed line. */
	    continue;

	if (hp)
	    /* Got an address; try to match either the IP address or as
	     * a text hostname. */
	    if (!(ip && wildmat(ip, fields[0])) && !wildmat(hp, fields[0]))
		continue;

	/* See if we should lookup a specific user in the passwd file */
	if (user && pass && EQ(fields[2], "+")) {
	    if ((pwd = getpwnam(user)) == NULL)
		continue;
	    if (!EQ(pwd->pw_passwd, crypt(pass, pwd->pw_passwd)))
		continue;
	}
	/* Matching for a specific user or just the host? */
	else if (user && (!EQ(user, fields[2]) || !EQ(pass, fields[3])))
	    continue;

	PERMcanread = strchr(fields[1], 'R') != NULL;
	PERMcanpost = strchr(fields[1], 'P') != NULL;
	(void)strcpy(PERMuser, user ? user : fields[2]);
	(void)strcpy(PERMpass, pass ? pass : fields[3]);
	(void)strcpy(accesslist, fields[4]);
	found = TRUE;
    }
    (void)fclose(F);
    return found;
}


/*
**  Determine access rights of the client.
*/
STATIC void
StartConnection(accesslist)
    char		*accesslist;
{
    struct sockaddr_in	sin;
    int			length;
    char		buff[SMBUF];
    char		*ClientAddr;

    /* Get the peer's name. */
    length = sizeof sin;
    ClientAddr = NULL;
    if (getpeername(STDIN, (struct sockaddr *)&sin, &length) < 0) {
	if (!isatty(STDIN)) {
	    syslog(L_ERROR, "%s cant getpeername %m", "?");
            (void)strcpy(ClientHost, "?"); /* so stats generation looks correct. */
	    Printf("%d I can't get your name.  Goodbye.\r\n", NNTP_ACCESS_VAL);
	    ExitWithStats(1);
	}
	(void)strcpy(ClientHost, "stdin");
    }
#if defined(AF_DECnet)
    else if (sin.sin_family == AF_DECnet) {
	char *p, *MyDECnetDomain;
	(void) strcpy(ClientHost, getenv("REMNODE"));
	for (p = ClientHost; *p; p++)
	    if (CTYPE(isupper, *p))
		*p = tolower(*p);
#define _CONF_DECNETDOMAIN "decnetdomain"
	if ((MyDECnetDomain = GetConfigValue(_CONF_DECNETDOMAIN)) != NULL) {
	    (void)strcat(ClientHost, ".");
	    (void)strcat(ClientHost, MyDECnetDomain);
	}
    }
#endif
    else {
	if (sin.sin_family != AF_INET) {
	    syslog(L_ERROR, "%s bad_address_family %ld",
		"?", (long)sin.sin_family);
	    Printf("%d Bad address family.  Goodbye.\r\n", NNTP_ACCESS_VAL);
	    ExitWithStats(1);
	}

	/* Get client's name. */
#if	defined(DO_NNRP_GETHOSTBYADDR)
	if (!Address2Name(&sin.sin_addr, ClientHost, sizeof ClientHost)) {
	    (void)strcpy(ClientHost, inet_ntoa(sin.sin_addr));
	    syslog(L_NOTICE,
		"? cant gethostbyaddr %s %m -- using IP address for access",
		ClientHost);
	}
	else {
	    ClientAddr = buff;
	    (void)strcpy(buff, inet_ntoa(sin.sin_addr));
	}
#else
	(void)strcpy(ClientHost, inet_ntoa(sin.sin_addr));
#endif /* defined(DO_NNRP_GETHOSTBYADDR) */
    }

    strncpy (LogName,ClientHost,sizeof(LogName) - 1) ;
    LogName[sizeof(LogName) - 1] = '\0';

    syslog(L_NOTICE, "%s connect", ClientHost);
    if (!PERMinfile(ClientHost, ClientAddr, (char *)NULL, (char *)NULL,
	    accesslist)) {
	syslog(L_NOTICE, "%s no_access", ClientHost);
	Printf("%d You are not in my access file.  Goodbye.\r\n",
	    NNTP_ACCESS_VAL);
	ExitWithStats(1);
    }
}


#if	!defined(VAR_NONE)

#if	defined(VAR_VARARGS)
#if	defined(lint)
#define START_VARARG(fmt, vp, type)	va_start(vp); fmt = NULL
#else
#define START_VARARG(fmt, vp, type)	va_start(vp); fmt = va_arg(vp, type)
#endif	/* defined(lint) */
#endif	/* defined(VAR_VARARGS) */
#if	defined(VAR_STDARGS)
#define START_VARARG(fmt, vp, type)	va_start(vp, fmt)
#endif	/* defined(VAR_STDARGS) */

/*
**  Send a reply, possibly with debugging output.
*/
/*VARARGS*/
void
#if	defined(VAR_VARARGS)
Reply(va_alist)
    va_dcl
#endif	/* defined(VAR_VARARGS) */
#if	defined(VAR_STDARGS)
Reply(char *fmt, ...)
#endif	/* defined(VAR_STDARGS) */
{
    register int	oerrno;
    register char	*p;
    va_list		vp;
    char		buff[2048];
#if	defined(VAR_VARARGS)
    register char	*fmt;
#endif	/* defined(VAR_VARARGS) */

    START_VARARG(fmt, vp, char*);
    (void)vprintf(fmt, vp);
    va_end(vp);

    if (Tracing) {
	oerrno = errno;
	START_VARARG(fmt, vp, char*);

	/* Copy output, but strip trailing CR-LF. */
	(void)vsprintf(buff, fmt, vp);
	p = buff + strlen(buff) - 1;
	while (p >= buff && (*p == '\n' || *p == '\r'))
	    *p-- = '\0';
	syslog(LOG_DEBUG, "%s > %s", ClientHost, buff);

	va_end(vp);
	errno = oerrno;
    }
}
#endif	/* !defined(VAR_NONE) */


/*
**  Got a signal; toggle tracing.
*/
STATIC SIGHANDLER
ToggleTrace(s)
    int		s;
{
    ChangeTrace = TRUE;
    (void)signal(s, ToggleTrace);
}


/*
**  Print a usage message and exit.
*/
STATIC void
Usage()
{
    (void)fprintf(stderr, "Usage error.\n");
    exit(1);
}


/* ARGSUSED0 */
int
main(argc, argv, env)
    int			argc;
    char		*argv[];
    char		*env[];
{
#if	NNRP_LOADLIMIT > 0
    int			load;
#endif	/* NNRP_LOADLIMIT > 0 */
    CMDENT		*cp;
    char		buff[NNTP_STRLEN];
    char		**av;
    int			ac;
    READTYPE		r;
    TIMEINFO		Now;
    register int	i;
    char		*Reject;
    char		accesslist[BIG_BUFFER];
    int			timeout;

#if	!defined(HPUX)
    /* Save start and extent of argv for TITLEset. */
    TITLEstart = argv[0];
    TITLEend = argv[argc - 1] + strlen(argv[argc - 1]) - 1;
#endif	/* !defined(HPUX) */

    /* Parse arguments.   Must COPY() optarg if used because the
     * TITLEset() routine would clobber it! */
    Reject = NULL;
    while ((i = getopt(argc, argv, "S:r:s:t")) != EOF)
	switch (i) {
	default:
	    Usage();
	    /* NOTREACHED */
	case 'i':			/* Initial command */
	    PushedBack = COPY(optarg);
	    break;
	case 'S':			/* We're a slave to NNTP master */
	    RemoteMaster = COPY(optarg);
	    break;
	case 's':			/* Unused title string */
	    break;
	case 't':			/* Tracing */
	    Tracing = TRUE;
	    break;
	case 'r':			/* Reject connection message */
	    Reject = COPY(optarg);
	    break;
	}
    argc -= optind;
    if (argc)
	Usage();

    /* Setup. */
    openlog("nnrpd", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);
    if (GetTimeInfo(&Now) < 0) {
	syslog(L_FATAL, "cant gettimeinfo %m");
	exit(1);
    }
    STATstart = TIMEINFOasDOUBLE(Now);

    if ((MyHostName = GetConfigValue(_CONF_PATHHOST)) == NULL) {
	syslog(L_FATAL, "cant getconfigvalue %m");
	ExitWithStats(1);
    }
    MyHostName = COPY(MyHostName);

#if	NNRP_LOADLIMIT > 0
    if ((load = GetLoadAverage()) > NNRP_LOADLIMIT) {
	syslog(L_NOTICE, "load %d > %d", load, NNRP_LOADLIMIT);
	Reply("%d load at %d, try later\r\n", NNTP_GOODBYE_VAL, load);
	ExitWithStats(1);
    }
#endif	/* NNRP_LOADLIMIT > 0 */

    strcpy (LogName,"?") ;
    
    /* Ignore SIGPIPE, since we'll see closed connections with read. */
    (void)signal(SIGPIPE, SIG_IGN);

    /* Arrange to toggle tracing. */
    (void)signal(SIGHUP, ToggleTrace);

    /* Get permissions and see if we can talk to this client */
    StartConnection(accesslist);
    if (!PERMcanread && !PERMcanpost) {
	syslog(L_NOTICE, "%s no_permission", ClientHost);
	Printf("%d You have no permission to talk.  Goodbye.\r\n",
	       NNTP_ACCESS_VAL);
	ExitWithStats(1);
    }

    /* Proceed with initialization. */
    PERMneedauth = PERMuser[0] != '\0' && PERMpass[0] != '\0';
    PERMspecified = NGgetlist(&PERMlist, accesslist);
    TITLEset("connect");

    /* Were we told to reject connections? */
    if (Reject) {
	syslog(L_NOTICE, "%s rejected %s", ClientHost, Reject);
	Reply("%s %s\r\n", NNTP_GOODBYE, Reject);
	ExitWithStats(0);
    }

    ARTreadschema();
    if (!GetGroupList()) {
	/* This shouldn't really happen. */
	syslog(L_NOTICE, "%s cant getgrouplist %m", ClientHost);
	Reply("%d NNTP server unavailable. Try later.\r\n", NNTP_TEMPERR_VAL);
	ExitWithStats(1);
    }

#if defined(DO_PERL)
    /* Load the Perl code */
    PERLsetup(NULL, _PATH_PERL_FILTER_NNRPD, "filter_post");
    PerlFilter (TRUE) ;
#endif /* defined(DO_PERL) */

    Reply("%d %s InterNetNews NNRP server %s ready (%s).\r\n",
	   PERMcanpost ? NNTP_POSTOK_VAL : NNTP_NOPOSTOK_VAL,
	   MyHostName, INNVersion(),
	   PERMcanpost ? "posting ok" : "no posting");

    /* Main dispatch loop. */
    for (timeout = INITIAL_TIMEOUT, av = NULL; ; timeout = CLIENT_TIMEOUT) {
	(void)fflush(stdout);
	if (ChangeTrace) {
	    Tracing = Tracing ? FALSE : TRUE;
	    syslog(L_TRACE, "trace %sabled", Tracing ? "en" : "dis");
	    ChangeTrace = FALSE;
	}
	if (PushedBack) {
	    if (PushedBack[0] == '\0')
		continue;
	    if (Tracing)
		syslog(L_TRACE, "%s < %s", ClientHost, PushedBack);
	    ac = Argify(PushedBack, &av);
	}
	else
	    switch (r = READline(buff, (int)sizeof buff, timeout)) {
	    default:
		syslog(L_ERROR, "%s internal %d in main", ClientHost, r);
		/* FALLTHROUGH */
	    case RTtimeout:
		if (timeout < CLIENT_TIMEOUT)
		    syslog(L_NOTICE, "%s timeout short", ClientHost);
		else
		    syslog(L_NOTICE, "%s timeout", ClientHost);
		Printf("%d Timeout after %d seconds, closing connection.\r\n",
		       NNTP_TEMPERR_VAL, timeout);
		ExitWithStats(1);
		break;
	    case RTlong:
		Reply("%d Line too long\r\n", NNTP_BAD_COMMAND_VAL);
		continue;
	    case RTok:
		/* Do some input processing, check for blank line. */
		if (Tracing)
		    syslog(L_TRACE, "%s < %s", ClientHost, buff);
		if (buff[0] == '\0')
		    continue;
		ac = Argify(buff, &av);
		break;
	    case RTeof:
		/* Handled below. */
		break;
	    }

	/* Client gone? */
	if (r == RTeof)
	    break;
	if (ac == 0 || caseEQ(av[0], "quit"))
	    break;

	/* Find command. */
	for (cp = CMDtable; cp->Name; cp++)
	    if (caseEQ(cp->Name, av[0]))
		break;
	if (cp->Name == NULL) {
	    if ((int)strlen(buff) > 40)
		syslog(L_NOTICE, "%s unrecognized %.40s...", ClientHost, buff);
	    else
		syslog(L_NOTICE, "%s unrecognized %s", ClientHost, buff);
	    Reply("%d What?\r\n", NNTP_BAD_COMMAND_VAL);
	    continue;
	}

	/* Check usage. */
	if ((cp->Minac != CMDany && ac < cp->Minac)
	 || (cp->Maxac != CMDany && ac > cp->Maxac)) {
	    Reply("%d %s\r\n",
		NNTP_SYNTAX_VAL,  cp->Help ? cp->Help : "Usage error");
	    continue;
	}

	/* Check permissions and dispatch. */
	if (cp->Needauth && PERMneedauth) {
	    Reply("%d Authentication required for command\r\n",
		NNTP_AUTH_NEEDED_VAL);
	    continue;
	}
	TITLEset(av[0]);
	(*cp->Function)(ac, av);
    }

    Reply("%s\r\n", NNTP_GOODBYE_ACK);

    ExitWithStats(0);
    /* NOTREACHED */
}
