view src/group.c @ 494:372f8b55506e noffle

[svn] Apply patch from Jan De Luyck. Add new option 'add-messageid-if-missing', which optionally postpones adding a message ID to the upstream server. If this is done, post-locally must be off. This is to deal with an upstream server troubling Jan. It usually (but not always) rejects posts with a Noffle message ID. I have changed Jan's original option of 'add-message-id-if-missing' for consistency with 'replace-messageid' and added the manual page entry. See SourceForge feature request 1513395.
author bears
date Wed, 12 Jul 2006 20:26:41 +0100
parents ff7a2dc6023e
children
line wrap: on
line source

/*
  group.c

  The group database resides in groupinfo.gdbm and stores all we know about
  the groups we know of. One database record is cached in the global struct
  grp. Group information is transfered between the grp and the database by
  loadGrp() and saveGrp(). This is done transparently. Access to the groups
  database is done by group name, by the functions defined in group.h.        

  $Id: group.c 466 2003-02-26 11:30:41Z bears $
*/

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

#include <stdio.h>
#include <gdbm.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "configfile.h"
#include "wildmat.h"
#include "group.h"
#include "log.h"
#include "util.h"
#include "portable.h"

/* max length of a group name: */
#define MAX_GROUPNAME 78

/* currently only used within grp */
typedef struct
{
    int first;		/* number of first article within group */
    int last;		/* number of last article within group */
    int rmtNext;
    time_t created;
    time_t lastAccess;
} Entry;

struct
{
    Str name;		/* name of the group */
    Entry entry;	/* more information about this group */
    Str serv;		/* server the group resides on */
    Str dsc;		/* description of the group */
    char postAllow;	/* Posting status */
    time_t lastPost;	/* Time last article arrived */
    GDBM_FILE dbf;
} grp = { "(no grp)", { 0, 0, 0, 0, 0 }, "", "", ' ', (time_t) 0, NULL };

/*
  Note: postAllow and lastPost should really go in Entry. But
  changing Entry would make backwards group file format capability
  tricky, so they go where they are, and we test the length of the
  retrieved record to determine if they exist.

  Someday if we really change the record format this should be tidied up.
 */

static const char *
errMsg( void )
{
    if ( errno != 0 )
        return strerror( errno );
    return gdbm_strerror( gdbm_errno );
}

/*
  Some newsgroups names are reserved for server-specific or server
  pseudo groups. We don't want to fetch them. For example, INN
  keeps all its control messages in a 'control' hierarchy, and
  used the "to." hierarchy for dark and mysterious purposes I think
  are to do with newsfeeds. The recommended restrictions are documented
  in C.Lindsay, "News Article Format", <draft-ietf-usefor-article-08.txt>.

  Given that we shouldn't fetch them, it's also best if we don't allow them
  to be created locally, to avoid potential confusion with downstream
  readers/servers.

  That being said, the draft also specifies restrictions on the character
  set. Enforcing these generally would mean treating the name as
  UTF-8 and inspecting for forbidden character ranges. This is left as
  an exercise for the reader; for the meantime, Noffle will simply
  apply the few remaining naming restrictions.
*/
struct ForbiddenGroupName
{
    const char *pattern;
    Bool match;				/* TRUE if match means 'invalid group name' */
} forbiddenGroupNames[] =
{
#if 0
	/*
	  This is a tricky one. Single component newsgroups should be
	  restricted to a local server or a LAN, and the draft highlights
	  several single-component names which are typically pseudo-groups.
	  Previously we've forbidden single component groups, but this is
	  perhaps too harsh; you should be able to use Noffle to host them
	  and pass them around other Noffles on the same LAN. So change to
	  allowing single component names....
	*/
    { "*.*", FALSE },                   /* Single component */
#else
	/* .. but forbidding the ones flagged as pseudo-groups in the draft */
	{"control", TRUE },
	{"junk", TRUE },
	{"poster", TRUE },
#endif	
    { "control.*", TRUE },              /* control.* groups */
    { "to.*", TRUE },                   /* to.* groups */
    { "*.all", TRUE },                  /* 'all' as a component */
    { "*.all.*", TRUE },
    { "all.*", TRUE },
    { "*.ctl", TRUE },                  /* 'ctl' as a component */
    { "*.ctl.*", TRUE },
    { "ctl.*", TRUE },
    { "example.*", TRUE },              /* example.* groups */
    { "*,*", TRUE },                    /* newsgroups separator */
#if 0
	/* Not sure who decided this should be allowed, but leave it be. */
	{ "_*", TRUE },						/* reserved for future use, but accept nevertheless */
#endif	
    { "+*", TRUE },                     /* reserved */
    { "-*", TRUE }
};

Bool
Grp_open( void )
{
    Str name;
    int flags;

    ASSERT( grp.dbf == NULL );
    snprintf( name, MAXCHAR, "%s/data/groupinfo.gdbm", Cfg_spoolDir() );
    flags = GDBM_WRCREAT | GDBM_FAST;
    if ( ! ( grp.dbf = gdbm_open( name, 512, flags, 0644, Log_gdbm_fatal ) ) )
    {
        Log_err( "Error opening %s for r/w (%s)", errMsg() );
        return FALSE;
    }
    Log_dbg( LOG_DBG_NEWSBASE, "%s opened for r/w", name );
    return TRUE;
}

void
Grp_close( void )
{
    ASSERT( grp.dbf );
    Log_dbg( LOG_DBG_NEWSBASE, "Closing groupinfo" );
    gdbm_close( grp.dbf );
    grp.dbf = NULL;
    Utl_cpyStr( grp.name, "" );
}

/*
 * Load group info from gdbm-database into global struct grp
 *
 * Note use of memcpy when packing buffer; avoids pointer alignment
 * problems.
 */
static Bool
loadGrp( const char *name )
{
    const char *p;
    datum key, val;

    ASSERT( grp.dbf );
    if ( strcmp( grp.name, name ) == 0 )
         return TRUE;
    key.dptr = (void *)name;
    key.dsize = strlen( name ) + 1;
    val = gdbm_fetch( grp.dbf, key );
    if ( val.dptr == NULL )
        return FALSE;
    memcpy( &grp.entry, val.dptr, sizeof( grp.entry ) );
    p = val.dptr + sizeof( grp.entry );
    Utl_cpyStr( grp.serv, p );
    p += strlen( p ) + 1;
    Utl_cpyStr( grp.dsc, p );
    p += strlen( p) + 1;

    /*
     * Extension items. Initialise to default first.
     * We default to allowing posting, and the time
     * of the last post being a second before the last
     * access.
     */
    grp.postAllow = 'y';
    grp.lastPost = grp.entry.lastAccess - 1;
    
    if ( p - val.dptr < val.dsize )
    {
	grp.postAllow = p[ 0 ];
	p++;
	if ( p - val.dptr < val.dsize )
	    memcpy( &grp.lastPost, p, sizeof( grp.lastPost ) );
    }

    Utl_cpyStr( grp.name, name );
    free( val.dptr );
    return TRUE;
}

/*
 * Save group info from global struct grp into gdbm-database
 *
 * Note use of memcpy when packing buffer; avoids pointer alignment
 * problems.
 */
static void
saveGrp( void )
{
    size_t lenServ, lenDsc, bufLen;
    datum key, val;
    void *buf;
    char *p;

    ASSERT( grp.dbf );
    lenServ = strlen( grp.serv );
    lenDsc = strlen( grp.dsc );
    bufLen =
	sizeof( grp.entry ) + lenServ + lenDsc + 2
	+ sizeof( char ) + sizeof( time_t );
    buf = malloc( bufLen );
    memcpy( buf, &grp.entry, sizeof( grp.entry ) );
    p = (char *)buf + sizeof( grp.entry );
    strcpy( p, grp.serv );
    p += lenServ + 1;
    strcpy( p, grp.dsc );
    p += lenDsc + 1;
    p[ 0 ] = grp.postAllow;
    p++;
    memcpy( p, &grp.lastPost, sizeof( grp.lastPost ) );
    key.dptr = (void *)grp.name;
    key.dsize = strlen( grp.name ) + 1;
    val.dptr = buf;
    val.dsize = bufLen;
    if ( gdbm_store( grp.dbf, key, val, GDBM_REPLACE ) != 0 )
        Log_err( "Could not save group %s: %s", errMsg() );
    free( buf );
}

Bool
Grp_exists( const char *name )
{
    datum key;

    ASSERT( grp.dbf );
    key.dptr = (void*)name;
    key.dsize = strlen( name ) + 1;
    return gdbm_exists( grp.dbf, key );
}

Bool
Grp_local( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return ( strcmp( grp.serv, GRP_LOCAL_SERVER_NAME ) == 0 );
}

void
Grp_create( const char *name )
{
    Utl_cpyStr( grp.name, name );
    Utl_cpyStr( grp.serv, "(unknown)" );
    grp.dsc[ 0 ] = '\0';
    grp.entry.first = 1;
    grp.entry.last = 0;
    grp.entry.rmtNext = GRP_RMT_NEXT_NOT_SUBSCRIBED;
    grp.entry.created = time( NULL );
    grp.entry.lastAccess = 0;
    grp.postAllow = 'y';
    saveGrp();
}

void
Grp_delete( const char *name )
{
    datum key;

    ASSERT( grp.dbf );
    key.dptr = (void*)name;
    key.dsize = strlen( name ) + 1;
    gdbm_delete( grp.dbf, key );
}

const char *
Grp_dsc( const char *name )
{
    if ( ! loadGrp( name ) )
        return NULL;
    return grp.dsc;
}

const char *
Grp_server( const char *name )
{
    static Str serv = "";

    if ( ! loadGrp( name ) )
        return "[unknown grp]";
    if ( Cfg_servListContains( grp.serv )
         || Grp_local( name ) )
        Utl_cpyStr( serv, grp.serv );
    else
        snprintf( serv, MAXCHAR, "[%s]", grp.serv );
    return serv;
}

int
Grp_first( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.entry.first;
}

int
Grp_last( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.entry.last;
}

int
Grp_lastAccess( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.entry.lastAccess;
}

int
Grp_rmtNext( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.entry.rmtNext;
}

time_t
Grp_created( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.entry.created;
}

char
Grp_postAllow( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.postAllow;
}


time_t
Grp_lastPostTime( const char *name )
{
    if ( ! loadGrp( name ) )
        return 0;
    return grp.lastPost;
}

/* Replace group's description (only if value != ""). */
void
Grp_setDsc( const char *name, const char *value )
{
    if ( loadGrp( name ) )
    {
        Utl_cpyStr( grp.dsc, value );
        saveGrp();
    }
}

void
Grp_setLocal( const char *name )
{
    Grp_setServ( name, GRP_LOCAL_SERVER_NAME );
}

void
Grp_setServ( const char *name, const char *value )
{
    if ( loadGrp( name ) )
    {
        Utl_cpyStr( grp.serv, value );
        saveGrp();
    }
}

void
Grp_setRmtNext( const char *name, int value )
{
    if ( loadGrp( name ) )
    {
        grp.entry.rmtNext = value;
        saveGrp();
    }
}

void
Grp_setLastAccess( const char *name )
{
    if ( loadGrp( name ) )
    {
        grp.entry.lastAccess = time( NULL );
        saveGrp();
    }
}

void
Grp_setPostAllow( const char *name, char postAllow )
{
    if ( loadGrp( name ) )
    {
        grp.postAllow = postAllow;
        saveGrp();
    }
}

void
Grp_setFirstLast( const char *name, int first, int last )
{
    if ( loadGrp( name ) )
    {
        grp.entry.first = first;
        grp.entry.last = last;
        saveGrp();
    }
}

void
Grp_setLastPostTime( const char *name )
{
    if ( loadGrp( name ) )
    {
        grp.lastPost = time( NULL );
        saveGrp();
    }
}

static datum cursor = { NULL, 0 };

Bool
Grp_firstGrp( const char **name )
{
    ASSERT( grp.dbf );
    if ( cursor.dptr != NULL )
    {
        free( cursor.dptr );
        cursor.dptr = NULL;
    }
    cursor = gdbm_firstkey( grp.dbf );
    *name = cursor.dptr;
    return ( cursor.dptr != NULL );
}

Bool
Grp_nextGrp( const char **name )
{
    void *oldDptr = cursor.dptr;

    ASSERT( grp.dbf );
    if ( cursor.dptr == NULL )
        return FALSE;
    cursor = gdbm_nextkey( grp.dbf, cursor );
    free( oldDptr );
    *name = cursor.dptr;
    return ( cursor.dptr != NULL );
}

/* Group names' sanity checks. Groups with forbidden names
   can't be safely deleted or created. */
Bool
Grp_isForbiddenName( const char *name)
{
    const char *illegalchars = "\t\n\v\r /:\\";
/*  "\t\n\v\r " whitespace
    "/:\\" directory prefix (Unix, MacOS, Freedos filesystems) */
    /* Find illegal characters. */
    if ( strpbrk( name, illegalchars ) )
        return TRUE;
    /* Find '.' dot directory prefix to prevent exploits. */
    if ( name[0] == '.')  /* prevent noffle -C ../fetchlist */
        return TRUE; /* group name invalid */
    return FALSE;
}

/*
   Forbidden or restricted group names or hierarchies. Please refer to
   draft-ietf-usefor-article-06, chapter 5.5.1. Groups with invalid
   names can't be created, but can still be deleted.
 */
Bool
Grp_isValidName( const char *name)
{
    size_t i;
    int len;

    /* Groups with lengthy names like
       alt.the.lame.troll.should.be.posting.again.in.just.a.few.more.weeks.from.what.he.said
       or
       microsoft.public.windows.inetexplorer.ie55.programming.components.codedownload
       are most likely bogus groups that have been mistakenly created.
    */
    len = strlen( name );
    if ( len > MAX_GROUPNAME || len < 1 )
        return FALSE;
    
    for ( i = 0;
          i < sizeof( forbiddenGroupNames ) /
              sizeof( struct ForbiddenGroupName );
          ++i )
    {
        /* Negate result of Wld_match to ensure it is 1 or 0. */
        if ( forbiddenGroupNames[i].match !=
             ( ! Wld_match( name, forbiddenGroupNames[i].pattern ) ) )
            return FALSE;
    }
    /* no match? then assume the group is valid. */
    return TRUE;
}