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