1 /*
2   group.c
3 
4   The group database resides in groupinfo.gdbm and stores all we know about
5   the groups we know of. One database record is cached in the global struct
6   grp. Group information is transfered between the grp and the database by
7   loadGrp() and saveGrp(). This is done transparently. Access to the groups
8   database is done by group name, by the functions defined in group.h.
9 
10   $Id: group.c,v 1.16 2003/02/26 11:30:41 bears Exp $
11 */
12 
13 #if HAVE_CONFIG_H
14 #include <config.h>
15 #endif
16 
17 #include <stdio.h>
18 #include <gdbm.h>
19 #include <errno.h>
20 #include <fcntl.h>
21 #include <sys/stat.h>
22 #include "configfile.h"
23 #include "wildmat.h"
24 #include "group.h"
25 #include "log.h"
26 #include "util.h"
27 #include "portable.h"
28 
29 /* max length of a group name: */
30 #define MAX_GROUPNAME 78
31 
32 /* currently only used within grp */
33 typedef struct
34 {
35     int first;		/* number of first article within group */
36     int last;		/* number of last article within group */
37     int rmtNext;
38     time_t created;
39     time_t lastAccess;
40 } Entry;
41 
42 struct
43 {
44     Str name;		/* name of the group */
45     Entry entry;	/* more information about this group */
46     Str serv;		/* server the group resides on */
47     Str dsc;		/* description of the group */
48     char postAllow;	/* Posting status */
49     time_t lastPost;	/* Time last article arrived */
50     GDBM_FILE dbf;
51 } grp = { "(no grp)", { 0, 0, 0, 0, 0 }, "", "", ' ', (time_t) 0, NULL };
52 
53 /*
54   Note: postAllow and lastPost should really go in Entry. But
55   changing Entry would make backwards group file format capability
56   tricky, so they go where they are, and we test the length of the
57   retrieved record to determine if they exist.
58 
59   Someday if we really change the record format this should be tidied up.
60  */
61 
62 static const char *
errMsg(void)63 errMsg( void )
64 {
65     if ( errno != 0 )
66         return strerror( errno );
67     return gdbm_strerror( gdbm_errno );
68 }
69 
70 /*
71   Some newsgroups names are reserved for server-specific or server
72   pseudo groups. We don't want to fetch them. For example, INN
73   keeps all its control messages in a 'control' hierarchy, and
74   used the "to." hierarchy for dark and mysterious purposes I think
75   are to do with newsfeeds. The recommended restrictions are documented
76   in C.Lindsay, "News Article Format", <draft-ietf-usefor-article-08.txt>.
77 
78   Given that we shouldn't fetch them, it's also best if we don't allow them
79   to be created locally, to avoid potential confusion with downstream
80   readers/servers.
81 
82   That being said, the draft also specifies restrictions on the character
83   set. Enforcing these generally would mean treating the name as
84   UTF-8 and inspecting for forbidden character ranges. This is left as
85   an exercise for the reader; for the meantime, Noffle will simply
86   apply the few remaining naming restrictions.
87 */
88 struct ForbiddenGroupName
89 {
90     const char *pattern;
91     Bool match;				/* TRUE if match means 'invalid group name' */
92 } forbiddenGroupNames[] =
93 {
94 #if 0
95 	/*
96 	  This is a tricky one. Single component newsgroups should be
97 	  restricted to a local server or a LAN, and the draft highlights
98 	  several single-component names which are typically pseudo-groups.
99 	  Previously we've forbidden single component groups, but this is
100 	  perhaps too harsh; you should be able to use Noffle to host them
101 	  and pass them around other Noffles on the same LAN. So change to
102 	  allowing single component names....
103 	*/
104     { "*.*", FALSE },                   /* Single component */
105 #else
106 	/* .. but forbidding the ones flagged as pseudo-groups in the draft */
107 	{"control", TRUE },
108 	{"junk", TRUE },
109 	{"poster", TRUE },
110 #endif
111     { "control.*", TRUE },              /* control.* groups */
112     { "to.*", TRUE },                   /* to.* groups */
113     { "*.all", TRUE },                  /* 'all' as a component */
114     { "*.all.*", TRUE },
115     { "all.*", TRUE },
116     { "*.ctl", TRUE },                  /* 'ctl' as a component */
117     { "*.ctl.*", TRUE },
118     { "ctl.*", TRUE },
119     { "example.*", TRUE },              /* example.* groups */
120     { "*,*", TRUE },                    /* newsgroups separator */
121 #if 0
122 	/* Not sure who decided this should be allowed, but leave it be. */
123 	{ "_*", TRUE },						/* reserved for future use, but accept nevertheless */
124 #endif
125     { "+*", TRUE },                     /* reserved */
126     { "-*", TRUE }
127 };
128 
129 Bool
Grp_open(void)130 Grp_open( void )
131 {
132     Str name;
133     int flags;
134 
135     ASSERT( grp.dbf == NULL );
136     snprintf( name, MAXCHAR, "%s/data/groupinfo.gdbm", Cfg_spoolDir() );
137     flags = GDBM_WRCREAT | GDBM_FAST;
138     if ( ! ( grp.dbf = gdbm_open( name, 512, flags, 0644, Log_gdbm_fatal ) ) )
139     {
140         Log_err( "Error opening %s for r/w (%s)", errMsg() );
141         return FALSE;
142     }
143     Log_dbg( LOG_DBG_NEWSBASE, "%s opened for r/w", name );
144     return TRUE;
145 }
146 
147 void
Grp_close(void)148 Grp_close( void )
149 {
150     ASSERT( grp.dbf );
151     Log_dbg( LOG_DBG_NEWSBASE, "Closing groupinfo" );
152     gdbm_close( grp.dbf );
153     grp.dbf = NULL;
154     Utl_cpyStr( grp.name, "" );
155 }
156 
157 /*
158  * Load group info from gdbm-database into global struct grp
159  *
160  * Note use of memcpy when packing buffer; avoids pointer alignment
161  * problems.
162  */
163 static Bool
loadGrp(const char * name)164 loadGrp( const char *name )
165 {
166     const char *p;
167     datum key, val;
168 
169     ASSERT( grp.dbf );
170     if ( strcmp( grp.name, name ) == 0 )
171          return TRUE;
172     key.dptr = (void *)name;
173     key.dsize = strlen( name ) + 1;
174     val = gdbm_fetch( grp.dbf, key );
175     if ( val.dptr == NULL )
176         return FALSE;
177     memcpy( &grp.entry, val.dptr, sizeof( grp.entry ) );
178     p = val.dptr + sizeof( grp.entry );
179     Utl_cpyStr( grp.serv, p );
180     p += strlen( p ) + 1;
181     Utl_cpyStr( grp.dsc, p );
182     p += strlen( p) + 1;
183 
184     /*
185      * Extension items. Initialise to default first.
186      * We default to allowing posting, and the time
187      * of the last post being a second before the last
188      * access.
189      */
190     grp.postAllow = 'y';
191     grp.lastPost = grp.entry.lastAccess - 1;
192 
193     if ( p - val.dptr < val.dsize )
194     {
195 	grp.postAllow = p[ 0 ];
196 	p++;
197 	if ( p - val.dptr < val.dsize )
198 	    memcpy( &grp.lastPost, p, sizeof( grp.lastPost ) );
199     }
200 
201     Utl_cpyStr( grp.name, name );
202     free( val.dptr );
203     return TRUE;
204 }
205 
206 /*
207  * Save group info from global struct grp into gdbm-database
208  *
209  * Note use of memcpy when packing buffer; avoids pointer alignment
210  * problems.
211  */
212 static void
saveGrp(void)213 saveGrp( void )
214 {
215     size_t lenServ, lenDsc, bufLen;
216     datum key, val;
217     void *buf;
218     char *p;
219 
220     ASSERT( grp.dbf );
221     lenServ = strlen( grp.serv );
222     lenDsc = strlen( grp.dsc );
223     bufLen =
224 	sizeof( grp.entry ) + lenServ + lenDsc + 2
225 	+ sizeof( char ) + sizeof( time_t );
226     buf = malloc( bufLen );
227     memcpy( buf, &grp.entry, sizeof( grp.entry ) );
228     p = (char *)buf + sizeof( grp.entry );
229     strcpy( p, grp.serv );
230     p += lenServ + 1;
231     strcpy( p, grp.dsc );
232     p += lenDsc + 1;
233     p[ 0 ] = grp.postAllow;
234     p++;
235     memcpy( p, &grp.lastPost, sizeof( grp.lastPost ) );
236     key.dptr = (void *)grp.name;
237     key.dsize = strlen( grp.name ) + 1;
238     val.dptr = buf;
239     val.dsize = bufLen;
240     if ( gdbm_store( grp.dbf, key, val, GDBM_REPLACE ) != 0 )
241         Log_err( "Could not save group %s: %s", errMsg() );
242     free( buf );
243 }
244 
245 Bool
Grp_exists(const char * name)246 Grp_exists( const char *name )
247 {
248     datum key;
249 
250     ASSERT( grp.dbf );
251     key.dptr = (void*)name;
252     key.dsize = strlen( name ) + 1;
253     return gdbm_exists( grp.dbf, key );
254 }
255 
256 Bool
Grp_local(const char * name)257 Grp_local( const char *name )
258 {
259     if ( ! loadGrp( name ) )
260         return 0;
261     return ( strcmp( grp.serv, GRP_LOCAL_SERVER_NAME ) == 0 );
262 }
263 
264 void
Grp_create(const char * name)265 Grp_create( const char *name )
266 {
267     Utl_cpyStr( grp.name, name );
268     Utl_cpyStr( grp.serv, "(unknown)" );
269     grp.dsc[ 0 ] = '\0';
270     grp.entry.first = 1;
271     grp.entry.last = 0;
272     grp.entry.rmtNext = GRP_RMT_NEXT_NOT_SUBSCRIBED;
273     grp.entry.created = time( NULL );
274     grp.entry.lastAccess = 0;
275     grp.postAllow = 'y';
276     saveGrp();
277 }
278 
279 void
Grp_delete(const char * name)280 Grp_delete( const char *name )
281 {
282     datum key;
283 
284     ASSERT( grp.dbf );
285     key.dptr = (void*)name;
286     key.dsize = strlen( name ) + 1;
287     gdbm_delete( grp.dbf, key );
288 }
289 
290 const char *
Grp_dsc(const char * name)291 Grp_dsc( const char *name )
292 {
293     if ( ! loadGrp( name ) )
294         return NULL;
295     return grp.dsc;
296 }
297 
298 const char *
Grp_server(const char * name)299 Grp_server( const char *name )
300 {
301     static Str serv = "";
302 
303     if ( ! loadGrp( name ) )
304         return "[unknown grp]";
305     if ( Cfg_servListContains( grp.serv )
306          || Grp_local( name ) )
307         Utl_cpyStr( serv, grp.serv );
308     else
309         snprintf( serv, MAXCHAR, "[%s]", grp.serv );
310     return serv;
311 }
312 
313 int
Grp_first(const char * name)314 Grp_first( const char *name )
315 {
316     if ( ! loadGrp( name ) )
317         return 0;
318     return grp.entry.first;
319 }
320 
321 int
Grp_last(const char * name)322 Grp_last( const char *name )
323 {
324     if ( ! loadGrp( name ) )
325         return 0;
326     return grp.entry.last;
327 }
328 
329 int
Grp_lastAccess(const char * name)330 Grp_lastAccess( const char *name )
331 {
332     if ( ! loadGrp( name ) )
333         return 0;
334     return grp.entry.lastAccess;
335 }
336 
337 int
Grp_rmtNext(const char * name)338 Grp_rmtNext( const char *name )
339 {
340     if ( ! loadGrp( name ) )
341         return 0;
342     return grp.entry.rmtNext;
343 }
344 
345 time_t
Grp_created(const char * name)346 Grp_created( const char *name )
347 {
348     if ( ! loadGrp( name ) )
349         return 0;
350     return grp.entry.created;
351 }
352 
353 char
Grp_postAllow(const char * name)354 Grp_postAllow( const char *name )
355 {
356     if ( ! loadGrp( name ) )
357         return 0;
358     return grp.postAllow;
359 }
360 
361 
362 time_t
Grp_lastPostTime(const char * name)363 Grp_lastPostTime( const char *name )
364 {
365     if ( ! loadGrp( name ) )
366         return 0;
367     return grp.lastPost;
368 }
369 
370 /* Replace group's description (only if value != ""). */
371 void
Grp_setDsc(const char * name,const char * value)372 Grp_setDsc( const char *name, const char *value )
373 {
374     if ( loadGrp( name ) )
375     {
376         Utl_cpyStr( grp.dsc, value );
377         saveGrp();
378     }
379 }
380 
381 void
Grp_setLocal(const char * name)382 Grp_setLocal( const char *name )
383 {
384     Grp_setServ( name, GRP_LOCAL_SERVER_NAME );
385 }
386 
387 void
Grp_setServ(const char * name,const char * value)388 Grp_setServ( const char *name, const char *value )
389 {
390     if ( loadGrp( name ) )
391     {
392         Utl_cpyStr( grp.serv, value );
393         saveGrp();
394     }
395 }
396 
397 void
Grp_setRmtNext(const char * name,int value)398 Grp_setRmtNext( const char *name, int value )
399 {
400     if ( loadGrp( name ) )
401     {
402         grp.entry.rmtNext = value;
403         saveGrp();
404     }
405 }
406 
407 void
Grp_setLastAccess(const char * name)408 Grp_setLastAccess( const char *name )
409 {
410     if ( loadGrp( name ) )
411     {
412         grp.entry.lastAccess = time( NULL );
413         saveGrp();
414     }
415 }
416 
417 void
Grp_setPostAllow(const char * name,char postAllow)418 Grp_setPostAllow( const char *name, char postAllow )
419 {
420     if ( loadGrp( name ) )
421     {
422         grp.postAllow = postAllow;
423         saveGrp();
424     }
425 }
426 
427 void
Grp_setFirstLast(const char * name,int first,int last)428 Grp_setFirstLast( const char *name, int first, int last )
429 {
430     if ( loadGrp( name ) )
431     {
432         grp.entry.first = first;
433         grp.entry.last = last;
434         saveGrp();
435     }
436 }
437 
438 void
Grp_setLastPostTime(const char * name)439 Grp_setLastPostTime( const char *name )
440 {
441     if ( loadGrp( name ) )
442     {
443         grp.lastPost = time( NULL );
444         saveGrp();
445     }
446 }
447 
448 static datum cursor = { NULL, 0 };
449 
450 Bool
Grp_firstGrp(const char ** name)451 Grp_firstGrp( const char **name )
452 {
453     ASSERT( grp.dbf );
454     if ( cursor.dptr != NULL )
455     {
456         free( cursor.dptr );
457         cursor.dptr = NULL;
458     }
459     cursor = gdbm_firstkey( grp.dbf );
460     *name = cursor.dptr;
461     return ( cursor.dptr != NULL );
462 }
463 
464 Bool
Grp_nextGrp(const char ** name)465 Grp_nextGrp( const char **name )
466 {
467     void *oldDptr = cursor.dptr;
468 
469     ASSERT( grp.dbf );
470     if ( cursor.dptr == NULL )
471         return FALSE;
472     cursor = gdbm_nextkey( grp.dbf, cursor );
473     free( oldDptr );
474     *name = cursor.dptr;
475     return ( cursor.dptr != NULL );
476 }
477 
478 /* Group names' sanity checks. Groups with forbidden names
479    can't be safely deleted or created. */
480 Bool
Grp_isForbiddenName(const char * name)481 Grp_isForbiddenName( const char *name)
482 {
483     const char *illegalchars = "\t\n\v\r /:\\";
484 /*  "\t\n\v\r " whitespace
485     "/:\\" directory prefix (Unix, MacOS, Freedos filesystems) */
486     /* Find illegal characters. */
487     if ( strpbrk( name, illegalchars ) )
488         return TRUE;
489     /* Find '.' dot directory prefix to prevent exploits. */
490     if ( name[0] == '.')  /* prevent noffle -C ../fetchlist */
491         return TRUE; /* group name invalid */
492     return FALSE;
493 }
494 
495 /*
496    Forbidden or restricted group names or hierarchies. Please refer to
497    draft-ietf-usefor-article-06, chapter 5.5.1. Groups with invalid
498    names can't be created, but can still be deleted.
499  */
500 Bool
Grp_isValidName(const char * name)501 Grp_isValidName( const char *name)
502 {
503     size_t i;
504     int len;
505 
506     /* Groups with lengthy names like
507        alt.the.lame.troll.should.be.posting.again.in.just.a.few.more.weeks.from.what.he.said
508        or
509        microsoft.public.windows.inetexplorer.ie55.programming.components.codedownload
510        are most likely bogus groups that have been mistakenly created.
511     */
512     len = strlen( name );
513     if ( len > MAX_GROUPNAME || len < 1 )
514         return FALSE;
515 
516     for ( i = 0;
517           i < sizeof( forbiddenGroupNames ) /
518               sizeof( struct ForbiddenGroupName );
519           ++i )
520     {
521         /* Negate result of Wld_match to ensure it is 1 or 0. */
522         if ( forbiddenGroupNames[i].match !=
523              ( ! Wld_match( name, forbiddenGroupNames[i].pattern ) ) )
524             return FALSE;
525     }
526     /* no match? then assume the group is valid. */
527     return TRUE;
528 }
529