view src/server.c @ 388:8e14809bf172 noffle

[svn] * src/content.c: Don't be fooled into thinking a filename starting ".." is just "..".
author bears
date Thu, 22 May 2003 09:27:02 +0100
parents c02c4eb95f95
children f81fdcc2696b
line wrap: on
line source

/*
  server.c

  $Id: server.c 420 2003-01-10 23:25:45Z bears $
*/

#if HAVE_CONFIG_H
#include <config.h>
#endif

#if TIME_WITH_SYS_TIME
#include <sys/time.h>
#include <time.h>
#else
#if HAVE_SYS_TIME_H
#include <sys/time.h>
#else
#include <time.h>
#endif
#endif

#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/types.h>
#include <unistd.h>
#include "authenticate.h"
#include "client.h"
#include "common.h"
#include "configfile.h"
#include "content.h"
#include "database.h"
#include "dynamicstring.h"
#include "fetch.h"
#include "fetchlist.h"
#include "group.h"
#include "itemlist.h"
#include "lock.h"
#include "log.h"
#include "online.h"
#include "over.h"
#include "post.h"
#include "protocol.h"
#include "pseudo.h"
#include "request.h"
#include "server.h"
#include "util.h"
#include "wildmat.h"
#include "portable.h"

enum AuthState {
    NEED_USER,
    NEED_PASS,
    AUTH_DONE
};

struct
{
    Bool running;
    time_t lastServerOpen;
    int artPtr;
    Str grp; /* selected group, "" if none */
    DynStr *reply;
    Bool eotAfterReply;
    Bool groupReady;
    enum AuthState auth;
    char *user;
} server = {
    FALSE,		/* running */
    0L,			/* lastServerOpen */
    0,			/* artPtr */
    "",			/* grp */
    NULL,		/* reply */
    FALSE,		/* eotAfterReply */
    FALSE,		/* groupReady */
    NEED_USER,		/* auth */
    NULL		/* user */
};

typedef struct Cmd
{
    const char *name;
    const char *syntax;
    Bool needAuth;
    /* Returns false, if quit cmd */
    Bool (*cmdProc)( char *arg, const struct Cmd *cmd );
}
Cmd;

static Bool doArt( char *arg, const Cmd *cmd );
static Bool doAuthinfo( char *arg, const Cmd *cmd );
static Bool doBody( char *arg, const Cmd *cmd );
static Bool doGrp( char *arg, const Cmd *cmd );
static Bool doHead( char *arg, const Cmd *cmd );
static Bool doHelp( char *arg, const Cmd *cmd );
static Bool doIhave( char *arg, const Cmd *cmd );
static Bool doLast( char *arg, const Cmd *cmd );
static Bool doList( char *arg, const Cmd *cmd );
static Bool doListgrp( char *arg, const Cmd *cmd );
static Bool doMode( char *arg, const Cmd *cmd );
static Bool doNewgrps( char *arg, const Cmd *cmd );
static Bool doNext( char *arg, const Cmd *cmd );
static Bool doPost( char *arg, const Cmd *cmd );
static Bool doSlave( char *arg, const Cmd *cmd );
static Bool doStat( char *arg, const Cmd *cmd );
static Bool doQuit( char *arg, const Cmd *cmd );
static Bool doXhdr( char *arg, const Cmd *cmd );
static Bool doXpat( char *arg, const Cmd *cmd );
static Bool doXOver( char *arg, const Cmd *cmd );
static Bool notImplemented( char *arg, const Cmd *cmd );
static void putStat( unsigned int stat, const char *fmt, ... );

static void closeServer( void );
static Bool initServer( void );

Cmd commands[] =
{
    { "article", "ARTICLE [msg-id|n]", TRUE, &doArt },
    { "authinfo", "AUTHINFO USER username|PASS password", FALSE, &doAuthinfo },
    { "body", "BODY [msg-id|n]", TRUE, &doBody },
    { "head", "HEAD [msg-id|n]", TRUE, &doHead },
    { "group", "GROUP grp", TRUE, &doGrp },
    { "help", "HELP", FALSE, &doHelp },
    { "ihave", "IHAVE (ignored)", TRUE, &doIhave },
    { "last", "LAST", TRUE, &doLast },
    { "list", "LIST [ACTIVE [pat]]|ACTIVE.TIMES [pat]|"
      "EXTENSIONS|NEWSGROUPS [pat]|OVERVIEW.FMT", TRUE, &doList },
    { "listgroup", "LISTGROUP grp", TRUE, &doListgrp },
    { "mode", "MODE (ignored)", FALSE, &doMode },
    { "newgroups", "NEWGROUPS [xx]yymmdd hhmmss [GMT]", TRUE, &doNewgrps },
    { "newnews", "NEWNEWS (not implemented)", TRUE, &notImplemented },
    { "next", "NEXT", TRUE, &doNext },
    { "post", "POST", TRUE, &doPost },
    { "quit", "QUIT", FALSE, &doQuit },
    { "slave", "SLAVE (ignored)", TRUE, &doSlave },
    { "stat", "STAT [msg-id|n]", TRUE, &doStat },
    { "xhdr", "XHDR over-field [msg-id|m[-[n]]]", TRUE, &doXhdr },
    { "xpat", "XPAT over-field msg-id|m[-[n]] pat", TRUE, &doXpat },
    { "xover", "XOVER [m[-[n]]]", TRUE, &doXOver }
};

/*
  Notice interest in reading this group.
  Automatically subscribe if option set in config file.
*/
static void
noteInterest( void )
{
    FetchMode mode;

    Grp_setLastAccess( server.grp );
    if ( ! Grp_local ( server.grp ) && ! Online_true() )
    {
        Fetchlist_read();
        if ( ! Fetchlist_contains( server.grp, NULL ) )
	{
	    if ( Cfg_autoSubscribe() )
	    {
		if ( strcmp( Cfg_autoSubscribeMode( server.grp ), "full" ) == 0 )
		    mode = FULL;
		else if ( strcmp( Cfg_autoSubscribeMode( server.grp ), "thread" ) == 0 )
		    mode = THREAD;
		else
		    mode = OVER;
		Fetchlist_add( server.grp, mode );
		Fetchlist_write();
		Pseudo_autoSubscribed();
	    }
	    else if ( Cfg_infoAlways() )
	    {
		int first, last;

		/* Ensure new gen info for next time */
		first = Cont_first();
		last = Cont_last();

		if ( first == last )
		    first = last + 1;
		
		Grp_setFirstLast( Cont_grp(), first, last );
	    }
	}
    }
}

static void
putStat( unsigned int stat, const char *fmt, ... )
{
    Str s, line;
    va_list ap;

    ASSERT( stat <= 999 );
    va_start( ap, fmt );
    vsnprintf( s, MAXCHAR, fmt, ap );
    va_end( ap );
    snprintf( line, MAXCHAR, "%u %s", stat, s );
    DynStr_appLn( server.reply, line );    
    Log_dbg( LOG_DBG_PROTOCOL, "[S] %s", line );
}

static void
putTxtLn( const char *fmt, ... )
{
    Str line;
    va_list ap;

    va_start( ap, fmt );
    vsnprintf( line, MAXCHAR, fmt, ap );
    va_end( ap );
    DynStr_appLn( server.reply, line );
}

static void
putTxtBuf( const char *buf )
{
    if ( buf )
	DynStr_app( server.reply, buf );
}

static void
putEndOfTxt( void )
{
    server.eotAfterReply = TRUE;
}

static void
putSyntax( const Cmd *cmd )
{
    putStat( STAT_SYNTAX_ERR, "Syntax error. Usage: %s", cmd->syntax );
}

static void
initOutput( void )
{
    server.reply = new_DynStr( 100 );
    server.eotAfterReply = FALSE;
}

static void
sendOutput( void )
{
    Prt_putTxtBuf( DynStr_str( server.reply ), stdout );
    if ( server.eotAfterReply )
	Prt_putEndOfTxt( stdout );
    fflush( stdout );
    Log_dbg( LOG_DBG_PROTOCOL, "[S FLUSH]" );
    del_DynStr( server.reply );
}

static Bool
getTxtLn( Str line, Bool *err )
{
    return Prt_getTxtLn( line, err, stdin, -1 );
}

static Bool
notImplemented( char *arg, const Cmd *cmd )
{
    UNUSED( arg );
    UNUSED( cmd );
    
    putStat( STAT_NO_PERMISSION, "Command not implemented" );
    return TRUE;
}

static void
checkNewArts( const char *grp )
{
    if ( ! Online_true()
         || strcmp( grp, server.grp ) == 0
         || Grp_local( grp )
         || time( NULL ) - Grp_lastAccess( server.grp ) < 1800 )
        return;
    if ( Fetch_init( Grp_server( grp ) ) )
    {
	FetchMode mode;

	/*
	 * See if group is on the fetchlist and if so use that mode.
	 * That way we preserve the download semantics for groups on
	 * the fetchlist. If the group is not on the fetchlist use
	 * OVER to get just the overview info.
	 */
	Fetchlist_read();
	if ( ! Fetchlist_contains( grp, &mode ) )
	    mode = OVER;
	
        Fetch_getNewArts( grp, mode );
        Fetch_close();
    }
    else
    {
        Log_inf( "Connection to server failed. Leaving online mode." );        
        Online_set( FALSE );
    }
}

static void
postArts( void )
{
    Str s;

    Cfg_beginServEnum();
    while ( Cfg_nextServ( s ) )
        if ( Fetch_init( s ) )
        {
            Fetch_postArts();
            Fetch_close();
        }
}

static Bool
needsPseudoGenInfo( const char *grp )
{
    return ! ( Grp_local( grp )
	       || Fetchlist_contains( grp, NULL )
	       || Online_true() );
}

static void
readCont( const char *name )
{
    Fetchlist_read();
    Cont_read( name );
    if ( needsPseudoGenInfo( name ) )
    { 
        Pseudo_appGeneralInfo();
	/* This adds the pseudo message to the overview contents
	   but NOT to the group info. If an article gets added,
	   the group info will get updated then from the
	   contents, so the article number will be preserved.
	   Otherwise it will be lost when the content is discarded. */
    }
}

static void
loadGrpInfo( const char *grp )
{
    checkNewArts( grp );
    Utl_cpyStr( server.grp, grp );
    readCont( grp );

    /*
     * This routine is used to change back to a group after releasing
     * the lock. We need to preserve the group cursor if at all possible.
     * So, if the article pointer points to an article before or after
     * the current range, adjust it to the first/last article. Otherwise
     * leave it well alone.
     */
    if ( server.artPtr < Cont_first() )
	server.artPtr = Cont_first();
    else if ( server.artPtr > Cont_last() )
	server.artPtr = Cont_last();
    
    server.groupReady = TRUE;
}

static void
changeToGrp( const char *grp )
{
    loadGrpInfo( grp );
    server.artPtr = Cont_first();
}

static Bool
doGrp( char *arg, const Cmd *cmd )
{
    int first, last, numb;

    if ( arg[ 0 ] == '\0' )
        putSyntax( cmd );
    else if ( ! Grp_exists( arg ) )
        putStat( STAT_NO_SUCH_GRP, "No such group" );
    else
    {
	Utl_cpyStr( server.grp, arg );
	server.groupReady = FALSE;
	server.artPtr = Grp_first( arg );
	first = server.artPtr;
	last = Grp_last( arg );
	
	if ( ( first == 0 && last == 0 )
	     || first > last ) {
		changeToGrp( arg );
		first = Cont_first();
		last = Cont_last();
	}
	
	if ( ( first == 0 && last == 0 )
	     || first > last )
            first = last = numb = 0;
	else
	    numb = last - first + 1;
        putStat( STAT_GRP_SELECTED, "%lu %lu %lu %s selected",
                 numb, first, last, arg );
    }
    return TRUE;
}

static Bool
loadGrpIfSelected( void )
{
    Str group;
    if ( *server.grp == '\0' )
    {
        putStat( STAT_NO_GRP_SELECTED, "No group selected" );
        return FALSE;
    }
    if ( ! server.groupReady )
    {
    	Utl_cpyStr( group, server.grp );
    	loadGrpInfo( group );
    }
    return TRUE;
}

static void
findServer( const char *msgId, Str result )
{
    const char *p, *pColon, *srv;
    Str s, grp;

    Utl_cpyStr( result, "(unknown)" );
    if ( Db_contains( msgId ) )
    {
        Utl_cpyStr( s, Db_xref( msgId ) );
        p = strtok( s, " \t" );
        if ( p )
            do
            {
                pColon = strstr( p, ":" );
                if ( pColon )
                {
                    Utl_cpyStrN( grp, p, pColon - p );
                    srv = Grp_server( grp );
                    if ( Cfg_servIsPreferential( srv, result ) )
                        Utl_cpyStr( result, srv );
                }
            }
            while ( ( p = strtok( NULL, " \t" ) ) );
    }
}

static Bool
retrieveArt( const char *msgId )
{
    Str s;
    int stat;

    findServer( msgId, s );    
    if ( strcmp( s, "(unknown)" ) == 0 
         || strcmp( s, GRP_LOCAL_SERVER_NAME ) == 0 )
        return FALSE;        
    if ( ! Client_connect( s ) )
    {
        Log_inf( "Connection to server failed. Leaving online mode." );        
        Online_set( FALSE );
        return FALSE;
    }
    stat = Client_retrieveArt( msgId );
    Client_disconnect();
    if ( IS_FATAL( stat ) )
    {
        Log_inf( "Server connection failed or newsbase problem. "
		 "Leaving online mode." );        
        Online_set( FALSE );
        return FALSE;
    }
    return TRUE;
}

static Bool
checkNumb( int numb )
{
    if ( ! loadGrpIfSelected() )
        return FALSE;
    if ( ! Cont_validNumb( numb ) )
    {
        putStat( STAT_NO_SUCH_NUMB, "No such article number" );
        return FALSE;
    }
    return TRUE;
}

/*
  Parse arguments for ARTICLE, BODY, HEAD, STAT commands.
  Return message-ID and article number (0 if unknown).
*/
static Bool
whichId( const char **msgId, int *numb, char *arg )
{
    const Over *ov;
    int n;

    if ( sscanf( arg, "%d", &n ) == 1 )
    {
        if ( ! checkNumb( n ) )
            return FALSE;
        server.artPtr = n;
        ov = Cont_get( n );
        *msgId = Ov_msgId( ov );
        *numb = n;
    }
    else if ( strcmp( arg, "" ) == 0 )
    {
        if ( ! checkNumb( server.artPtr ) )
            return FALSE;
        ov = Cont_get( server.artPtr );
        *msgId = Ov_msgId( ov );
        *numb =  server.artPtr;
    }
    else
    {
        *msgId = arg;
        *numb = 0;
    }
    if ( ! Pseudo_isGeneralInfo( *msgId ) && ! Db_contains( *msgId ) )
    {
        putStat( STAT_NO_SUCH_ID, "No such article" );
        return FALSE;
    }
    return TRUE;
}

static void
touchArticle( const char *msgId )
{
    unsigned status = Db_status( msgId );
    status |= DB_INTERESTING;
    Db_setStatus( msgId, status );
    Db_updateLastAccess( msgId );
}

static void
touchReferences( const char *msgId )
{
    Str s;
    int len;
    char *p;
    const char *ref = Db_ref( msgId );

    while ( TRUE )
    {
        p = s;
        while ( *ref != '<' )
            if ( *(ref++) == '\0' )
                return;
        len = 0;
        while ( *ref != '>' )
        {
            if ( *ref == '\0' || ++len >= MAXCHAR - 1 )
                return;
            *(p++) = *(ref++);
        }
        *(p++) = '>';
        *p = '\0';
        if ( Db_contains( s ) )
            touchArticle( s );
    }
}

static void
updateArt( const char *msgId )
/* retrieve Article if in online mode */
{
    unsigned status;

    touchArticle( msgId );
    touchReferences( msgId );
    status = Db_status( msgId );
    if ( Online_true() && ( status & DB_NOT_DOWNLOADED ) )
    {
        retrieveArt( msgId );
    }
}

static void
doBodyInDb( const char *msgId )
{
    unsigned status;
    Str srv;

    status = Db_status( msgId );
    if ( status & DB_RETRIEVING_FAILED )
    {
        Db_setStatus( msgId, status & ~DB_RETRIEVING_FAILED );
        putTxtBuf( Db_body( msgId ) );
    }
    else if ( status & DB_NOT_DOWNLOADED )
    {
        findServer( msgId, srv );
        if ( Req_contains( srv, msgId ) )
            putTxtBuf( Pseudo_alreadyMarkedBody() );
        else if ( strcmp( srv, "(unknown)" ) != 0 && 
		  strcmp( srv, GRP_LOCAL_SERVER_NAME ) != 0 && 
		  Req_add( srv, msgId ) )
            putTxtBuf( Pseudo_markedBody() );
        else
            putTxtBuf( Pseudo_markingFailedBody() );
    }
    else
        putTxtBuf( Db_body( msgId ) );
}

static Bool
doBody( char *arg, const Cmd *cmd )
{
    const char *msgId;
    int numb;

    UNUSED( cmd );
    
    if ( ! whichId( &msgId, &numb, arg ) )
        return TRUE;
    putStat( STAT_BODY_FOLLOWS, "%ld %s Body", numb, msgId );
    if ( Pseudo_isGeneralInfo( msgId ) )
        putTxtBuf( Pseudo_generalInfoBody() );
    else 
    {
        updateArt( msgId ); 
        doBodyInDb( msgId );
    }
    putEndOfTxt();
    noteInterest();
    return TRUE;
}

static void
doHeadInDb( const char *msgId )
{
    putTxtBuf( Db_header( msgId ) );
}

static Bool
doHead( char *arg, const Cmd *cmd )
{
    const char *msgId;
    int numb;

    UNUSED( cmd );
    
    if ( ! whichId( &msgId, &numb, arg ) )
        return TRUE;
    putStat( STAT_HEAD_FOLLOWS, "%ld %s Head", numb, msgId );
    if ( Pseudo_isGeneralInfo( msgId ) )
        putTxtBuf( Pseudo_generalInfoHead() );
    else
        doHeadInDb( msgId );
    putEndOfTxt();
    return TRUE;
}

static void
doArtInDb( const char *msgId )
{
    updateArt( msgId ); 
    doHeadInDb( msgId );
    putTxtLn( "" );
    doBodyInDb( msgId );
}

static Bool
doArt( char *arg, const Cmd *cmd )
{
    const char *msgId;
    int numb;
    
    UNUSED( cmd );
    
    if ( ! whichId( &msgId, &numb, arg ) )
        return TRUE;
    putStat( STAT_ART_FOLLOWS, "%ld %s Article", numb, msgId );
    if ( Pseudo_isGeneralInfo( msgId ) )
    {
        putTxtBuf( Pseudo_generalInfoHead() );
        putTxtLn( "" );
        putTxtBuf( Pseudo_generalInfoBody() );
    }
    else
        doArtInDb( msgId );
    putEndOfTxt();
    noteInterest();
    return TRUE;
}

static Bool
doAuthinfo( char *line, const Cmd *cmd )
{
#if USE_AUTH
    Str s, arg;
    const char *data;

    if ( ! Cfg_needClientAuth() )
    {
	putStat( STAT_AUTH_REJECTED, "Authentication not required" );
	return TRUE;
    }

    if ( sscanf( line, MAXCHAR_FMT, s ) != 1 )
    {
    badsyntax:	
	if ( server.auth != AUTH_DONE )
	    server.auth = NEED_USER;
	putSyntax( cmd );
    }
    else
    {
	Utl_toLower( s );
	Utl_cpyStr( arg, Utl_restOfLn( line, 1 ) );
	data = Utl_stripWhiteSpace( arg );

	if ( strcmp( "user", s ) != 0 && strcmp( "pass", s ) != 0 )
	    goto badsyntax;
	
	if ( strcmp( "user", s ) == 0 && server.auth == NEED_USER )
	{
	    if ( server.user != NULL )
		free( server.user );
	    Utl_allocAndCpy( &server.user, data );
	    server.auth = NEED_PASS;
	    putStat( STAT_MORE_AUTH_REQUIRED, "More authentication required" );
	}
	else if ( strcmp( "pass", s ) == 0 && server.auth == NEED_PASS )
	{
	    enum AuthResult authRes = Auth_authenticate( server.user, data );
	    char *p;

	    /* Zap the password */
	    for ( p = line; *p != '\0'; p++ )
		*p = 'X';
	    
	    switch ( authRes )
	    {
	    case AUTH_OK:
		server.auth = AUTH_DONE;
		putStat( STAT_AUTH_ACCEPTED, "Authentication accepted" );
		Log_inf( "User %s authenticated", server.user );
		break;

	    case AUTH_FAILED:
		server.auth = NEED_USER;
		putStat( STAT_NO_PERMISSION, "No permission");
		Log_dbg( LOG_DBG_AUTH, "User %s password %s rejected",
			 server.user, data );
		break;

	    case AUTH_DISCONNECT:
		putStat( STAT_NO_PERMISSION, "No permission - disconnecting" );
		Log_dbg( LOG_DBG_AUTH,
			 "User %s password %s rejected - disconnecting",
			 server.user, data );
		return FALSE;
		
	    default:	/* AUTH_ERROR */
		putStat( STAT_PROGRAM_FAULT, "Authentication program error" );
		Log_dbg( LOG_DBG_AUTH,
			 "Error authenticating User %s password %s",
			 server.user, data );
		return FALSE;
	    }
	}
	else
	{
	    if ( server.auth == AUTH_DONE )
		putStat( STAT_AUTH_REJECTED, "Reauthentication not possible" );
	    else
	    {
		putStat( STAT_AUTH_REJECTED, "Authentication rejected" );
		server.auth = NEED_USER;
	    }
	}
    }
#else
    UNUSED( line );
    UNUSED( cmd );
    
    putStat( STAT_AUTH_REJECTED, "Authentication not possible" );
#endif    
    return TRUE;
}

static Bool
doHelp( char *arg, const Cmd *cmd )
{
    unsigned int i;

    UNUSED( arg );
    UNUSED( cmd );

    putStat( STAT_HELP_FOLLOWS, "Help" );
    putTxtBuf( "\nCommands:\n\n" );
    for ( i = 0; i < sizeof( commands ) / sizeof( commands[ 0 ] ); ++i )
        putTxtLn( "%s", commands[ i ].syntax );
    putEndOfTxt();
    return TRUE;
}

static Bool
doIhave( char *arg, const Cmd *cmd )
{
    UNUSED( arg );
    UNUSED( cmd );
    
    putStat( STAT_ART_REJECTED, "Command not used" );
    return TRUE;
}

static Bool
doLast( char *arg, const Cmd *cmd )
{
    int n;

    UNUSED( arg );
    UNUSED( cmd );

    if ( loadGrpIfSelected() )
    {
        n = server.artPtr;
        if ( ! Cont_validNumb( n ) )
            putStat( STAT_NO_ART_SELECTED, "No article selected" );
        else
        {
            while ( ! Cont_validNumb( --n ) && n >= Cont_first() );
            if ( ! Cont_validNumb( n ) )
                putStat( STAT_NO_PREV_ART, "No previous article" );
            else
            {
                putStat( STAT_ART_RETRIEVED, "%ld %s selected",
                         n, Ov_msgId( Cont_get( n ) ) );
                server.artPtr = n;
            }
        }
    }
    return TRUE;
}

static Bool
containsWildcards( const char *pattern )
{
    return ( strpbrk( pattern, "?*[\\" ) == NULL ? FALSE : TRUE );
}

static void
printGroups( const char *pat, void (*printProc)( Str, const char* ) )
{
    Str line;
    const char *g;

    putStat( STAT_GRPS_FOLLOW, "Groups" );
    if ( containsWildcards( pat ) )
    {
	if ( Grp_firstGrp( &g ) )
	    do
		if ( Wld_match( g, pat ) )
		{
		    (*printProc)( line, g );
		    putTxtLn( line );
		}
	    while ( Grp_nextGrp( &g ) );
    }
    else if ( Grp_exists( pat ) )
    {
        (*printProc)( line, pat );
	putTxtLn( line );
    }
    putEndOfTxt();
}

static void
printActiveTimes( Str result, const char *grp )
{
    snprintf( result, MAXCHAR, "%s %ld", grp, Grp_created( grp ) );
}

static void
doListActiveTimes( const char *pat )
{
    printGroups( pat, &printActiveTimes );
}

static void
printActive( Str result, const char *grp )
{
    int last;
    
    /* If there will be a pseudo gen info message waiting when we join
       this group, fiddle the group numbers to show it. */
    last = Grp_last( grp );
    if ( needsPseudoGenInfo( grp ) )
	last++;
    
    snprintf( result, MAXCHAR, "%s %d %d %c",
              grp, last, Grp_first( grp ), Grp_postAllow( grp ) );
}

static void
doListActive( const char *pat )
{
    /* We need to read the fetchlist so we know whether a pseudo
       gen info article needs to be faked when printing the group
       last. */
    Fetchlist_read();
    printGroups( pat, &printActive );
}

static void
printNewsgrp( Str result, const char *grp )
{
    snprintf( result, MAXCHAR, "%s %s", grp, Grp_dsc( grp ) );
}

static void
doListNewsgrps( const char *pat )
{
    printGroups( pat, &printNewsgrp );
}

static void
putGrp( const char *name )
{
    putTxtLn( "%s %lu %lu y", name, Grp_last( name ), Grp_first( name ) );
}

static void
doListOverFmt( void )
{
    putStat( STAT_GRPS_FOLLOW, "Overview format" );
    putTxtBuf( "Subject:\n"
               "From:\n"
               "Date:\n"
               "Message-ID:\n"
               "References:\n"
               "Bytes:\n"
               "Lines:\n" );
    putEndOfTxt();
}

static void
doListExtensions( void )
{
    putStat( STAT_CMD_OK, "Extensions" );
    putTxtBuf( " LISTGROUP\n"
               " XOVER\n" );
    putEndOfTxt();    
}

static Bool
doList( char *line, const Cmd *cmd )
{
    Str s, arg;
    const char *pat;

    if ( sscanf( line, MAXCHAR_FMT, s ) != 1 )
        doListActive( "*" );
    else
    {
        Utl_toLower( s );
        Utl_cpyStr( arg, Utl_restOfLn( line, 1 ) );
        pat = Utl_stripWhiteSpace( arg );
        if ( pat[ 0 ] == '\0' )
            pat = "*";
        if ( strcmp( "active", s ) == 0 )
            doListActive( pat );
        else if ( strcmp( "overview.fmt", s ) == 0 )
            doListOverFmt();
        else if ( strcmp( "newsgroups", s ) == 0 )
            doListNewsgrps( pat );
        else if ( strcmp( "active.times", s ) == 0 )
            doListActiveTimes( pat );
        else if ( strcmp( "extensions", s ) == 0 )
            doListExtensions();
        else
            putSyntax( cmd );
    }
    return TRUE;
}

static Bool
doListgrp( char *arg, const Cmd *cmd )
{
    int first, last, i;

    UNUSED( cmd );

    if ( *arg != '\0' )
    {
	if ( ! Grp_exists( arg ) )
	{
	    putStat( STAT_NO_SUCH_GRP, "No such group" );
	    return TRUE;
	}
	changeToGrp( arg );
    }
    else
    {
	if ( ! loadGrpIfSelected() )
	    return TRUE;
	
    }
    
    first = Cont_first();
    last = Cont_last();
    putStat( STAT_GRP_SELECTED, "Article list" );
    for ( i = first; i <= last; ++i )
	if ( Cont_get( i ) != NULL )
	    putTxtLn( "%d", i );
    putEndOfTxt();
    return TRUE;
}

static Bool
doMode( char *arg, const Cmd *cmd )
{
    UNUSED( arg );
    UNUSED( cmd );
    
    putStat( STAT_READY_POST_ALLOW, "Ok" );
    return TRUE;
}

/*
 * Given a time specification (local time or GMT), return a time_t.
 */
static time_t
getTimeInSeconds( int year, int mon, int day,
		  int hour, int min, int sec,
		  Bool timeIsGMT )
{
    struct tm t;
    time_t result;

    ASSERT( year >= 1900 );
    ASSERT( mon >= 1 );
    ASSERT( mon <= 12 );
    ASSERT( day >= 1 );
    ASSERT( day <= 31 );
    ASSERT( hour >= 0 );
    ASSERT( hour <= 23 );
    ASSERT( min >= 0 );
    ASSERT( min <= 59 );
    ASSERT( sec >= 0 );
    ASSERT( sec <= 59 );
    memset( &t, 0, sizeof( t ) );
    t.tm_year = year - 1900;
    t.tm_mon = mon - 1;
    t.tm_mday = day;
    t.tm_hour = hour;
    t.tm_min = min;
    t.tm_sec = sec;
    t.tm_isdst = -1;
    result = timeIsGMT ? Utl_mktimeGMT( &t ) : mktime( &t );
    return result;
}


static Bool
doNewgrps( char *arg, const Cmd *cmd )
{
    time_t t, now, lastUpdate;
    int year, mon, day, hour, min, sec, len, fields, nowYear;
    const char *g;
    Str date, timeofday, file, utc;
    Bool timeIsGMT = FALSE;
    struct tm *tm;

    fields = sscanf( arg, MAXCHAR_FMT " " MAXCHAR_FMT " " MAXCHAR_FMT,
                     date, timeofday, utc );
    if ( fields == 3 )
    {
        Utl_toLower( utc );
        if ( strcmp( utc, "gmt" ) != 0 && strcmp( utc, "utc" ) != 0 )
        {
            putSyntax( cmd );
            return TRUE;
        }
        timeIsGMT = TRUE;
    }
    else if ( fields != 2 ) 
    {
        putSyntax( cmd );
        return TRUE;
    }
    len = strlen( date );
    switch ( len )
    {
    case 6:
        if ( sscanf( date, "%2d%2d%2d", &year, &mon, &day ) != 3 )
        {
            putSyntax( cmd );
            return TRUE;
        }
	/*
	 * As per current IETF draft, year is this century if <= now,
	 * else last century.
	 */
	now = time( NULL );
	tm = timeIsGMT ? gmtime( &now ) : localtime( &now );
	nowYear = tm->tm_year + 1900;
	year += ( nowYear / 100 ) * 100;
	if ( year % 100 > nowYear % 100 )
	    year -= 100;
        break;
    case 8:
        if ( sscanf( date, "%4d%2d%2d", &year, &mon, &day ) != 3 )
        {
            putSyntax( cmd );
            return TRUE;
        }
        break;
    default:
        putSyntax( cmd );
        return TRUE;
    }
    if ( sscanf( timeofday, "%2d%2d%2d", &hour, &min, &sec ) != 3 )
    {
        putSyntax( cmd );
        return TRUE;
    }
    if ( year < 1970 || mon < 1 || mon > 12 || day < 1 || day > 31
         || hour < 0 || hour > 23 || min < 0 || min > 59
         || sec < 0 || sec > 60 )
    {
        putSyntax( cmd );
        return TRUE;
    }
    snprintf( file, MAXCHAR, "%s/groupinfo.lastupdate", Cfg_spoolDir() );
    t = getTimeInSeconds( year, mon, day, hour, min, sec, timeIsGMT );

    if ( ! Utl_getStamp( &lastUpdate, file ) )
    {
        /* Can't get stamp. Put out error message. */
        putStat( STAT_PROGRAM_FAULT, "Server error reading %s", file );        
        return TRUE;
    }

    /* Show timestamp back as news format time for confirmation. */
    Utl_newsDate( t, timeofday );
    
    putStat( STAT_NEW_GRP_FOLLOW, "New groups since %s", timeofday );

    if ( t == (time_t)-1 || t <= lastUpdate )
    {
        if ( Grp_firstGrp( &g ) )
            do
                if ( Grp_created( g ) > t )
                    putGrp( g );
            while ( Grp_nextGrp( &g ) );
    }
    putEndOfTxt();
    return TRUE;
}

static Bool
doNext( char *arg, const Cmd *cmd )
{
    int n;

    UNUSED(arg);
    UNUSED(cmd);

    if ( loadGrpIfSelected() )
    {
        n = server.artPtr;
        if ( ! Cont_validNumb( n ) )
            putStat( STAT_NO_ART_SELECTED, "No article selected" );
        else
        {
            while ( ! Cont_validNumb( ++n ) && n <= Cont_last() );
            if ( ! Cont_validNumb( n ) )
                putStat( STAT_NO_NEXT_ART, "No next article" );
            else
            {
                putStat( STAT_ART_RETRIEVED, "%ld %s selected",
                         n, Ov_msgId( Cont_get( n ) ) );
                server.artPtr = n;
            }
        }
    }
    return TRUE;
}

static Bool
doPost( char *arg, const Cmd *cmd )
{
    DynStr *s;
    Str line;
    Bool err;

    UNUSED(arg);
    UNUSED(cmd);
    
    /*
     * The article may take some time coming in, so release the
     * lock while collecting it.
     */
    putStat( STAT_SEND_ART, "Continue (end with period)" );
    sendOutput();
    closeServer();
    
    s = new_DynStr( 10000 );
    err = FALSE;
    while ( ! err && getTxtLn( line, &err ) )
	DynStr_appLn( s, line );

    initOutput();
    if ( ! initServer() )
    {
	del_DynStr( s );
	return FALSE;
    }

    if ( ! err
	 && Post_open( DynStr_str( s ), 0 )
	 && Post_post() )
    {
        putStat( STAT_POST_OK, "Message posted" );
        if ( Online_true() )
            postArts();
    }
    else
        putStat( STAT_POST_FAILED, "Posting failed" );
    Post_close();
    del_DynStr( s );
    return TRUE;
}

static void
parseRange( const char *s, int *first, int *last, int *numb )
{
    int r, i;
    char* p;
    Str t;

    Utl_cpyStr( t, s );
    p = Utl_stripWhiteSpace( t );
    r = sscanf( p, "%d-%d", first, last );
    if ( r < 1 )
    {
        *first = server.artPtr;
        *last = server.artPtr;
    }
    else if ( r == 1 )
    {
        if ( p[ strlen( p ) - 1 ] == '-' )
            *last = Cont_last();
        else
            *last = *first;
    }    
    if ( *first < Cont_first() )
        *first = Cont_first();
    if ( *last > Cont_last() )
        *last = Cont_last();
    if ( *first > Cont_last() ||  *last < Cont_first() )
        *last = *first - 1;
    *numb = 0;
    for ( i = *first; i <= *last; ++i )
        if ( Cont_validNumb( i ) )
            ++(*numb);
}

enum XhdrType { SUBJ, FROM, DATE, MSG_ID, REF, BYTES, LINES, XREF, UNKNOWN };

static enum XhdrType
whatXhdrField( const char * fieldname )
{
    Str name;

    Utl_cpyStr( name, fieldname );
    Utl_toLower( name );
    if ( strcmp( name, "subject" ) == 0 )
        return SUBJ;
    else if ( strcmp( name, "from" ) == 0 )
        return FROM;
    else if ( strcmp( name, "date" ) == 0 )
        return DATE;
    else if ( strcmp( name, "message-id" ) == 0 )
        return MSG_ID;
    else if ( strcmp( name, "references" ) == 0 )
        return REF;
    else if ( strcmp( name, "bytes" ) == 0 )
        return BYTES;
    else if ( strcmp( name, "lines" ) == 0 )
        return LINES;
    else if ( strcmp( name, "xref" ) == 0 )
        return XREF;
    else
	return UNKNOWN;
}

static void
getXhdrField( enum XhdrType what, const Over * ov, Str res )
{
    const char * msgId;
    Str host;
    
    switch ( what )
    {
    case SUBJ:
	Utl_cpyStr( res, Ov_subj( ov ) );
	break;
    case FROM:
	Utl_cpyStr( res, Ov_from( ov ) );
	break;
    case DATE:
	Utl_cpyStr( res, Ov_date( ov ) );
	break;
    case MSG_ID:
	Utl_cpyStr( res, Ov_msgId( ov ) );
	break;
    case REF:
	Utl_cpyStr( res, Ov_ref( ov ) );
	break;
    case BYTES:
	snprintf( res, MAXCHAR, "%ld", Ov_bytes( ov ) );
	break;
    case LINES:
	snprintf( res, MAXCHAR, "%ld", Ov_lines( ov ) );
	break;
    case XREF:
	msgId = Ov_msgId( ov );
	/*
	 * Gen info messages don't have an Xref header. When INN is asked
	 * for a header that doesn't exist in a message, it reports the
	 * header value as '(none)', so do the same.
	 */
	if ( Pseudo_isGeneralInfo( msgId ) )
	    Utl_cpyStr( res, "none" );
	else
	{
	    gethostname( host, MAXCHAR );
	    snprintf( res, MAXCHAR, "%s %s", host, Db_xref( msgId ) );
	}
	break;
    default:
	ASSERT( FALSE );
    }
}

/*
  Note this only handles a subset of headers. But they are all
  the headers any newsreader should need to work properly.
 
  That last sentence will at some stage be proved wrong.
 */
static Bool
doXhdr( char *arg, const Cmd *cmd )
{
    enum XhdrType what;
    const char *p;
    Str whatStr;

    if ( sscanf( arg, MAXCHAR_FMT, whatStr ) != 1 )
    {
        putSyntax( cmd );
        return TRUE;
    }
    what = whatXhdrField( whatStr );
    if ( what == UNKNOWN )
    {
        putStat( STAT_HEAD_FOLLOWS, "Unknown header (empty list follows)" );
        putEndOfTxt();
        return TRUE;
    }
    p = Utl_restOfLn( arg, 1 );
    if ( p[ 0 ] == '<' )
    {
	Over * ov;
	Str field;
	
	/* Argument is message ID */
	ov = Db_over( p );
	if ( ov == NULL )
	{
	    putStat( STAT_NO_SUCH_ID, "No such article" );
	    return TRUE;
	}
        putStat( STAT_HEAD_FOLLOWS, "%s header %s", whatStr, p ) ;
	getXhdrField( what, ov, field );
	putTxtLn( "%s %s", p, field );
	del_Over( ov );
    }
    else
    {
	const Over * ov;
	int first, last, i, n, numb;
	Str field;
	
	/* Argument is article no. or range */
	if ( ! loadGrpIfSelected() )
	    return TRUE;
	parseRange( p, &first, &last, &numb );
	if ( numb == 0 )
	{
	    putStat( STAT_NO_ART_SELECTED, "No articles selected" );
	    return TRUE;
	}
        putStat( STAT_HEAD_FOLLOWS, "%s header %lu-%lu",
		 whatStr, first, last ) ;
        for ( i = first; i <= last; ++i )
            if ( ( ov = Cont_get( i ) ) )
            {
                n = Ov_numb( ov );
		getXhdrField( what, ov, field );
		putTxtLn( "%lu %s", n, field );
            }
    }
    putEndOfTxt();
    return TRUE;
}

static Bool
doXpat( char *arg, const Cmd *cmd )
{
    enum XhdrType what;
    Str whatStr, articles, pat;

    if ( sscanf( arg, MAXCHAR_FMT " " MAXCHAR_FMT " " MAXCHAR_FMT,
                 whatStr, articles, pat ) != 3 )
    {
	putSyntax( cmd );
	return TRUE;
    }
    what = whatXhdrField( whatStr );
    if ( what == UNKNOWN )
    {
        putStat( STAT_HEAD_FOLLOWS, "Unknown header (empty list follows)" );
        putEndOfTxt();
        return TRUE;
    }
    if ( articles[ 0 ] == '<' )
    {
	Over * ov;
	Str field;
	
	/* Argument is message ID */
	ov = Db_over( articles );
	if ( ov == NULL )
	{
	    putStat( STAT_NO_SUCH_ID, "No such article" );
	    return TRUE;
	}
        putStat( STAT_HEAD_FOLLOWS, "%s header %s", whatStr, articles ) ;
	getXhdrField( what, ov, field );
	if ( Wld_match( field, pat ) )
	    putTxtLn( "%s %s", articles, field );
	del_Over( ov );
    }
    else
    {
	const Over * ov;
	Str field;
	int first, last, i, n, numb;

	/* Argument is article no. or range */
	if ( ! loadGrpIfSelected() )
	    return TRUE;
	parseRange( articles, &first, &last, &numb );
	if ( numb == 0 )
	{
	    putStat( STAT_NO_ART_SELECTED, "No articles selected" );
	    return TRUE;
	}
        putStat( STAT_HEAD_FOLLOWS, "%s header %lu-%lu",
		 whatStr, first, last ) ;
        for ( i = first; i <= last; ++i )
            if ( ( ov = Cont_get( i ) ) )
            {
                n = Ov_numb( ov );
		getXhdrField( what, ov, field );
		if ( Wld_match( field, pat ) )
		    putTxtLn( "%lu %s", n, field );
            }
    }
    putEndOfTxt();
    return TRUE;
}

static Bool
doSlave( char *arg, const Cmd *cmd )
{
    UNUSED( arg );
    UNUSED( cmd );
    
    putStat( STAT_CMD_OK, "Ok" );
    return TRUE;
}

static Bool
doStat( char *arg, const Cmd *cmd )
{
    const char *msgId;
    int numb;

    UNUSED( cmd );
    
    if ( ! whichId( &msgId, &numb, arg ) )
        return TRUE;
    if ( numb > 0 )
        putStat( STAT_ART_RETRIEVED, "%ld %s selected",
                 numb, msgId );
    else
        putStat( STAT_ART_RETRIEVED, "0 %s selected", msgId );
    return TRUE;
}

static Bool
doQuit( char *arg, const Cmd *cmd )
{
    UNUSED( arg );
    UNUSED( cmd );
    
    putStat( STAT_GOODBYE, "Goodbye" );
    return FALSE;
}

static Bool
doXOver( char *arg, const Cmd *cmd )
{
    int first, last, i, n;
    const Over *ov;

    UNUSED( cmd );
    
    if ( ! loadGrpIfSelected() )
        return TRUE;
    parseRange( arg, &first, &last, &n );
    if ( n == 0 )
	first = last = server.artPtr;
    putStat( STAT_OVERS_FOLLOW, "Overview %ld-%ld", first, last );
    for ( i = first; i <= last; ++i )
	if ( ( ov = Cont_get( i ) ) )
	    putTxtLn( "%lu\t%s\t%s\t%s\t%s\t%s\t%d\t%d\t",
		      Ov_numb( ov ), Ov_subj( ov ), Ov_from( ov ),
		      Ov_date( ov ), Ov_msgId( ov ), Ov_ref( ov ),
		      Ov_bytes( ov ), Ov_lines( ov ) );
    putEndOfTxt();
    Grp_setLastAccess( server.grp );
    return TRUE;
}

static void
putFatal( const char *fmt, ... )
{
    va_list ap;
    Str s;

    va_start( ap, fmt );
    vsnprintf( s, MAXCHAR, fmt, ap );
    va_end( ap );
    Log_err( s );
    putStat( STAT_PROGRAM_FAULT, "%s", s );
}

/* Parse line, execute command and return FALSE, if it was the quit command. */
static Bool
parseAndExecute( Str line, Bool authDone )
{
    unsigned int i, n;
    Cmd *c;
    Str s, arg;
    Bool ret;

    if ( sscanf( line, MAXCHAR_FMT, s ) == 1 )
    {
        Utl_toLower( s );
        Utl_cpyStr( arg, Utl_restOfLn( line, 1 ) );
        n = sizeof( commands ) / sizeof( commands[ 0 ] );
        for ( i = 0, c = commands; i < n; ++i, ++c )
            if ( strcmp( c->name, s ) == 0 )
            {
#if USE_AUTH
		if ( c->needAuth && ! authDone )
		{
		    putStat( STAT_AUTH_REQUIRED, "Authentication required" );
		    return TRUE;
		}
#else
		UNUSED( authDone );
#endif		     
                ret = c->cmdProc( Utl_stripWhiteSpace( arg ), c );
                return ret;
            }
    }
    putStat( STAT_NO_SUCH_CMD, "Command not recognized" );
    return TRUE;
}

static void
putWelcome( void )
{
    initOutput();
    putStat( STAT_READY_POST_ALLOW, "NNTP server NOFFLE %s",
             Cfg_version() );
    sendOutput();
}

static Bool
initServer( void )
{
    ASSERT( ! server.running );
    if ( ! Lock_openDatabases() )
      return FALSE;
    server.running = TRUE;
    server.lastServerOpen = time( NULL );
    return TRUE;
}

static void
closeServer( void )
{
    ASSERT( server.running );
    server.running = FALSE;
    Lock_closeDatabases();
}

void
Server_flushCache( void )
{
    server.groupReady = FALSE;
}

void
Server_run( void )
{
    Bool done;
    Str line;

    putWelcome();

#if USE_AUTH
    /*
     * If authentication is required, we issue the welcome message and
     * go into a command loop that doesn't open the databases and just
     * accepts authentication commands. Once successfully authenticated,
     * we drop root privs (if we have them - they are needed for PAM
     * authentication to work) and proceed to the normal loop.
     */
    if ( Cfg_needClientAuth() )
    {
	if ( ! Auth_open() )
	{
	    initOutput();
	    putFatal( "Cannot open authorisation" );
	    sendOutput();
	    return;
	}

	done = FALSE;
	while ( ! done )
	{
	    if ( Prt_getLn( line, stdin, -1 ) )
	    {
		initOutput();

		if ( ! parseAndExecute( line, FALSE ) )
		    done = TRUE;

		if ( server.auth == AUTH_DONE )
		    done = TRUE;

		sendOutput();
	    }
	    else
	    {
		Log_inf( "Client disconnected. Terminating." );
		done = TRUE;
	    }
	}

	Auth_close();

	/* Did we finish because we successfully authenticated? */
	if ( server.auth != AUTH_DONE )
	    return;
    }
#endif

    if ( ! Auth_dropPrivs() )
    {
	initOutput();
	putFatal( "Cannot set user privs" );
	sendOutput();
	return;
    }
    
    done = FALSE;
    while ( ! done )
    {
	if ( Prt_getLn( line, stdin, -1 ) )
	{
	    initOutput();

	    if ( ! initServer() )
	    {
		putFatal( "Cannot init server" );
		done = TRUE;
	    }
	    else
	    {
		if ( ! parseAndExecute( line, TRUE ) )
		    done = TRUE;
	    }

	    if ( server.running )
		closeServer();

	    sendOutput();
        }
	else
        {
            Log_inf( "Client disconnected. Terminating." );
            done = TRUE;
        }
    }
    if ( server.running )
        closeServer();
}