sn-0.3.8/0000700000175000001440000000000010106142567012603 5ustar patrikusers00000000000000sn-0.3.8/commands.c0000600000175000001440000003106610042571016014552 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * NNTP commands here. Each command is a function of * type void foo (void). */ #include #include #include #include #include #include #include #include #include "config.h" #include "times.h" #include "dhash.h" #include "art.h" #include "group.h" #include "args.h" #include "body.h" #include "snntpd.h" #include "key.h" #include #include #include #include #include static const char ver_ctrl_id[] = "$Id: commands.c 29 2004-04-24 23:02:38Z patrik $"; #define putdot() args_write(1, ".\r\n") void do_ihave (void) { args_write(1, "435 I'm happy for you\r\n"); } void do_sendme (void) { args_write(1, "500 No thanks\r\n"); } void do_slave (void) { args_write(1, "202 Whatever\r\n"); } void do_help (void) { args_write(1, "100 Help yourself\r\n.\r\n"); } void do_mode (void) { if (posting_ok) args_write(1, "200 Hi, you can post (sn version " VERSION ")\r\n"); else args_write(1, "201 Hi, you can't post (sn version " VERSION ")\r\n"); } /* Several ways to specify an article. The group may be the current one, or it may be a different one */ static char *specgroup; static int speclo, spechi; /* ...but with just one of two results */ static struct article article; static char *specerror; /* * For nntp commands that refer to an article, or a range of them, * these always appear in the form of a number, or range, or a mssage * ID. readspec() understands these and sets the low and high end of * the range, and the group, and any error. */ static int readspec (char *spec) { char *p; if (!spec) { if ((specgroup = currentgroup)) if ((spechi = speclo = currentserial) > 0) return 0; else specerror = "420 No article selected\r\n"; else specerror = "412 No group selected\r\n"; return -1; } if ('<' == *spec) { struct data d; d.messageid = spec + 1; if ((p = strchr(d.messageid, '>'))) { *p = '\0'; if (0 == dh_find(&d, FALSE)) { spechi = speclo = d.serial; specgroup = d.newsgroup; /* is static */ return 0; } else specerror = "430 No such article\r\n"; } else specerror = "430 Invalid message ID\r\n"; return -1; } if ((specgroup = currentgroup)) { char *end; speclo = strtoul(spec, &end, 10); spec = end; if ('-' == *end) { spec++; if (*spec) { spechi = strtoul(spec, &end, 10); if (*end) goto invalid; if (spechi < speclo) goto invalid; } else spechi = INT_MAX; if (spechi > currentinfo.last) { if (speclo <= currentinfo.last) spechi = currentinfo.last; else spechi = speclo; } } else if (!*spec) spechi = speclo; else goto invalid; } return 0; invalid: specerror = "501 Invalid range\r\n"; return -1; } static char *findid (void) { char *id; if ((id = art_findfield(article.head, "Message-ID")) && *id) return id; return "<0>"; } /* * Each time writefifo is called, the degree of interest in this * group increases until the fifo is written. */ static void writefifo (void) { static char oldgroup[GROUPNAMELEN + 1] = ""; static int interested = 0; if (-1 == fifo) return; if (strcasecmp(oldgroup, currentgroup) == 0) { interested++; if (3 == interested % 10) { int len; len = strlen(currentgroup); currentgroup[len] = '\n'; (void) write(fifo, currentgroup, len + 1); currentgroup[len] = '\0'; } } else { interested = 0; strcpy(oldgroup, currentgroup); } } #define PUTHEAD 0x1 #define PUTBODY 0x2 static void putarticle (int flag, int code, char *msg) { char *p; if (-1 == readspec(args[1])) { args_write(1, specerror); return; } if (-1 == art_gimme(specgroup, speclo, &article)) { args_write(1, "430 No such article\r\n"); return; } if (speclo != spechi) { args_write(1, "512 Invalid argument\r\n"); return; } if (flag & PUTBODY) do { /* * Bit dangerous. To test the validity of the body we are about * to display, check these 2 things: the bytes -before- and * -after- the body buffer must be '\0'. This is not normally * checked for in art_gimme() because it will fault in the page, * but now we know we will be accessing it. */ if (!art_bodyiscorrupt(article.body, article.blen)) if (0 == body(&article.body, &article.blen)) if (!article.body[article.blen]) break; else p = "Internal error, uncompressed to corrupt body"; else p = "Internal error, can't uncompress body"; else p = "Article is corrupt"; args_write(1, "423 %s\r\n", p); return; } while (0); if (!args[1] || '<' != *args[1]) { args_write(1, "%d %d %s %s\r\n", code, speclo, findid(), msg); currentserial = speclo; writefifo(); } else args_write(1, "%d %d <%s> %s\r\n", code, speclo, args[1] + 1, msg); switch (flag) { case 0: return; case PUTHEAD: case PUTBODY | PUTHEAD: /* should never have let Xref into header, now have to remove it */ p = strstr(article.head, "Xref:"); if (p && (p == article.head || '\n' == p[-1])) writef(1, "%SXref: %s %s:%d%s", p - article.head, article.head, me, specgroup, speclo, p + 5); else writef(1, "%sXref: %s %s:%d\r\n", article.head, me, specgroup, speclo); if (PUTHEAD == flag) break; write(1, "\r\n", 2); case PUTBODY: write(1, article.body, article.blen); if ('\n' != article.body[article.blen - 1]) { LOG("putarticle:%s:%d does not end in newline", specgroup, speclo); write(1, "\r\n", 2); } } putdot(); } void do_article (void) { putarticle(PUTHEAD | PUTBODY, 220, "Article follows"); } void do_body (void) { putarticle(PUTBODY, 222, "Body follows"); } void do_head (void) { putarticle(PUTHEAD, 221, "Head follows"); } void do_stat (void) { putarticle(0, 223, "Request text separately"); } void do_group (void) { if (alltolower(args[1]) >= GROUPNAMELEN || make_current(args[1])) args_write(1, "411 No such group here as %s\r\n", args[1]); else { args_write(1, "211 %d %d %d %s\r\n", currentinfo.nr_articles, currentinfo.first, currentinfo.last, args[1]); specgroup = 0; writefifo(); } } static void nextlast (int inc, char *ermsg) { int serial; int count = 2 * ARTSPERFILE; for (serial = currentserial + inc; serial && count; serial += inc, count--) if (0 == art_gimme(currentgroup, serial, &article)) { args_write(1, "223 %d %s request text separately\r\n", serial, findid()); currentserial = serial; return; } args_write(1, ermsg); } void do_last (void) { nextlast(-1, "422 No previous article\r\n"); } void do_next (void) { nextlast(+1, "421 No next article\r\n"); } /* date: YYMMDD; clock: HHMMSS */ #define DIG(c) (c - '0') static time_t converttime (char *date, char *clock, char *gmt) { struct tm tm = { 0, }; time_t t = 0; int difference = 0; if (strspn(date, "0123456789") != 6 || strspn(clock, "0123456789") != 6) return ((time_t) -1); tm.tm_year = DIG(*date) * 10 + DIG(date[1]); date += 2; if (tm.tm_year < 70) tm.tm_year += 100; /* 00 - 69 => 2000 - 2069 */ tm.tm_mon = DIG(*date) * 10 + DIG(date[1]); date += 2; tm.tm_mon -= 1; tm.tm_mday = DIG(*date) * 10 + DIG(date[1]); /* FvL was here */ tm.tm_hour = DIG(*clock) * 10 + DIG(clock[1]); clock += 2; tm.tm_min = DIG(*clock) * 10 + DIG(clock[1]); clock += 2; tm.tm_sec = DIG(*clock) * 10 + DIG(clock[1]); tm.tm_isdst = -1; /* Avoid having the hour adjusted +1 by mktime() during DST */ if (gmt && 0 == strcasecmp(gmt, "GMT")) { /* Convert to local time, all our stuff is in local time. */ time_t then = mktime(&tm); struct tm *then_tm = gmtime(&then); then_tm->tm_isdst = -1; /* Avoid having the hour adjusted +1 by mktime() during DST */ difference = then - mktime(then_tm); } t = mktime(&tm); if (t == ((time_t) -1)) return ((time_t) -1); return (t + difference); } void do_newgroups (void) { time_t cutoff; cutoff = converttime(args[1], args[2], args[3]); if ((long) cutoff > 0) { /* XXX Do we -have- to always reply 231? */ args_write(1, "231 new newsgroups follows\r\n"); if (nr_keys > 0) { int n, i; struct key *kp; char *group; char buf[GROUPNAMELEN + 32]; struct stat st; for_each_key(i, n, kp) { group = KEY(kp); formats(buf, sizeof (buf) - 1, "%s/.created", group); if (-1 == stat(buf, &st)) { LOG("do_newsgroups:stat(%s/.created):%m", group); continue; } if (st.st_mtime >= cutoff) { struct group g; if (-1 == group_info(group, &g)) continue; args_write(1, "%s %d %d y\r\n", group, g.first, g.last); } } } else LOG2("do_newgroups:we have no groups"); } else args_write(1, "231 Bad date or time\r\n"); putdot(); } void do_newnews (void) { time_t cutoff; extern int nr_keys; int i, j, serial; char *pat; cutoff = converttime(args[2], args[3], args[4]); if (cutoff > 0) { struct key *kp; struct group g; args_write(1, "230 list of new articles follows\r\n"); alltolower(args[1]); while ((pat = tokensep(args + 1, ","))) for_each_key(i, j, kp) { char *group; group = KEY(kp); if (wildmat(group, pat)) if (0 == group_info(group, &g)) if ((serial = times_since(group, cutoff)) > 0) for (; serial < g.last; serial++) if (0 == art_gimmenoderef(group, serial, &article)) args_write(1, "%s\r\n", findid()); } } else args_write(1, "230 Bad date or time\r\n"); args_write(1, ".\r\n"); } static void xlooper (char *spec, void (*f) (void), char *msg) { if (-1 == readspec(spec)) { args_write(1, specerror); return; } args_write(1, msg); for (; speclo <= spechi; speclo++) if (0 == art_gimme(specgroup, speclo, &article)) f(); putdot(); if (spec && '<' != *spec) writefifo(); } static void putxover (void) { struct xover x; art_makexover(&article, &x); writef(1, "%d\t%S\t%S\t%S\t%S\t%S\t%S\t%S\tXref: %s %s:%d\r\n", speclo, x.subject.len, x.subject.pointer, x.from.len, x.from.pointer, x.date.len, x.date.pointer, x.messageid.len, x.messageid.pointer, x.references.len, x.references.pointer, x.bytes.len, x.bytes.pointer, x.lines.len, x.lines.pointer, me, specgroup, speclo); } void do_xover (void) { xlooper(args[1], putxover, "224 XOVER follows\r\n"); } static void putxhdr (void) { char *id; id = art_findfield(article.head, args[1]); if (*id) args_write(1, "%d %s\r\n", speclo, id); else if (strcasecmp(args[1], "xref") == 0) /* Xref needs to be generated on the fly */ args_write(1, "%d %s %s:%d\r\n", speclo, me, specgroup, speclo); } void do_xhdr (void) { xlooper(args[2], putxhdr, "221 XHDR follows\r\n"); } static char *field; static void putxpat (void) { char **globs; char *line; globs = args + 3; if ((line = art_findfield(article.head, field)) && *line) for (; *globs; globs++) /* XXX ORed? */ if (wildmat(line, *globs)) { args_write(1, "%d %s: %s\r\n", speclo, field, line); break; } } void do_xpat (void) { field = args[1]; xlooper(args[2], putxpat, "221 Headers follows\r\n"); } sn-0.3.8/path.h0000600000175000001440000000043107564256733013727 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Ensure PATH environment variable contains BINDIR. */ extern int set_path_var (void); sn-0.3.8/lib/0000700000175000001440000000000010106142567013351 5ustar patrikusers00000000000000sn-0.3.8/lib/wildmat.h0000600000175000001440000000105307564256733015203 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef WILDMAT_H #define WILDMAT_H /* * Case-insensitive globbing. Returns 1 if pattern matches the * candidate string entirely, else 0. Supported glob features are *, * ? [] including - as a range. Leading . is not special. Newlines * are not special. Please don't send bad glob patterns! */ extern int wildmat (char *candidate, char *pattern); #endif sn-0.3.8/lib/wildmat.c0000600000175000001440000000663510042571016015164 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Globbing function. */ #include #include #include #include "defs.h" static const char ver_ctrl_id[] = "$Id: wildmat.c 29 2004-04-24 23:02:38Z patrik $"; #if 1 #define TOLOWER(x) tolower(x) /* case insensitive */ #else #define TOLOWER(x) x /* case sensitive */ #endif static int match (char *candidate, char *pattern); static int bracket (char *candidate, char *pattern) { bool negate = FALSE; char *end; end = ++pattern; while (*end && ']' != *end) { if ('\\' == *end) end++; end++; } if (!*end) return -1; if ('^' == *pattern) { negate = TRUE; pattern++; } if ('-' == *pattern) goto isnormalchar; while (*pattern) { char c; int flag; if ('\\' == *pattern) { pattern++; goto isnormalchar; } if (']' == *pattern) return (1 + match(candidate + 1, end + 1)); switch (pattern[1]) { case '\0': return -1; case '-': c = TOLOWER(*candidate); flag = (c >= TOLOWER(*pattern) && c <= TOLOWER(pattern[2])); if (flag) { if (negate) return -1; else pattern += 2; } else { if (negate) pattern += 2; else return -1; } break; isnormalchar: default: if (TOLOWER(*candidate) == TOLOWER(*pattern)) { if (negate) pattern++; else return (1 + match(candidate + 1, end + 1)); } else if (negate) return (1 + match(candidate + 1, end + 1)); else pattern++; } } return -1; } static int match (char *candidate, char *pattern) { int ret = 0; /* if( (!*candidate) ^ (!*pattern) )return(-1); */ while (1) { if ('\\' == *pattern) goto default_char; switch (*pattern) { case '\r': case '\n': case '\0': switch (*candidate) { case '\0': case '\r': case '\n': return ret; } return -1; case '*': /* return(ret + star(candidate, pattern)); */ { char *c; int len; int clen; while ('*' == *pattern) pattern++; if (!*pattern) return (ret + strcspn(candidate, "\r\n")); clen = strcspn(candidate, "\r\n"); for (c = candidate; clen && *c; c++, clen--, pattern) { switch (*c) { case '\r': case '\n': return -1; } len = match(c, pattern); if (len == clen) return (len + (c - candidate)); } return -1; } /* Not Reached */ case '[': return (ret + bracket(candidate, pattern)); case '?': pattern++; if (!*candidate) return -1; ret++; candidate++; continue; default_char: pattern++; /* Fall Through */ default: if (TOLOWER(*pattern) == TOLOWER(*candidate)) { pattern++; candidate++; ret++; } else return -1; } } } int wildmat (char *candidate, char *pattern) { if (match(candidate, pattern) == strcspn(candidate, "\r\n")) return 1; return 0; } #undef TOLOWER sn-0.3.8/lib/opt.c0000600000175000001440000000372310042571016014320 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * alternative to getopt. DOES NOT FOLLOW GETOPT STANDARD. * opts is string of flag chars for which an argument is expected. * opt_ind is current index to argv. option flag is returned, unless * no options are found, then -1 is returned. If that option flag * is in opts (has an associated value), that value is in opt_arg, * or NULL if no value was found. Caller can set opt_ind to start * option checking at any point. */ static const char ver_ctrl_id[] = "$Id: opt.c 29 2004-04-24 23:02:38Z patrik $"; int opt_ind = 0; char *opt_arg = 0; static int opt_char = 0; int opt_get (int c, char **v, char *opts) { if (0 == opt_ind) opt_ind = 1; while (1) { if (opt_ind >= c) goto done; if (0 == opt_char) { if ('-' != v[opt_ind][opt_char]) goto done; opt_char++; if ('-' == v[opt_ind][opt_char]) if (!v[opt_ind][opt_char + 1]) { opt_arg = 0; opt_char = 0; return -1; } if (!v[opt_ind][opt_char]) /* bare dash */ goto noarg; } if (!v[opt_ind][opt_char]) { opt_ind++; opt_char = 0; continue; } break; } for (; *opts && *opts != v[opt_ind][opt_char]; opts++) ; if (!*opts) goto noarg; if (!v[opt_ind][opt_char + 1]) /* arg not catted with flag */ { if (opt_ind + 1 == c) { opt_arg = 0; return *opts; } opt_arg = v[opt_ind + 1]; opt_ind += 2; } else /* arg catted with flag */ { opt_arg = v[opt_ind] + opt_char + 1; opt_ind++; } opt_char = 0; return *opts; noarg: opt_arg = 0; return v[opt_ind][opt_char++]; done: opt_arg = 0; opt_char = 0; return -1; } sn-0.3.8/lib/readln.c0000600000175000001440000000512210042571016014756 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Get a single line from an fd. */ #include #include #include #include #include #include #include "readln.h" static const char ver_ctrl_id[] = "$Id: readln.c 29 2004-04-24 23:02:38Z patrik $"; int readln_ready (int fd, int tmo, struct readln *rp) { rp->buf = rp->bf; rp->size = sizeof (rp->bf); rp->fd = fd; rp->eaten = rp->used = 0; rp->tmo = tmo; return 0; } void readln_done (struct readln *rp) { if (rp && rp->buf != rp->bf) free(rp->buf); } int readln (register struct readln *rp, char **line, int ch) { char *endp; int len; if (rp->eaten == rp->used) rp->eaten = rp->used = 0; else if (rp->eaten && rp->eaten + 64 >= rp->used) { register char *from; register char *to; int n; from = rp->buf + rp->eaten; to = rp->buf; for (n = rp->used - rp->eaten; n; n--) *to++ = *from++; rp->used -= rp->eaten; rp->eaten = 0; } while (1) { char *lim; int count; lim = rp->buf + rp->used; for (endp = rp->buf + rp->eaten; endp < lim; endp++) if (ch == *endp) goto done; if (rp->size - rp->used < 16) { char *tmp; if (rp->buf == rp->bf) tmp = malloc(rp->size = 504); else tmp = malloc(rp->size *= 2); if (!tmp) return -1; memcpy(tmp, rp->buf + rp->eaten, rp->used - rp->eaten); rp->used -= rp->eaten; rp->eaten = 0; if (rp->buf != rp->bf) free(rp->buf); rp->buf = tmp; } if (rp->tmo > 0) { struct timeval tv; fd_set set; FD_ZERO(&set); for (;;) { FD_SET(rp->fd, &set); tv.tv_usec = 0; tv.tv_sec = rp->tmo; switch (select(rp->fd + 1, &set, 0, 0, &tv)) { case 0: return -1; case -1: if (EINTR == errno) continue; return -1; } break; } } count = read(rp->fd, rp->buf + rp->used, rp->size - rp->used); if (count <= 0) return count; rp->used += count; } done: *line = rp->buf + rp->eaten; len = 1 + endp - (rp->buf + rp->eaten); rp->eaten = 1 + endp - rp->buf; return len; } sn-0.3.8/lib/opt.h0000600000175000001440000000152107564256733014344 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef OPT_GET_H #define OPT_GET_H /* * Alternative to getopt(3) for command line parsing. * opts is a string consisting of option characters WHICH TAKE AN * ARGUMENT. Returns an option character which caller will have to * test if it is recognized, or -1 on end of options. opt_ind and * opt_arg are as for getopt(3). * "--" is supported, meaning end of options. Catted options are * ok; if a catted option takes an arg, the arg is assumed to begin * immediately after the end of that option character, or is the next * argument otherwise. */ extern int opt_ind; extern char *opt_arg; extern int opt_get (int c, char **v, char *opts); #endif sn-0.3.8/lib/openf.c0000600000175000001440000000126510042571016014624 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include "format.h" static const char ver_ctrl_id[] = "$Id: openf.c 29 2004-04-24 23:02:38Z patrik $"; static char buf[MAXPATHLEN + 1]; int openf (int mode, int flags, char *fmt, ...) { int len; va_list ap; va_start(ap, fmt); len = formatv(buf, sizeof (buf) - 1, fmt, ap); va_end(ap); if (len >= sizeof (buf)) { errno = ENAMETOOLONG; return -1; } return open(buf, flags, mode); } sn-0.3.8/lib/out.c0000600000175000001440000000301610042571016014320 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Simple logging function using varargs. */ #include #include #include #include #include "format.h" static const char ver_ctrl_id[] = "$Id: out.c 29 2004-04-24 23:02:38Z patrik $"; static char outbuf[1024]; int writefv (int fd, char *fmt, va_list ap) { int wrote; int used; char *p; int len; char tmp[40]; int er; for (used = wrote = 0; *fmt; fmt++) if ('%' == *fmt) { fmt++; if (!*fmt) break; vachar(*fmt, ap, len); for (; len; len--) { if (used >= sizeof (outbuf)) { er = write(fd, outbuf, used); if (-1 == er) return -1; wrote += er; used = 0; } outbuf[used++] = *p++; } } else { if (used >= sizeof (outbuf)) { er = write(fd, outbuf, used); if (-1 == er) return -1; wrote += er; used = 0; } outbuf[used++] = *fmt; } er = write(fd, outbuf, used); if (-1 == er) return -1; return (wrote + er); } int writef (int fd, char *fmt, ...) { va_list ap; int er; va_start(ap, fmt); er = writefv(fd, fmt, ap); va_end(ap); return er; } sn-0.3.8/lib/readln.h0000600000175000001440000000221607564256733015011 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef READLN_H #define READLN_H /* * Read a line from the given descriptor. A line is considered as * terminated by the ch character. Different ch terminators may * be used in different calls. The length of the line is returned, * INCLUDING the terminator, but no null is appended. The pointer * in *line refers to an internal data structure that will be * overwritten on the next call. Data is not copied like fgets(3)! * readln_ready() must be called to initialize the structure. tmo, * if > 0, is the timeout for reading, else no timeout. readln() * will never return less than a complete line. readln_done() frees * up the structure. */ struct readln { int fd; char *buf; char bf[104]; int size; int used; int eaten; int tmo; }; /* readln.c */ extern int readln_ready (int fd, int tmo, struct readln *rp); extern void readln_done (struct readln *rp); extern int readln (register struct readln *rp, char **line, int ch); #endif sn-0.3.8/lib/out.h0000600000175000001440000000175310042043172014330 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef OUT_H #define OUT_H #include /* * General functions for output. * progname, if set, is prepended to each message written with log() * or fail(). All calls end in a write, so no buffer flushing problems. * writef() works like fprintf(3), writefv() like vfprintf(3); not * all conversion specs are supported. %m is supported, and is the * string obtained from strerror(errno). * writes() writes a null terminated string. */ extern char *progname; extern void logv (char *fmt, va_list ap); extern void log_ (char *fmt, ...); extern void fail (int ex, char *fmt, ...); extern int writefv (int fd, char *fmt, va_list ap); extern int writef (int fd, char *fmt, ...); #define writes(fd, str) \ write(fd, str, (sizeof (str) != sizeof (char *)) ? sizeof (str) - 1 : strlen(str)) #endif sn-0.3.8/lib/openf.h0000600000175000001440000000060407564256733014652 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef OPENF_H #define OPENF_H /* * Open the file specified by the format, with mode mode and open * flags specified by flags. */ extern int openf (int mode, int flags, char *fmt, ...); #endif sn-0.3.8/lib/statf.c0000600000175000001440000000125510042571016014635 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include "format.h" static const char ver_ctrl_id[] = "$Id: statf.c 29 2004-04-24 23:02:38Z patrik $"; static char buf[MAXPATHLEN + 1]; int statf (struct stat *stp, char *fmt, ...) { int len; va_list ap; va_start(ap, fmt); len = formatv(buf, sizeof (buf) - 1, fmt, ap); va_end(ap); if (len >= sizeof (buf)) { errno = ENAMETOOLONG; return -1; } return stat(buf, stp); } sn-0.3.8/lib/nap.c0000600000175000001440000000106410042571016014270 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include static const char ver_ctrl_id[] = "$Id: nap.c 29 2004-04-24 23:02:38Z patrik $"; void nap (int sec, int msec) { struct timeval tv; /* Counting on Linux to update tv */ tv.tv_sec = sec; tv.tv_usec = msec * 1000; while (-1 == select(0, 0, 0, 0, &tv) && EINTR == errno) ; } sn-0.3.8/lib/nap.h0000600000175000001440000000046707564256733014330 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef NAP_H #define NAP_H /* * Like sleep(2), but immune to signals. */ extern void nap (int sec, int msec); #endif sn-0.3.8/lib/b.c0000600000175000001440000000200310042571016013725 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Dynamic buffer routines. */ #include #include #include "b.h" static const char ver_ctrl_id[] = "$Id: b.c 29 2004-04-24 23:02:38Z patrik $"; int b_appendl (register struct b *bp, char *str, int len) { if (NULL == bp->buf) if (NULL == (bp->buf = malloc(bp->size = 248))) return -1; else bp->used = 0; if (len + bp->used >= bp->size) { int newsize; char *tmp; if (len > bp->size) newsize = bp->size + len; else newsize = bp->size * 2; newsize += 16; tmp = malloc(newsize); if (NULL == tmp) return -1; memcpy(tmp, bp->buf, bp->used + 1); bp->buf = tmp; bp->size = newsize; } memcpy(bp->buf + bp->used, str, len); bp->used += len; bp->buf[bp->used] = '\0'; return 0; } sn-0.3.8/lib/log.c0000600000175000001440000000206010042571016014270 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include "format.h" static const char ver_ctrl_id[] = "$Id: log.c 29 2004-04-24 23:02:38Z patrik $"; char *progname = 0; static char logbuf[1024]; void logv (char *fmt, va_list ap) { int len = 0; if (progname) { for (; progname[len]; len++) logbuf[len] = progname[len]; logbuf[len++] = ':'; } len += formatv(logbuf + len, sizeof (logbuf) - len - 2, fmt, ap); logbuf[len] = '\n'; write(2, logbuf, len + 1); } /* Renamed log -> log_ in order to avoid the annoying warning "conflicting types for built-in function `log'" that seems to have appeared in gcc 3.3... */ void log_ (char *fmt, ...) { va_list ap; va_start(ap, fmt); logv(fmt, ap); va_end(ap); } void fail (int ex, char *fmt, ...) { va_list ap; va_start(ap, fmt); logv(fmt, ap); va_end(ap); _exit(ex); } sn-0.3.8/lib/b.h0000600000175000001440000000077107564256733013771 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef B_H #define B_H /* * Append strings to a dynamically allocated buffer. Returns -1 on * failure. Calls malloc(3) implicitly. */ struct b { char *buf; int size; int used; }; #define b_append(bp,str) b_appendl(bp,str,strlen(str)) extern int b_appendl (struct b *bp, char *str, int len); #endif sn-0.3.8/lib/cmdopen.c0000600000175000001440000000363310042571016015143 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include static const char ver_ctrl_id[] = "$Id: cmdopen.c 29 2004-04-24 23:02:38Z patrik $"; int cmdopen (char **command, int *read, int *write) { int p0[2] = { -1, -1 }; int p1[2] = { -1, -1 }; int pid = 0; int er = 0; int s; if (read) er = pipe(p0); if (write) if (0 == er) er = pipe(p1); if (-1 == er) goto fail; pid = fork(); if (-1 == pid) goto fail; if (0 == pid) { if (read && write) { dup2(p0[0], 0); dup2(p1[1], 1); } else { if (read) { close(p0[0]); dup2(p0[1], 1); } if (write) { close(p1[1]); dup2(p1[0], 0); } } execvp(command[0], command); exit(19); } if (!(read && write)) { if (read) { close(p0[1]); *read = p0[0]; } if (write) { close(p1[0]); *write = p1[1]; } } else { *read = p0[0]; *write = p1[1]; } switch (waitpid(pid, &s, WNOHANG)) { case 0: return pid; case -1: pid = -1; break; default: if (WIFEXITED(s)) pid = 0 - WEXITSTATUS(s); else if (WIFSIGNALED(s)) pid = 0 - (128 + WTERMSIG(s)); } fail: if (p0[0] > -1) close(p0[0]); if (p0[1] > -1) close(p0[1]); if (p1[0] > -1) close(p1[0]); if (p1[1] > -1) close(p1[1]); return pid; } int cmdopensh (char *cmd, int *read, int *write) { char *commands[4] = { "sh", "-c", 0, 0 }; commands[2] = cmd; return cmdopen(commands, read, write); } int cmdwait (int pid) { int s; if (-1 == waitpid(pid, &s, 0)) return -1; if (WIFEXITED(s)) return WEXITSTATUS(s); else if (WIFSIGNALED(s)) return (128 + WTERMSIG(s)); return 0; } sn-0.3.8/lib/statf.h0000600000175000001440000000045007564256733014663 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef STATF_H #define STATF_H struct stat; extern int statf (struct stat *stp, char *fmt, ...); #endif sn-0.3.8/lib/defs.h0000600000175000001440000000052610042066025014441 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef DEFS_H #define DEFS_H #undef FALSE /* Mac OS X apparently defines these */ #undef TRUE typedef enum { FALSE, TRUE } bool; #endif /* DEFS_H */ sn-0.3.8/lib/cmdopen.h0000600000175000001440000000140507564256733015170 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef CMDOPEN_H #define CMDOPEN_H /* * Simple wrapper to fork/exec, without any fancy stuff. */ /* command must be NULL terminated, like execv(). If read/write is not NULL, the command is opend for reading/writing and the descriptor is placed in *read / *write. Returns the pid */ extern int cmdopen (char **command, int *read, int *write); /* Use sh -c, otherwise as above */ extern int cmdopensh (char *cmd, int *read, int *write); /* Returns same value that sh would: <= 255 means the exit code; > 255 means signal number + 255 */ extern int cmdwait (int pid); #endif sn-0.3.8/lib/format.c0000600000175000001440000000310010043507662015002 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include "defs.h" #include "format.h" static const char ver_ctrl_id[] = "$Id: format.c 36 2004-04-27 16:52:02Z patrik $"; static char *ichars = "0123456789abcdefghijklmnopqrstuvwxyz"; char *istr (int i, int base, char *tmp) { int n; bool negative = FALSE; tmp[n = 39] = '\0'; if (i < 0) { negative = TRUE; i = 0 - i; } do tmp[--n] = ichars[i % base]; while (i /= base); if (negative) tmp[--n] = '-'; return (tmp + n); } char *uistr (unsigned int u, int base, char *tmp) { int n; tmp[n = 39] = '\0'; do tmp[--n] = ichars[u % base]; while (u /= base); return (tmp + n); } int formatv (char *buf, int size, char *fmt, va_list ap) { char *p; int len; char tmp[40]; char *lim; char *start; lim = buf + size; start = buf; for (; *fmt; fmt++) if ('%' == *fmt) { fmt++; vachar(*fmt, ap, len); for (; len; len--) { *buf++ = *p++; if (buf >= lim) goto done; } } else { *buf++ = *fmt; if (buf >= lim) break; } done: *buf = '\0'; return (buf - start); } int formats (char *buf, int size, char *fmt, ...) { va_list ap; va_start(ap, fmt); return formatv(buf, size, fmt, ap); va_end(ap); } sn-0.3.8/lib/tokensep.c0000600000175000001440000000134210043770471015350 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include static const char ver_ctrl_id[] = "$Id: tokensep.c 40 2004-04-28 18:00:25Z patrik $"; char *tokensep (char **p, char *delim) { char map[256] = { 0, }; char *start; char *end; while (*delim) { map[(unsigned) *delim] = 1; delim++; } for (start = *p; *start && map[(unsigned) *start]; start++) ; if (!*start) return NULL; for (end = start; *end && !map[(unsigned) *end]; end++) ; if (*end) { *p = end + 1; *end = '\0'; } else *p = end; return start; } sn-0.3.8/lib/Makefile0000600000175000001440000000032507564256733015032 0ustar patrikusers00000000000000all: stuff.a libstuff.a stuff.a: set -e; for i in *.c; \ do $(CC) -c -O $$i -o `basename $$i .c`.o; done ar rc stuff.a *.o ranlib stuff.a libstuff.a: stuff.a ln -s stuff.a libstuff.a clean: rm -f *.o *.a sn-0.3.8/lib/format.h0000600000175000001440000000340310041245700015003 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef FORMAT_H #define FORMAT_H #include /* * Formatting like printf(3). Not all format specs are supported, * in particular, no field specs allowed. */ /* Print integer into tmp, returns pointer to start of it */ extern char *istr (int i, int base, char *tmp); extern char *uistr (unsigned int u, int base, char *tmp); /* 2 forms, like snprintf */ extern int formatv (char *buf, int size, char *fmt, va_list ap); extern int formats (char *buf, int size, char *fmt, ...); /* "Returns" (in p) string based on format character c. Length of string, which may NOT be null terminated, is in len. This is now a macro because it uses va_arg and you can only use that in a single function call. Macro-ized by Chris Niekel */ #define vachar(c, ap, len) \ { \ int e, i; \ unsigned u; \ p = 0; \ switch(c) \ { \ case 'S': len = va_arg(ap, int); p = va_arg(ap, char *); break; \ case 's': p = va_arg(ap, char *); len = strlen(p); break; \ case 'i': /* fall through */ \ case 'd': i = va_arg(ap, int); p = istr(i, 10, tmp); len = strlen(p); break; \ case 'u': u = va_arg(ap, unsigned); p = uistr(u, 10, tmp); len = strlen(p); break; \ case 'o': u = va_arg(ap, unsigned); p = uistr(u, 8, tmp); len = strlen(p); break; \ case 'x': u = va_arg(ap, unsigned); p = uistr(u, 16, tmp); len = strlen(p); break; \ case 'm': p = strerror(errno); len = strlen(p); break; \ case '%': len = 1; p = "%"; break; \ default: len = 0; p = ""; \ } \ } #endif sn-0.3.8/lib/tokensep.h0000600000175000001440000000100207564256733015364 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef TOKENSEP_H #define TOKENSEP_H /* * Like strtok(3), but reentrant. Returns the next token, as * delimited by any of the characters in delim, or NULL if no more * tokens. *p is altered on each call, and is the start of the * buffer to read tokens from. */ extern char *tokensep (char **p, char *delim); #endif sn-0.3.8/snlockf.c0000600000175000001440000000177110042571016014410 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * lockf() a file, for shell scripts. */ #include #include #include #include static const char ver_ctrl_id[] = "$Id: snlockf.c 29 2004-04-24 23:02:38Z patrik $"; int main (int argc, char **argv) { char *p; char buffer[40]; progname = ((p = strrchr(argv[0], '/')) ? p + 1 : argv[0]); if (argc != 1) /* FIXME: Usage string is never printed... */ fail(1, "Usage: %s (no arguments or options)\n" "%s always locks (using lockf()) descriptor 3, which should be open for writing.", progname, progname); if (lockf(3, F_TLOCK, 0) == -1) _exit(2); formats(buffer, 40, "%d\n", getpid()); /* Tell the script what our pid is, so it can kill us */ write(1, buffer, strlen(buffer)); fsync(1); sleep(60 * 60); _exit(0); } sn-0.3.8/args.h0000600000175000001440000000100007564256733013720 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef ARGS_H #define ARGS_H #include "lib/readln.h" extern char *args[20]; extern char args_outbuf[1024]; extern char args_inbuf[1024]; extern int args_write (int fd, char *fmt, ...); extern int args_read (struct readln *rp); extern void args_report (char *tag); extern void args_flushtodot (struct readln *rp); #endif sn-0.3.8/snnewgroup.c0000600000175000001440000001152010043507662015160 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Work like the script. */ #include #include #include #include #include #include #include "config.h" #include "addr.h" #include "parameters.h" #include "valid.h" #include #include #include static const char ver_ctrl_id[] = "$Id: snnewgroup.c 36 2004-04-27 16:52:02Z patrik $"; int debug = 0; void usage (void) { fail(1, "Usage:%s newsgroup [server [port]]", progname); } void makedir (char *fn) { struct stat st; if (0 == stat(fn, &st)) { if (!S_ISDIR(st.st_mode)) fail(1, "\"%s\" exists but is not a directory, remove it first", fn); LOG1("directory \"%s\" already exists, ok", fn); return; } if (ENOENT != errno) fail(2, "Weird error on stat(%s):%m", fn); LOG1("Creating directory \"%s\"...", fn); if (-1 == mkdir(fn, 0755)) fail(2, "Can't mkdir(%s):%m", fn); LOG1("Done"); } int touch (char *fn) { int fd; LOG1("Creating \"%s\" file...", fn); fd = open(fn, O_WRONLY | O_CREAT, 0644); if (fd > -1) { LOG1("Done"); } else LOG("Can't create %s:%m", fn); return fd; } int main (int argc, char **argv) { struct stat st; char *outgoing; char *groupname; char *server; char *p; int port; int i; bool z; progname = ((p = strrchr(argv[0], '/')) ? p + 1 : argv[0]); port = 0; z = FALSE; outgoing = server = 0; while ((i = opt_get(argc, argv, "")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'd': debug++; break; case 'V': version(); _exit(0); case 'x': LOG("Option \"x\" no longer supported, ignored"); break; case 'z': z = TRUE; break; default: usage(); } if (opt_ind == argc) usage(); groupname = argv[opt_ind++]; if (opt_ind < argc) server = argv[opt_ind++]; if (opt_ind < argc) if ((port = strtoul(argv[opt_ind++], &p, 10)) <= 0 || *p) fail(1, "Invalid port number"); if (argc != opt_ind) usage(); if (0 == strcmp(groupname, JUNK_GROUP)) { server = 0; port = 0; } else { if (!is_valid_name(groupname)) /* cheat */ fail(1, "Illegal newsgroup name \"%s\"", groupname); for (p = groupname; *p; p++) if (*p <= 'Z' && *p >= 'A') *p += 'a' - 'A'; } if (z) LOG("Option \"z\" ignored" #ifdef USE_ZLIB " Use \"touch %s/%s/.compress\" instead", snroot, groupname #endif ); umask(0); parameters(TRUE); if (-1 == chdir(snroot)) { if (ENOENT == errno) fail(1, "\"%s\" doesn't exist, create it first", snroot); if (ENOTDIR == errno) fail(1, "\"%s\" isn't a directory, remove it or set the SNROOT environment variable", snroot); fail(2, "Can't chdir to \"%s\":%m", snroot); } if (0 == stat(groupname, &st)) { if (S_ISDIR(st.st_mode)) fail(1, "group %s already exists under %s", groupname, snroot); fail(1, "Remove file %s/%s first", snroot, groupname); } else if (ENOENT != errno) fail(2, "odd stat error:%m"); if (server) { if (!is_valid_name(server)) /* Cheat again */ fail(1, "\"%s\" doesn't looks like a valid server name", server); if (!*server) fail(1, "server name not valid"); if (!port) port = 119; makedir(".outgoing/"); i = sizeof ("../.outgoing/") + strlen(server) + 16; if (!(outgoing = malloc(i))) fail(2, "No memory"); formats(outgoing, i - 1, "../.outgoing/%s:%d", server, port); makedir(outgoing + 3); } makedir(groupname); if (0 == chdir(groupname)) { int fd; if ((fd = touch(".serial")) > -1) { i = write(fd, "0\n", 2); close(fd); if (2 == i) { if ((fd = touch(".created")) > -1) { close(fd); if ((fd = touch(".times")) > -1) { if (!outgoing || 0 == symlink(outgoing, ".outgoing")) { LOG("Created %s newsgroup \"%s\" under %s", outgoing ? "remote" : "local", groupname, snroot); _exit(0); } else LOG("Can't create symlink:%m"); unlink(".times"); } unlink(".created"); } } else LOG("Can't write \".serial\" file:%m"); unlink(".serial"); } chdir(".."); if (-1 == rmdir(groupname)) LOG("Can't remove \"%s\" directory to clean up:%m", groupname); } _exit(2); } sn-0.3.8/body.h0000600000175000001440000000100507564256733013726 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef BODY_H #define BODY_H #ifndef USE_ZLIB static const char body_[] = "$Compiled: without ZLIB $"; #define body(foo,bar) 0 #else #include #include static const char body_[] = "$Compiled: with ZLIB_VERSION=" ZLIB_VERSION " $"; extern int body (char **artbod, int *len); #endif /* USE_ZLIB */ #endif sn-0.3.8/THANKS0000600000175000001440000000232210106142506013510 0ustar patrikusers00000000000000The following people have participated in developing sn, either with code, testing, bug reports or suggestions. Harold Tay Marko Macek, fixes esp. for Netscape News; script. Richard Henderson, comments and code review. Nicolai Langfeldt, actually tested it. David Holland, idea for mail-to-news forwarding. Kal Ng, bugs and heavy testing. Eric Ortega, snlogin bug. Gabor Z. Papp, ease-of-use, pitfalls, rnews conversion, fido issues. Steven Flintham, LIST format error, compile-time error. Sam Mulvey, compile-time error. Zygo Blaxell, bugs and comments too numerous to mention. Patrik Rådman, comments, bugs, and suggestions. Felix von Leitner, NEWGROUPS date bug. Andreas Fuchs, DNEWS problem. Bob Pepin, portability issues. "vinceh", doc error regarding tcpserver options. Matan Ziv-Av, snexpire bug. Eric Wick, comments, bug reports and testing. Ulf Tigerstedt, bug reports. Ray Lischner, bug reports and testing. Ralph Corderoy, bug reports, comments and suggestions. Hilko Bengen, testing on HP-UX. Andras Bali, rnews batches with zero-length articles bug. Paul Pluzhnikov, bug reports and suggestions. Peter Fichtner, bug reports. Christian Garbs, bug reports. Dick Streefland Chris Niekel Sebastian D.B. Krause Phil Richards sn-0.3.8/TODO0000600000175000001440000000105207564256733013312 0ustar patrikusers00000000000000Figure out which man sections to use... does anyone read them anyway? Docs should mention flow and structure. When deleting a newsgroup, safer to remove all references to that newsgroup in the database, rather than scanning for IDs, and then deleting those. Use xover for filtering. If the file .filter exists, then have snfetch run BINDIR/snfilter, which sends XOVER and does some processing, passing the output to .filter shell script. The output of this whole chain to snfetch is a series of number id lines, which represent the desired articles. sn-0.3.8/sndumpdb.8.in0000600000175000001440000000125607564256733015142 0ustar patrikusers00000000000000.TH sndumpdb,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME sndumpdb \- dump contents of sn database. .SH SYNOPSIS .B sndumpdb (no arguments) .br .SH DESCRIPTION .B sndumpdb dumps the contents of the .B sn database (in no particular order) with one record per line, in the format .br .I newsgroup id serial .br where .I newsgroup is the newsgroup that article with message ID .I id can be found, with local serial number .IR serial . .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .SH FILES The database files are .RI !!SNROOT!!/{ .table , .chain , .newsgroup }. sn-0.3.8/INTERNALS0000600000175000001440000001161507564256733014112 0ustar patrikusers00000000000000This file describes how sn works, how it was designed. It probably won't interest anyone but a programmer. The precursor to sn was tn, from which the batching idea was hatched. But sn is a complete rewrite. sn uses mmap heavily. The idea is that if more than one snntpd process is running, they can share the same memory. The advantage gained is probably quite small though, considering the number of files. sn uses caching heavily. There is a separate cache module in cache.c, which can cache any kind of object pointer. sn uses it to cache article files (art.c), newsgroup information (group.c), and time-stored information (time.c). This strategy makes programming for the upper layers rather simpler, after the base methods are defined. The caching of article files for reading is mainly to help with aliasing. Since the behaviour of news readers is to read each article head in quick succession, there is little to be gained from a large cache. Cross posts tend to occur between newsgroups of similar content, so faulting in an aliased article file will save time, because that file may well be requested again soon. There is no provision for "faulting" ahead. An article file consists of an index, and following it, the heads and bodies of the articles. Whenever an article is to be stored, it is preferable to bunch all the heads together at the beginning of the file near the index, and the bodies after. sn appends the head and the body both into the file the moment it is received. Then when all slots are full, it rewrites the entire file so the heads are at the beginning. This strategy means the file spends much less time in an incosistent state, but at the expense of the time to write out a new file and rename it. It also means that the storing process can be terminated at any time and still control the damage. This means non-full files are not in optimum format, but only the last file in a newsgroup will be non-full. snfetch doesn't lock a newsgroup when it runs. I don't think this is a fair restriction to make. The news spool should simply and readily accept any input from anywhere. Not locking means you can run any number of storage processes at a time. If this is done, there is a danger of storing duplicate articles, since there is a window between when the ID is checked for existence, and when the article is committed to the spool. My decision to depart from the traditional /news/group/name/number structure was based on the observation that most modem users (for whom sn was designed) will not be interested in the tens of thousands of newsgroups available, but in only a few dozen at most. Keeping the directory structure simple made things easier for me. I may consider hashing the newsgroup names into another level of directories in the future, but I don't think its important. Being able to use '*' in shell scripts is awfully handy. The decision to keep control information in the group directory itself is in keeping with the general per-newsgroup structure of usenet. The exception is upstream servers, which are not a strictly per-newsgroup sort of thing. For this, I use a server directory for each server, and have each newsgroup maintain a symlink to it. This is the .outgoing file in each (global) newsgroup directory. I think this is far better than having a configuration file and parser (thanks djb), it lends itself well to automatic operation, and is also more obvious. Although the structure of usenet news centres around the newsgroup, there is also an important provision that articles be referenced by their ID. As a result, it is necessary to somehow map article IDs to their location. Any dbm can do this, but I wrote my own for 2 reasons: I wanted multiple writers; and I wanted database information shared among all instances of processes with the database open. This is accomplished using mmap(2) with MAP_SHARED. To keep things simple (for me), this database is a hash table using open chaining, and it cannot be resized. There are 2 files for this, the index of the table, and the chains. The index (.table) is just an array dumped to disk; the chain file (.chain) is an arena managed by a simple free-list allocator (allocate.c). This allocator maintains a linked list of every size of free object available, in increments of 4 bytes. If the required size is not found on the appropriate free list, the file is extended and the new space returned. The allocator doesn't know how to sweep and join, or split. This means under certain degenerate conditions (like, everyone suddenly decides never again to use message IDs of 84 bytes after having used them for months), the chain file could have large amounts of space in it that never get allocated. To keep things simple, I've kept the number of options per sn tool very low, and kept the function of each distinct. I think its much better to combine these tools in unix fashion using shell scripts, than to confuse someone with multiple options. sn-0.3.8/config.h0000600000175000001440000000420510042043172014213 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef CONFIG_H #define CONFIG_H /* * Before editing this, edit Makefile for SNROOT. */ /* * How many articles per file. 10 is good. The theory is: * ARTSPERFILE = (system page size in bytes)/(ave. size of news header) * This is also the granularity of expiration; a multiple of this * many articles will be expired at a time. Do NOT make it 1; * things will break. */ #define ARTSPERFILE 10 /* * How large the hash table should be. Currently this size is * static. Figure about 1000 articles per group, so the following * should be good for 10-40 groups. */ #define DH_SIZE 10240 /* * Default expiration age, in seconds. */ #define DEFAULT_EXP 60*60*24*7 /* * MAX_POST_AGE is the time in seconds a posting can remain unposted * before it is deleted. Refers to global groups only. */ #define MAX_POST_AGE 60*60*24*7 /* * Nothing more to edit for general configuration. */ #define version() writef(2, "%s version " VERSION /*" %s"*/ "\n", progname/*, rcsid*/) #define log_with_pid() \ do { char * p; int i; \ if ((p = malloc(i = strlen(progname) + 32))) \ formats(p, i-1, "%s[%u]", progname, getpid()); \ progname = p; } while (0) /* * Alignment on your machine. For i386, leave at 4. For others, I * don't know. */ #define ALLO_ALIGNMENT 4 /* * You can stop editing now. */ #ifdef __sun__ /* For Solaris */ #define DONT_HAVE_DIRFD #endif extern int debug; #define LOG log_ #define LOG1 if (debug >= 1) log_ #define LOG2 if (debug >= 2) log_ #define LOG3 if (debug >= 3) log_ #define FAIL fail #include #ifndef NAME_MAX /* For Solaris */ #define NAME_MAX (MAXNAMELEN - 1) #endif #if NAME_MAX > 512 #define GROUPNAMELEN 512 #else #define GROUPNAMELEN NAME_MAX #endif #ifndef MAP_FAILED /* For Linux libc5 and HP-UX 10 */ #define MAP_FAILED ((void *) -1) #endif #define JUNK_GROUP "=junk" #undef FALSE /* Mac OS X apparently defines these */ #undef TRUE typedef enum { FALSE, TRUE } bool; #endif /* CONFIG_H */ sn-0.3.8/addr.c0000600000175000001440000001402310044035540013654 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * The functions here are based (mainly) on RFC 822 / 2822 */ #include #include "config.h" #include "addr.h" static const char ver_ctrl_id[] = "$Id: addr.c 43 2004-04-28 23:16:48Z patrik $"; /* int addr_addrspec (char *buf) { char *p; int len; p = buf; len = addr_localpart(p); if (!len) return 0; p += len; if ('@' != *p) return 0; p++; len = addr_domain(p); if (!len) return 0; p += len; return (p - buf); } */ int addr_idleft (char *buf); int addr_idright (char *buf); /* * Checks for a valid Message-ID */ int addr_msgid (char *buf) { char *p; int len; p = buf; if (*p != '<') return 0; p++; len = addr_idleft(p); if (!len) return 0; p += len; if (*p != '@') return 0; p++; len = addr_idright(p); if (!len) return 0; p += len; if (*p != '>') return 0; p++; return (p - buf); } #define C(x) case x: int addr_domain (char *buf) { char *p; bool lb; p = buf; if ((lb = ('[' == *p))) p++; for (; ; p++) switch (*p) { C('-') if (lb) return 0; /* else fall through */ C('.') if (p == buf || p[-1] == '.' || !p[1]) return 0; else continue; /* Bugfix: no fall through */ C('a') C('b') C('c') C('d') C('e') C('f') C('g') C('h') C('i') C('j') C('k') C('l') C('m') C('n') C('o') C('p') C('q') C('r') C('s') C('t') C('u') C('v') C('w') C('x') C('y') C('z') C('A') C('B') C('C') C('D') C('E') C('F') C('G') C('H') C('I') C('J') C('K') C('L') C('M') C('N') C('O') C('P') C('Q') C('R') C('S') C('T') C('U') C('V') C('W') C('X') C('Y') C('Z') C('_') /* permit underscore.. why not */ if (lb) return 0; /* else fall through */ C('0') C('1') C('2') C('3') C('4') C('5') C('6') C('7') C('8') C('9') continue; C(']') if (lb) p++; /* fall through */ else return 0; default: if (buf != p) return (p - buf); else return 0; } } int addr_localpart (char *buf) { char *p; bool dq, bs; p = buf; dq = bs = FALSE; for (p = buf; *p; p++) if (!bs) switch (*p) { case '"': dq = (!dq); break; case '\\': bs = TRUE; break; case '@': if (dq) return 0; return (p - buf); case '(': case ')': case '<': case '>': case ',': case ';': case ':': case '[': case ']': if (!dq) return 0; break; default: if (!dq && *p <= ' ') return 0; } else bs = FALSE; /* end of string */ return 0; } /* * Like strchr() but ignores comments and quoted strings */ char *addr_qstrchr (char *str, int find) { bool bs, dq, cm; for (bs = dq = cm = FALSE; *str; str++) if (!bs) if (!cm) switch (*str) { case '"': dq = (!dq); break; case '\\': bs = TRUE; break; case '(': if (!dq) cm = TRUE; break; default: if (dq || cm || (char) find != *str) break; return str; } else { if (!dq && ')' == *str) cm = FALSE; } else bs = FALSE; return NULL; } int addr_unescape (char *from, char *to, int len) { char *dst; bool bs, dq; bs = dq = FALSE; dst = to; for (; len > 0; from++, len--) { if (!bs) switch (*from) { case '"': dq = (!dq); continue; case '\\': bs = TRUE; continue; default: ; } else bs = FALSE; if (dst != from) *dst = *from; dst++; } *dst = '\0'; return (dst - to); } /* * Used by addr_msgid to check the part left of the '@' */ int addr_idleft (char *buf) { char *p; bool dq, bs = FALSE; p = buf; if ((dq = (*p == '"'))) p++; for (; *p; p++) if (!bs) switch (*p) { case '"': if (dq && *++p == '@') return (p - buf); else return 0; case '\\': bs = TRUE; break; case '>': return 0; case '@': if (!dq) return (p - buf); else return 0; default: if (*p < 33 || *p > 126) return 0; } else bs = FALSE; return 0; } /* * Used by addr_msgid() to check the part right of the '@' */ int addr_idright (char *buf) { char *p; bool lb, bs = FALSE; p = buf; if ((lb = (*p == '['))) p++; for (; *p; p++) if (!bs) switch (*p) { case '[': return 0; case ']': if (lb && *++p == '>') return (p - buf); else return 0; case '\\': bs = TRUE; break; case '>': if (!lb) return (p - buf); else return 0; default: if (*p < 33 || *p > 126) return 0; } else bs = FALSE; return 0; } sn-0.3.8/contrib/0000700000175000001440000000000010106142566014242 5ustar patrikusers00000000000000sn-0.3.8/contrib/simple_authentication/0001700000175000001440000000000010106142566020633 5ustar patrikusers00000000000000sn-0.3.8/contrib/simple_authentication/nntp.sh0000600000175000001440000000422510042614733022151 0ustar patrikusers00000000000000#!/bin/bash # simple script for doing authentication with sn, # the usernames and passwords are defined inline # in this file # settings timeout=20 # seconds! SNNTPD=/usr/local/sbin/snntpd #users (array) users=(user1 user2 user3) #corresponding passwords (must not be shorter than users array!) passwords=(password1 password2 password3) # end settings checkuser () { user_count=${#users[@]} index=0 while [ "$index" -lt "$user_count" ] ; do if [ "${users[$index]}" = "$1" ] ; then if [ "${passwords[$index]}" = "$2" ] ; then return 0 else return 1 fi fi let "index = $index + 1" done return 1 } printf "200 Hi, you can post (sn simple auth script)\r\n" haveuser="" # no one yet # simple loop as long as no timeout or other read error occurs # loop also terminates when exec()'ing snntpd, for obvious reasons while read -t $timeout currentline ; do lenm1=`expr ${#currentline} - 1` currentline=${currentline:0:$lenm1} switchpart=`echo ${currentline:0:14} | tr [a-z] [A-Z]` #switchpart=`echo ${currentline:0:14} | tr [:lower:] [:upper:]` case "$switchpart" in "LIST EXTENSIONS") printf "202-Extensions supported:\r\n" printf " AUTHINFO USER\r\n" printf " MODE READER\r\n" printf ".\r\n" haveuser="" ;; "MODE READER") # some newsreaders (e.g. MacSoup ;-)) # send this command at the very beginning printf "200 You are already in this mode. Ignored.\r\n" ;; "QUIT") exit 0 ;; "AUTHINFO USER") # extract username haveuser=${currentline:14} printf "381 Continue with authorization sequence\r\n" # echo $haveuser ;; "AUTHINFO PASS") # check password havepass=${currentline:14} # extract password if checkuser $haveuser $havepass ; then printf "281 Authorization accepted\r\n" export POSTING_OK=1 # is this necessary? exec $SNNTPD -S logger -p news.info else printf "482 Authorization rejected $msg\r\n" fi haveuser="" ;; *) printf "480 Authorization required for this command\r\n" haveuser="" ;; esac donesn-0.3.8/contrib/simple_authentication/README0000600000175000001440000000032010042614733021506 0ustar patrikusers00000000000000This is a simple script for username/password authentication. The usernames and passwords are defined inline. by Johannes Berg (e-mail unknown), patched by Dominic Battre (e-mail unknown) and Patrik Rådman. sn-0.3.8/contrib/snget.sh0000700000175000001440000001204410101234052015705 0ustar patrikusers00000000000000#!/bin/bash # This script is no longer used, but you can look at it to get an # idea of how to feed the news spool in some other way. # Fetch articles via NNTP. Get the groups to fetch from the command # line, or else fetch all global groups in the spool. # Errors and messages to fd 2. [ -z "$SNROOT" ] && SNROOT="/var/sn" PATH=/usr/sn:$PATH export PATH SNROOT umask 022 progname=`basename $0` case "$1" in "-V") echo "$progname version 0.2.2" >&2; exit 0 ;; "-"*) echo "Usage: $progname [newsgroup]..." >&2; exit 1;; esac if which tcpclient >/dev/null; then use_suck= elif which suck >/dev/null && which rpost >/dev/null; then use_suck="-c" # Also used as option to snstore else echo "Can't find tcpclient or suck and rpost" >&2 exit 1 fi function fail () { echo "$@" >&2; exit 2; } function oops () { echo "$@" >&2; } cd $SNROOT || fail "Can't cd to $SNROOT" # # Collect newsgroups # newsgroups="$@" if [ -z "$newsgroups" ]; then for i in `ls -1`; do [ -d $i -a -d $i/.outgoing ] && newsgroups="$newsgroups $i" done fi # # Using suck? # function putquit () { echo "QUIT " >&7; } declare -fx putquit oops fail # # Define the function which will fetch articles from a particular # server. Articles fetched are written to standard output, so # caller must have redirected. # if [ -z "$use_suck" ]; then # # Not using suck, use tcpclient. # function runfetch () { local max= local group="$1" local serial= serial=`cat /var/sn/$group/.serial` 2>/dev/null [ -z "$serial" ] && serial="0" [ "$serial" = "0" ] && max=200 snfetch $group $serial $max && mv /var/sn/$group/.serial.tmp /var/sn/$group/.serial : } function getfromserver () { local server="$1" local port="$2" shift 2 # # Want arg to sh -c to expand before exec. # Optional: replace "sh -c ..." with # "throttle 6r1024 sh -c ..." below to prevent snget from hogging # the modem. The 1024 means to throttle to 1024 bytes/sec. # tcpclient -RHl0 -T10 $server $port sh -c \ 'snlogin '"$1"'; f=$? if [ $f = 0 -o $f = 9 ]; then snpost '"$1"'; else exit 1; fi for g in '"$*"'; do runfetch $g || break; done; putquit' } declare -fx runfetch else # # Using suck # function getfromserver () { local server="$1" local port="$2" local serial local username= local password= local extension="$$" local sedcmd='s/ // /^\. $/d /^Path:/d /^NNTP-Posting-Host:/d' shift 2 if [ -f .outgoing/$server:$port/username ]; then username=`cat .outgoing/$server:$port/username` 2>/dev/null [ "$username" ] && username="-U $username" fi if [ -f .outgoing/$server:$port/password ]; then password=`cat .outgoing/$server:$port/password` 2>/dev/null [ "$password" ] && password="-P $password" fi # # Send outgoing articles first. Can't lock article, oh well. # ( cd .outgoing/$server:$port && for p in `ls -1 '$'* 2>/dev/null`; do sed "$sedcmd" $p |rpost $server -N $port $username $password && rm -f $p done ) >&2 # # Create a temporary sucknewsrc file, run suck in "stdout" mode # for g in "$@"; do serial=`cat $g/.serial` 2>/dev/null [ -z "$serial" ] && serial=0 echo "$g $serial" done >sucknewsrc.$extension suck $server -p .$extension -N $port -H -q $username $password | sed -e 's/$/ /' -e 's/^\.\([^ ].*\)/..\1/' if [ -f ./suck.newrc.$extension ]; then while read group serial; do [ -z "$group" -o -z "$serial" ] && continue echo "$serial" >$group/.serial done <./suck.newrc.$extension else oops "Couldn't run suck properly" fi rm -f suck*.$extension } fi declare -fx getfromserver # # Sort newsgroups primarily by server, secondarily by name. # tmp=/tmp/$progname.$$ trap 'rm -f $tmp' 0 enable -n pwd; : prefix="./" for i in $newsgroups; do [ ! -d ${prefix}$i ] && fail "Not subscribed to group $i" [ ! -L ${prefix}$i/.outgoing ] && continue if cd ${prefix}$i/.outgoing; then prefix="../../" address=`basename \`command pwd\`` case "$address" in '') continue ;; ?*:?*) echo `echo "$address" |tr ':' ' '` $i ;; ?*:) echo `echo "$address" |tr ':' ' '` 119 $i ;; ?*) echo "$address 119 $i" ;; *) continue ;; esac fi done |sort >$tmp # # then go get them. # { read oldserver oldport groups [ -z "$groups" ] && exit 0 while read server port group; do if [ "$server" = "$oldserver" -a "$port" = "$oldport" ]; then groups="$groups $group" else if [ "$groups" ]; then getfromserver $oldserver $oldport $groups oldserver=$server oldport=$port groups=$group fi fi done getfromserver $oldserver $oldport $groups } <$tmp |snstore $use_suck && # # Index the news spool, maybe. # if [ -z "$*" ]; then if which sn-words >/dev/null; then if which index-make >/dev/null; then if sn-words * |index-make >.sn-index.new; then mv -f .sn-index.new .sn-index else rm -f .sn-index fi 2>/dev/null fi fi fi : sn-0.3.8/contrib/auto_subscribe/0001700000175000001440000000000010106142566017254 5ustar patrikusers00000000000000sn-0.3.8/contrib/auto_subscribe/snnewgroups0000600000175000001440000000533610042614733021600 0ustar patrikusers00000000000000#!/bin/sh # ---------------------------------------------------------------------- # snnewgroups - check for new groups and add them to the sn news server # # This script checks for new groups on the upstream news servers, using # the NEWGROUPS NNTP command. When a server is checked for the first # time, the complete group list is downloaded with LIST ACTIVE. The # following files in each .outgoing/:/ directory are used: # # .newgroups-stamp - time stamp for the NEWGROUPS command # .newgroups-filter - newsgroup filter patterns # # The pattern file should contain one newsgroup name pattern per line, # preceded by either '+' or '-'. A '+' selects matching group names, and # a '-' deselects matching group names. The first match takes precedence. # The pattern matching follows the Bourne shell rules for pattern matching. # To avoid surprises, a server is skipped when the pattern file is missing. # # Any missing group description files (.info) are created automatically. # # 2003-03-03, Dick Streefland # ---------------------------------------------------------------------- stamp=.newgroups-stamp filter=.newgroups-filter tmp=/tmp/infogroups.$$ trap "rm -f $tmp" 0 cd ${SNROOT:-/var/spool/sn} || exit 1 for dir in .outgoing/*:* do server=${dir#.outgoing/} server=${server%:*} port=${dir#*:} # --- get the group selection patterns test -f $dir/$filter || continue patt=`sed -n 's/^\([-+]\) *\(.*\)/\2)m=\1;;/p' $dir/$filter` # --- first, create the new time stamp to avoid missing new groups old=$dir/$stamp new=$dir/$stamp.new rm -f $new touch $new || exit 1 chown news.news $new > /dev/null 2>&1 # --- get the new groups, or get all groups the first time if [ -f $old ] then cmd="NEWGROUPS `date -u -r $old '+%y%m%d %H%M%S GMT'`" else cmd="LIST ACTIVE" fi echo -e "MODE READER\n$cmd\nquit" | { nc $server $port || rm $new ; } | grep '^[a-z]' | while read group junk do m=- eval case "\"$group\"" in $patt esac case "$m" in +) echo "$group" ;; esac done | while read group do if [ ! -d "$group" ] then /usr/sbin/snnewgroup "$group" $server $port fi done mv $new $old # --- get missing group descriptions: find * -lname "../$dir" -printf '%h\n' | while read group do [ -f "$group/.info" ] || echo "$group" done | head -99 > $tmp if [ -s $tmp ] then ( echo "MODE READER" sed 's/^/LIST NEWSGROUPS /' $tmp echo "QUIT" ) | nc $server $port | sed -n 's///; /^[a-z]/p' | while read group descr do echo "${descr:-?}" > $group/.info chown news.news $group/.info > /dev/null 2>&1 done while read group do if [ ! -f $group/.info ] then echo "?" > $group/.info chown news.news $group/.info > /dev/null 2>&1 fi done < $tmp fi done sn-0.3.8/contrib/auto_subscribe/README0000600000175000001440000000126710042614733020142 0ustar patrikusers00000000000000I'm sending you a script that can be run as a cron job, to automatically subscribe to newsgroups, based on a pattern file. The patterns are used to make an initial group selection, and to decide whether to subscribe to new groups. For instance, when you create the following pattern file: - *.advocacy + comp.os.* you will automatically be subscribed to all current and future comp.os.* groups, except for *.advocacy groups. If you like, you can include the script as an example in the sn distribution. -- Dick Streefland //// De Bilt dick.streefland@xs4all.nl (@ @) The Netherlands ------------------------------oOO--(_)--OOo------------------ sn-0.3.8/contrib/pam_authentication/0001700000175000001440000000000010106142566020117 5ustar patrikusers00000000000000sn-0.3.8/contrib/pam_authentication/patch0000600000175000001440000000173410042775427021156 0ustar patrikusers00000000000000--- Makefile.orig Sun Nov 11 14:28:47 2001 +++ Makefile Sun Nov 11 14:29:33 2001 @@ -59,10 +59,10 @@ addr.o valid.o key.o field.o OBJS =$(AOBJS) snscan.o snprimedb.o sndumpdb.o snntpd.o list.o \ post.o commands.o snexpire.o \ - snmail.o snget.o + snmail.o snget.o snauth.o BINS =snprimedb snntpd snfetch snexpire snsend \ snmail snget sngetd snscan sndumpdb \ - snnewgroup sndelgroup snlockf snsplit + snnewgroup sndelgroup snlockf snsplit snauth SCRIPTS =dot-outgoing.ex SNHELLO SNPOST PROGS =$(BINS) $(SCRIPTS) MANS =sn.8 sncat.8 sndelgroup.8 sndumpdb.8 snexpire.8 \ @@ -128,6 +128,8 @@ $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snlockf: snlockf.o $(LD) `cat cc-flags` $^ -o $@ $(LIBS) +snauth: snauth.o + $(LD) `cat cc-flags` $^ -o $@ -lpam %: %.in sed-cmd sed -f sed-cmd $< >$@ @@ -208,3 +210,4 @@ snmail.o: snmail.c config.h parameters.h hostname.h unfold.h path.h \ addr.h field.h snget.o: snget.c config.h get.h parameters.h path.h valid.h +snauth.o: snauth.c sn-0.3.8/contrib/pam_authentication/snauth.c0000600000175000001440000001010210042614733021557 0ustar patrikusers00000000000000#include #include #include #include #include #include #include #include #include #include #define MAXLINE 1024 char * name; int debug = 0; void usage (char * err) { if (err!=NULL) fprintf(stderr, "Error: %s\n", err); fprintf(stderr,"Usage:\n\t%s [-p ]\n\n\t-p \tuse as the application name for PAM authentication (default - sn)\n-d\tSend debugging information to syslog\n",name); exit(1); } char * get_line(char * line, int size) { char * res = fgets(line, size, stdin); if (res!=NULL) { if (debug) syslog(LOG_DEBUG," <<< %s",line); if (!strncasecmp(line,"QUIT",4)) { if (debug) syslog(LOG_DEBUG," >>> 205 ."); fputs("205 .\r\n",stdout); exit(0); } } return res; } int nntp_conv (int num_msg, const struct pam_message **msg, struct pam_response **resp, void * app_data) { int i,j; char line[MAXLINE]; char * res=NULL; for (i=0;imsg_style) { case PAM_PROMPT_ECHO_ON: if (debug) syslog(LOG_DEBUG," >>> 480 Authentication required - %s\n",msg[i]->msg); printf("480 Authentication required - %s\r\n",msg[i]->msg); if (get_line(line,MAXLINE) == NULL) return PAM_CONV_ERR; if (strncasecmp(line,"AUTHINFO USER ",14)) res=NULL; else res=strdup(line+14); break; case PAM_PROMPT_ECHO_OFF: if (debug) syslog(LOG_DEBUG," >>> 381 Password required - %s\n",msg[i]->msg); printf("381 Password required - %s\r\n",msg[i]->msg); if (get_line(line,MAXLINE) == NULL) return PAM_CONV_ERR; if (strncasecmp(line,"AUTHINFO PASS ",14)) res=NULL; else res=strdup(line+14); break; case PAM_TEXT_INFO: break; case PAM_ERROR_MSG: if (debug) syslog(LOG_DEBUG," >>> 481 Authentication failure - %s\n",msg[i]->msg); printf("481 Authentication failure - %s\r\n",msg[i]->msg); default: return PAM_CONV_ERR; } if (res!=NULL) for(j=strlen(res)-1; ((j>=0)&&((res[j]=='\r')||(res[j]=='\n')||(res[j]==' ')||(res[j]=='\t'))); res[j--]=0); resp[i]=(struct pam_response *) malloc(sizeof (struct pam_response)); resp[i]->resp=res; resp[i]->resp_retcode=0; } return PAM_SUCCESS; } void setrhost (pam_handle_t *pamh) { struct sockaddr_in from; socklen_t fromlen=sizeof (from); char * host; struct hostent *hp; if (getpeername(0, (struct sockaddr *)&from, &fromlen) < 0) return; hp = gethostbyaddr((char *)(&from.sin_addr), sizeof (struct in_addr), from.sin_family); if (hp!=NULL) host = hp->h_name; else host = inet_ntoa(from.sin_addr); pam_set_item(pamh,PAM_RHOST,host); } int main (int argc, char * const argv[]) { pam_handle_t *pamh = NULL; char * pam_name = "sn"; char path[sizeof(BINDIR)+MAXLINE]; struct pam_conv conv = { nntp_conv, NULL }; int i; name = argv[0]; for (i=1; i< argc; i++) { if ((! strcmp(argv[i],"-p"))&&(i+1>> 200 Hi, Posting is OK, but you need to authenticate"); fputs("200 Hi, Posting is OK, but you need to authenticate\r\n",stdout); if (get_line(path,sizeof(path)) == NULL) { syslog (LOG_ERR, "Never got the first message"); exit(1); } if (pam_start(pam_name, NULL, &conv, &pamh) != PAM_SUCCESS) { syslog (LOG_ERR, "PAM initialization failed"); exit(1); } setrhost (pamh); if (pam_authenticate(pamh, PAM_DISALLOW_NULL_AUTHTOK) != PAM_SUCCESS) { if (debug) syslog(LOG_DEBUG," >>> 481 Authentication failure"); fputs("481 Authentication failure\r\n",stdout); exit(1); } pam_end(pamh, PAM_SUCCESS); closelog(); setenv("POSTING_OK","yes",1); if (debug) syslog(LOG_DEBUG," >>> 281 Auth OK"); fputs("281 Auth OK\r\n",stdout); strcpy(path,BINDIR); strcat(path,":/bin:/usr/bin"); setenv("PATH",path,1); strcpy(path,BINDIR); strcat(path,"/snntpd"); if (debug) return(execl(path,path,"-S","-d","-d","-d","logger","-p","news.info",NULL)); else return(execl(path,path,"-S","logger","-p","news.info",NULL)); } sn-0.3.8/contrib/pam_authentication/README0000600000175000001440000000076510042775427021017 0ustar patrikusers00000000000000In the examples in INSTALL.run, you may want to run "snauth" instead of "snntpd ...". Snauth would only allow authorized users to read and will allow all authorized users to post (so you do not have to set POSTING_OK yourself). Snauth uses PAM for authentication - make sure your /etc/pam.d/sn is reasonable. To compile, move snauth.c to the main code directory, apply the patch (patch < contrib/pam_authentication/patch), and run "make". This code was written by Aleksey Nogin . sn-0.3.8/contrib/README0000600000175000001440000000044210042614733015123 0ustar patrikusers00000000000000This subtree contains tools and examples that are not maintained as part of the core system for some reason, probably mainly because I don't feel like supporting the feature in question. :) Each subdirectory contains a README file with information about the module, and who the author is. sn-0.3.8/times.c0000600000175000001440000001465110042571016014073 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Write/read the recieved time of an article. This is an array * in file .times. * TODO: maybe map to page limit and check in remap(), so we needn't * always actually remap. Probably not very consequential since * appending and finding mostly occur independently. */ #include #include #include #include #include #include #include #include "config.h" #include "times.h" #include "cache.h" #include #include #include static const char ver_ctrl_id[] = "$Id: times.c 29 2004-04-24 23:02:38Z patrik $"; extern int rename(const char *old, const char *new); /* * The times field in struct timesobj may be NULL, to allow for the * case where the .times file has not yet been created, or is empty. * This is not an error. */ struct timesobj { char *group; struct times *times; int fd; int nr; int mapsize; }; static int desc = -1; static int times_compare (void *a, void *b) { struct timesobj *x = (struct timesobj *) a; struct timesobj *y = (struct timesobj *) b; return strcmp(x->group, y->group); } static int remap (struct timesobj *tp) { struct stat st; if (-1 == fstat(tp->fd, &st)) { LOG("remap:fstat:%m"); return -1; } if (!tp->mapsize || st.st_size > tp->mapsize) do { int size; if (tp->times) munmap((caddr_t) tp->times, tp->mapsize); if (0 == (st.st_size % sizeof (struct times))) { size = (st.st_size + 2048) - (st.st_size % 1024); tp->times = (struct times *) mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, tp->fd, 0); if (tp->times != MAP_FAILED) { tp->mapsize = size; break; } else LOG("remap:mmap(%s/.times):%m", tp->group); } else LOG("remap:%s/.times wrong size", tp->group); return -1; } while (0); tp->nr = st.st_size / sizeof (struct times); return 0; } static struct timesobj *times_open (char *group) { struct timesobj *tp; int len; len = strlen(group); if (len >= GROUPNAMELEN) return 0; if ((tp = malloc(len + 1 + sizeof (struct timesobj)))) { tp->fd = openf(0644, O_RDWR | O_APPEND | O_CREAT, "%s/.times", group); if (tp->fd > -1) { tp->group = (char *) tp + sizeof (struct timesobj); strcpy(tp->group, group); tp->mapsize = 0; if (remap(tp) > -1) return tp; close(tp->fd); } else LOG("times_open:open(%s):%m", group); free(tp); } else LOG("times_open:no memory"); return 0; } static void times_close (void *p) { struct timesobj *tp = (struct timesobj *) p; if (tp->times) munmap((caddr_t) tp->times, tp->mapsize); if (tp->fd > -1) close(tp->fd); free(tp); } static int times_initialized = 0; int times_init (void) { desc = cache_init(3, times_compare, times_close, NULL); if (desc > -1) { times_initialized = 1; return 0; } return -1; } void times_fin (void) { if (debug >= 3) { int hit, miss; cache_stat(desc, &hit, &miss); LOG("times_close:cache requests:%dT=%dH+%dM", hit + miss, hit, miss); } cache_fin(desc); desc = -1; } static struct timesobj *gettimes (char *group) { struct timesobj t; struct timesobj *tp; if (!times_initialized) if (-1 == times_init()) return NULL; t.group = group; tp = cache_find(desc, &t); if (tp) return tp; tp = times_open(group); if (NULL == tp) return NULL; cache_insert(desc, tp); return tp; } /* returns the serial number of the first article stored after that date */ int times_since (char *group, time_t earliest) { struct timesobj *tp; int hi; int lo; int mid; struct times *t; if (NULL == (tp = gettimes(group))) return -1; if (-1 == remap(tp)) return -1; t = tp->times; if (!(hi = tp->nr)) return 0; /* NaAN */ lo = 0; mid = hi / 2; /* Earlier than anything we have */ if (earliest < t[0].stored) return 1; /* Later than everything we have */ if (tp->nr <= 1 || earliest > t[hi - 1].stored) return 0; while (lo < hi) { if (lo == mid) break; if (t[mid].stored > earliest) hi = mid; else if (t[mid].stored < earliest) lo = mid; else return t[mid].serial; mid = (hi + lo) / 2; } return t[lo].serial; } int times_append (char *group, int serial) { struct timesobj *tp; struct times t = { 0, }; if (NULL == (tp = gettimes(group))) return -1; t.serial = serial; t.stored = time(NULL); if (-1 == write(tp->fd, &t, sizeof (t))) { LOG("times_append:write:%m"); cache_invalidate(desc, tp); } return 0; } int times_expire (char *group, int until) { struct timesobj *tp; char buf[GROUPNAMELEN + sizeof ("/.time.tmp")]; char buf2[GROUPNAMELEN + sizeof ("/.times")]; int fd; int er; int i; if (NULL == (tp = gettimes(group))) return -1; if (NULL == tp->times) return -1; for (i = 0; i < tp->nr; i++) if (tp->times[i].serial >= until) break; if (0 == i) return 0; formats(buf, sizeof (buf) - 1, "%s/.time.tmp", group); fd = open(buf, O_WRONLY | O_TRUNC | O_CREAT, 0644); if (-1 == fd) { LOG("times_expire:open(%s):%m", buf); return -1; } er = write(fd, &tp->times[i], (tp->nr - i) * sizeof (struct times)); close(fd); if (-1 == er) { LOG("times_expire:write(%s):%m", group); return -1; } formats(buf2, sizeof (buf2) - 1, "%s/.times", group); if (-1 == rename(buf, buf2)) { LOG("times_expire:rename(%s):%m", buf); unlink(buf2); return -1; } cache_invalidate(desc, tp); return 0; } #ifdef TIMES_DEBUG int times_show (char *group) { struct timesobj *tp; int i; t.group = group; if (NULL == (tp = gettimes(group))) return -1; if (NULL == tp->times) return -1; for (i = 0; i < tp->nr; i++) writef(1, "%d:%u\n", tp->times[i].serial, tp->times[i].stored); return 0; } #endif sn-0.3.8/dot-outgoing.ex.in0000600000175000001440000000317107564256733016210 0ustar patrikusers00000000000000#!!!BASH!! # This is a SAMPLE .outgoing file suitable for use in a local # newsgroup to feed to a mailing list. # # You MUST edit this before copying it into news.group/.outgoing. # You must set at least LISTADDRESS and SENDMAIL: # Invocation environment: # Current directory will be $SNROOT if set else !!SNROOT!!. # The article is available on descriptor 0, in wire format. # If TCPREMOTEIP and TCPLOCALIP are set, that is the dotted-quad IP # of the posting host and server (us) respectively. # NEWSGROUP is set to this local newsgroup's name. # The script must exit 0 to indicate it has successfully handled the # article, or non-0 to indicate failure. LISTADDRESS=postmaster # mailing list address SENDMAIL="/var/qmail/bin/qmail-inject" # Running qmail? #SENDMAIL="/usr/lib/sendmail $LISTADDRESS" # Running sendmail? # # If you're using qmail, you can set these: # # QMAILSUSER=postmaster # For envelope sender # QMAILSHOST= # Ditto # export QMAILSUSER QMAILSHOST # # Otherwise, if you're using sendmail, you could set these: # # LOGNAME=postmaster # HOSTNAME= # export LOGNAME HOSTNAME # # Real work begins here. This turns a news article into # something suitable for mail. # { if [ "x$TCPREMOTEIP" != x ]; then if [ -s .me ]; then me=`cat .me`; else me="$TCPLOCALIP"; fi echo "Received: from $TCPREMOTEIP by ${me:-localhost} with nntp; `date +'%e %h %Y %H:%M:%S %Z'`" fi echo "To: $LISTADDRESS" sed 's/ $// 1,/^$/{ /^[nN][eE][wW][sS][gG][rR][oO][uU][pP][sS] *:/d /^[pP][aA][tT][hH] *:/d /^[lL][iI][nN][eE][sS] *:/d /^[bB][yY][tT][eE][sS] *:/d /^[tT][oO] *:/d } /^$/,${ s/^\.// } ' } |$SENDMAIL sn-0.3.8/addr.h0000600000175000001440000000117610044035540013666 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Parse email addresses, or fragments thereof. addr_domain() also * used to verify legal newsgroup names. addr_qstrchr() like * strchr() but ignores comments and quoted strings. */ /*extern int addr_addrspec (char *buf);*/ extern int addr_domain (char *buf); extern int addr_localpart (char *buf); extern int addr_msgid (char *buf); extern char *addr_qstrchr (char *str, int find); extern int addr_unescape (char *from, char *to, int len); sn-0.3.8/snfetch.8.in0000600000175000001440000000666307564256733014767 0ustar patrikusers00000000000000.TH snfetch,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snfetch \- fetch articles. .SH SYNOPSIS .B snfetch .RI [- r ] .RI [- t\ timeout ] .RI [- c\ depth ] .IR group\ [ serial \ [ max ]] .br .SH DESCRIPTION .B snfetch expects to read NNTP from file descriptor 6, and write NNTP to file descriptor 7, which descriptors must have already been open. Articles retrieved are written to descriptor 1. Each article displayed is separated from the next by a dot on a line by itself. These articles are NNTP safe, in the sense that double-dot unquoting is not done, and all lines end in CR-LF. .B snfetch expects to begin it's NNTP speech by giving the GROUP command. The upstream server's greeting must have already been read some other way. Likewise, when .B snfetch is done, it will not send a QUIT command. The output is suitable for feeding to .BR snstore . .B snfetch always checks the article ID of the prospective article before retrieving it. If the ID already exists in the ID database, the article will not be retrieved. Note that this does .B not guarantee that all articles retrieved will be unique. .B snfetch does not guarantee to leave the NNTP conversation in a decent state (there might be more data to read, but .B snfetch aborted part way). You will need to be root or own !!SNROOT!! in order to run this program. It is not safe to direct the output of several .BR snfetch es to the same pipe descriptor, but a file descriptor is all right. .SH OPTIONS .TP .RI - t\ timeout .B snfetch will wait only .I timeout seconds (default 180) for data before giving up, if the server doesn't respond. .TP .RI - r Output in news batch format instead. All articles will take the .B #! rnews form only, even if there is just one article, all lines end in bare linefeed, and dot-unquoting is performed. .TP .RI - c\ depth Employ a command pipeline of depth .IR depth . NNTP command pipelining is not officially sanctioned, so by default no pipeline is used. However, a pipeline with a .I depth greater than 0 will substantally reduce transaction latency .B if the server accepts it. A respectable value for .I depth might be between 1 and 5. .SH ARGUMENTS .B snfetch takes the name of a single newsgroup on its command line. If .I serial is given, this is taken to be the first article number on .I group on the server to fetch. If .I max is specified, this is taken to be the maximum number of articles to fetch. The first article retrieved may be after .I serial if .I max would be violated. If .I max is not specified, there is no limit. .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .SH FILES .TP .RI !!SNROOT!!/ newsgroup /.serial If .I serial is not specified, the starting serial number is taken from the file .RI !!SNROOT!!/ newsgroup / .serial . This defaults to .B 0 if the file can't be read. .TP .RI !!SNROOT!!/ newsgroup /.serial.tmp If .B snfetch exits with success (0), the new .I serial is written into this file. The original file .RI !!SNROOT!!/ newsgroup /.serial is readonly by .BR snfetch . .TP .RI !!SNROOT!!/ newsgroup /.max If .I max is not specified on the command line, .I max is read from this file. If the file cannot be read or does not exist, there is no limit. .SH EXIT CODES .B snfetch exits 1 on usage error, 2 on system failure, 3 on protocol error, 4 on read timeout, and 0 on success. .SH SEE ALSO snstore (8), snget (8) sn-0.3.8/sndelgroup.c0000600000175000001440000000467110042571016015135 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Work like the script. */ #include #include #include #include #include "config.h" #include "path.h" #include "parameters.h" #include "valid.h" #include #include #include #include static const char ver_ctrl_id[] = "$Id: sndelgroup.c 29 2004-04-24 23:02:38Z patrik $"; int debug = 0; void usage (void) { fail(1, "Usage:%s newsgroup ...", progname); } int main (int argc, char **argv) { int i; char *cp; char *v[5]; DIR *dir; struct dirent *dp; int pid; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); while ((i = opt_get(argc, argv, "")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'd': debug++; break; case 'V': version(); _exit(0); default: usage(); } if (opt_ind >= argc) usage(); parameters(TRUE); if (-1 == chdir(snroot)) fail(2, "Can't chdir(%s):%m", snroot); set_path_var(); LOG1("Deleting specified groups under %s", snroot); for (i = opt_ind; i < argc; i++) if (!is_valid_group(argv[i])) if (strcmp(argv[i], JUNK_GROUP)) fail(1, "%s is not a newsgroup", argv[i]); for (i = opt_ind; i < argc; i++) if (0 == chdir(argv[i]) && (dir = opendir("."))) { v[0] = "snexpire"; v[1] = "-0d"; v[2] = argv[i]; v[3] = 0; LOG1("expiring %s", argv[i]); pid = cmdopen(v, 0, 0); if (pid <= 0) fail(2, "Unable to run snexpire on %s", argv[i]); if (cmdwait(pid)) fail(2, "snexpire failed on %s", argv[i]); while ((dp = readdir(dir))) { cp = dp->d_name; if ('.' == *cp) if (!cp[1] || ('.' == cp[1] && !cp[2])) continue; LOG1("Removing file %s/%s", argv[i], cp); unlink(dp->d_name); } closedir(dir); chdir(".."); LOG1("Removing group directory \"%s\"", argv[i]); if (-1 == rmdir(argv[i])) fail(2, "Can't remove %s:%m", argv[i]); LOG("Removed group \"%s\" from under %s", argv[i], snroot); } else LOG("Ignoring %s, can't chdir:%m", argv[i]); _exit(0); } sn-0.3.8/allocate.c0000600000175000001440000001670710042571016014542 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Module to allocate space for keys and data in the file. * Space allocated is in multiples of 4 bytes, from 4 bytes to 160 * bytes inclusive. Space larger than that will not be allocated. * * It is assumed that alignment on 4 bytes boundaries is OK. Change * if not. It is also assumed the usage pattern will be very constant. * * How: * The file starts out empty but for an array of list headers, which * point to nothing. Each time a request is recieved, the size is * rounded up to the next multiple of 4, and the corresponding list * is checked for space. If it is empty, a new block is allocated * from the end of the file and returned. * * If the list isn't empty, the next free block is returned. * * Splits are not done, because there's no provision for * rejoining splits either. Without joining, the allocator would * fragment rapidly. * * There is no facility to grow a buffer. */ #include #include #include #include #include #include #include #include #include #include "config.h" #include "allocate.h" #include #define ALLO_MAGIC 0xd0bed0 #ifndef ALLO_ALIGNMENT #warning Must define ALLO_ALIGNMENT (4, 8, 16, etc bytes) #endif #ifndef ALLO_MIN_CHUNKSIZE #define ALLO_MIN_CHUNKSIZE ALLO_ALIGNMENT #endif #ifndef ALLO_MAX_CHUNKSIZE #define ALLO_MAX_CHUNKSIZE 256 #endif #define ALLO_MAX_CHAINS ((ALLO_MAX_CHUNKSIZE - ALLO_MIN_CHUNKSIZE)/ALLO_ALIGNMENT) #define ALLO_MAX_SIZE ALLO_MAX_CHAINS * ALLO_ALIGNMENT static const char ver_ctrl_id[] = "$Id: allocate.c 29 2004-04-24 23:02:38Z patrik $"; struct chainfile { int chain_magic; volatile int next[ALLO_MAX_CHAINS]; }; struct table { char *filename; char *map; int fd; int size; int oflag; int mprot; }; static struct table table = { 0, }; static int remapfile (void); void *allo_deref (unsigned int offset) { if (offset >= table.size - ALLO_MAX_CHUNKSIZE) { if (-1 == remapfile()) return 0; else if (offset >= table.size) return 0; } return ((void *) (table.map + offset)); } int allo_ref (void *obj) { int off; off = (int) obj - (int) table.map; if (off >= table.size) return -1; return off; } /* Create the file */ static int initfile (char *filename) { struct chainfile cf = { 0, }; int fd; int i; int ret = 0; fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644); if (-1 == fd) return -1; cf.chain_magic = ALLO_MAGIC; for (i = 0; i < ALLO_MAX_CHAINS; i++) cf.next[i] = 0; i = write(fd, &cf, sizeof (cf)); if (i == sizeof (cf)) { int pad; pad = sizeof (cf) % (ALLO_ALIGNMENT); if (pad > 0) for (i = 0; i < pad && 0 == ret; i++) if (1 != write(fd, "", 1)) ret = -1; } else ret = -1; close(fd); return ret; } static void unmapfile (void) { if (table.fd >= 0) { close(table.fd); table.fd = -1; } if (table.map) { munmap(table.map, table.size); table.map = NULL; table.size = 0; } } static size_t rounduptopagesize (size_t size) { static size_t pagesize = 0; int pages; if (0 == pagesize) pagesize = getpagesize(); pages = (size / pagesize) + 1; return ((size_t) (pages * pagesize)); } static int mapfile (void) { struct stat st; if (-1 == table.fd) table.fd = open(table.filename, table.oflag, 0644); if (-1 == table.fd) goto fail; if (-1 == fstat(table.fd, &st)) goto fail; table.size = rounduptopagesize(st.st_size); table.map = mmap(0, table.size, table.mprot, MAP_SHARED, table.fd, 0); if (!table.map || table.map == MAP_FAILED) goto fail; return 0; fail: unmapfile(); return -1; } /* * -Maybe- remap. If nothing has changed, why bother? Thanks rth. */ static int remapfile (void) { struct stat st; if (-1 == fstat(table.fd, &st)) return -1; if (st.st_size <= table.size) return 0; munmap((caddr_t) table.map, table.size); return mapfile(); } /* * Returns 0 if lock was obtained; -1 on failure. If it succeeded, * the entry may have been remapped, so caller will need to reset * automatic variables from global. */ static int lock (void) { if (-1 == lockf(table.fd, F_TLOCK, 0)) { do { if (EAGAIN != errno) return -1; else nap(0, 200); } while (-1 == lockf(table.fd, F_TLOCK, 0)); if (-1 == remapfile()) return -1; } return 0; } static void unlock (void) { lseek(table.fd, 0, SEEK_SET); lockf(table.fd, F_ULOCK, 0); } static int checkvalidfile (void) { if (table.size > 0) { if (table.size < sizeof (struct chainfile)) return -1; if (((struct chainfile *) table.map)->chain_magic != ALLO_MAGIC) return -1; } return 0; } /* * Initialize from file. Create if it doesn't exist */ #ifndef O_SYNC #define FLAG_SYNC 0 #else #define FLAG_SYNC O_SYNC #endif int allo_init (char *path, int flag) { if (O_RDWR == (flag & O_RDWR)) table.mprot = PROT_READ | PROT_WRITE; else table.mprot = PROT_READ; table.oflag = flag & (O_RDONLY | O_RDWR | FLAG_SYNC | O_CREAT); if (O_CREAT == (flag & O_CREAT)) if (-1 == initfile(path)) return -1; table.filename = path; table.fd = -1; if (-1 == mapfile() || -1 == checkvalidfile()) { unmapfile(); table.filename = NULL; return -1; } table.filename = strdup(path); return 0; } static int rounduptoalignment (int size) { if (size <= 0) return ALLO_ALIGNMENT; return ((((size - 1) / ALLO_ALIGNMENT) + 1) * ALLO_ALIGNMENT); } /* * Return an offset into the allocated area */ #define CFP ((struct chainfile *)table.map) int allo_make (int size) { static char tmpchunk[ALLO_MAX_CHUNKSIZE + 16] = { '\0', }; int chain; int block; int failures; size = rounduptoalignment(size); if (size > ALLO_MAX_SIZE) return -1; chain = (size / ALLO_ALIGNMENT) - 1; for (failures = 0; failures < 10; failures++) { if (CFP->next[chain]) { /* Have a free block we can use */ if (-1 == lock()) return -1; if (!(block = CFP->next[chain])) { unlock(); continue; } CFP->next[chain] = *(int *) (table.map + block); memset(table.map + block, 0, size); } else { /* No free block to use, extend the file */ if (-1 == lock()) return -1; block = lseek(table.fd, 0, SEEK_END); write(table.fd, tmpchunk, (1 + chain) * ALLO_ALIGNMENT); remapfile(); } unlock(); return block; } return -1; } /* * Must pass size, so we know which free list to hook this chunk to. * Giving the wrong size is a good way to create garbage. */ int allo_free (int chunk, int size) { int chain; size = rounduptoalignment(size); if (size > ALLO_MAX_SIZE) return -1; chain = (size / 4) - 1; if (-1 == lock()) return -1; *(int *) (table.map + chunk) = CFP->next[chain]; CFP->next[chain] = chunk; unlock(); return 0; } int allo_destroy (void) { unmapfile(); free(table.filename); table.filename = NULL; return 0; } sn-0.3.8/COPYING0000600000175000001440000004307607564256733013671 0ustar patrikusers00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, Cambridge, MA 02139, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. sn-0.3.8/allocate.h0000600000175000001440000000145207564256733014563 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef ALLOCATE_H #define ALLOCATE_H /* int functions return -1 on error, 0 on success */ /* Return the object described by offset */ extern void *allo_deref (unsigned int offset); /* Returns the offset of the object */ extern int allo_ref (void *object); /* Return a descriptor to refer to this allocated arena */ extern int allo_init (char *path, int openflag); /* Get a block of size size from the allocator */ extern int allo_make (int size); /* Give back a block of size size to the allocator */ extern int allo_free (int chunk, int size); /* Close this arena */ extern int allo_destroy (void); #endif sn-0.3.8/Makefile0000600000175000001440000001410210106142506014234 0ustar patrikusers00000000000000 # Check config.h for system-specific vars. # You MUST edit the following. Make sure there are no leading or # trailing spaces around the values you change: ## Enable compression? Yes: specify LIBZ as "-lz" or use path, e.g.: ZLIB =-lz ## or (if you prefer it static): #ZLIB =/usr/lib/libz.a ## No: Leave ZLIB undefined. #ZLIB = ## Where to install the executables and the man pages PREFIX =/usr/local #PREFIX =/home/patrik/stow/sn-current ## Where the news spool will be SNROOT =/var/spool/news #SNROOT =/home/patrik/spool/news ## Where to send mail for the admin if neither the NEWSMASTER nor ## the LOGNAME environment variable is set DEFAULT_ADMIN_EMAIL =newsmaster # # OS-specific settings. Uncomment only one section below. # ## For Linux: INSTALL =install LIBS =-L./lib -lstuff ## For Solaris: #INSTALL =ginstall #LIBS =-L./lib -lstuff -lxnet ## For FreeBSD / Mac OS X: #INSTALL =ginstall #LIBS =-L./lib -lstuff # # Stuff you probably won't need to edit. # CC =gcc LD =gcc BINDIR =$(PREFIX)/sbin MANDIR =$(PREFIX)/man # # You can stop editing here. # VERSION =0.3.8 AOBJS =art.o cache.o group.o times.o dh_find.o dhash.o \ allocate.o newsgroup.o hostname.o \ store.o parameters.o args.o body.o unfold.o path.o \ addr.o valid.o key.o field.o OBJS =$(AOBJS) snscan.o snprimedb.o sndumpdb.o snntpd.o list.o \ post.o commands.o snexpire.o \ snmail.o snget.o BINS =snprimedb snntpd snfetch snexpire snsend \ snmail snget sngetd snscan sndumpdb \ snnewgroup sndelgroup snlockf snsplit SCRIPTS =dot-outgoing.ex SNHELLO SNPOST PROGS =$(BINS) $(SCRIPTS) MANS =sn.8 sncat.8 sndelgroup.8 sndumpdb.8 snexpire.8 \ snfetch.8 snget.8 snmail.8 snnewgroup.8 snntpd.8 \ snprimedb.8 snscan.8 sncancel.8 snsend.8 snstore.8 \ sngetd.8 snsplit.8 all: cc-flags $(OBJS) $(AOBJS) libs $(PROGS) sed-cmd $(MANS) $(SCRIPTS) cc-flags: echo ' -g -Wall -pedantic -O' >$@.t echo ' -I./lib' >>$@.t echo ' -DVERSION="$(VERSION)"' >>$@.t echo ' -DSNROOT="$(SNROOT)"' >>$@.t echo ' -DBINDIR="$(BINDIR)"' >>$@.t -[ 'x$(ZLIB)' = x ] || echo ' -DUSE_ZLIB' >>$@.t mv $@.t $@ sed-cmd: echo 's,!!SNROOT!!,$(SNROOT),g' >$@.t echo 's,!!BINDIR!!,$(BINDIR),g' >>$@.t echo 's,!!VERSION!!,$(VERSION),g' >>$@.t echo 's,!!DEFAULT_ADMIN_EMAIL!!,$(DEFAULT_ADMIN_EMAIL),g' >>$@.t bash=`which bash`; echo "s,!!BASH!!,$${bash:-/bin/bash},g" >>$@.t mv $@.t $@ libs: sn.a lib/libstuff.a lib/libstuff.a: cd lib; $(MAKE) all CC=$(CC) sn.a: $(AOBJS) ar rc $@ $^ ranlib $@ snsplit: snsplit.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snscan: snscan.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) $(ZLIB) sncat: snscan ln -s snscan sncat sncancel: snscan ln -s snscan sncancel snprimedb: snprimedb.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) sndumpdb: sndumpdb.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snntpd: snntpd.o post.o commands.o list.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) $(ZLIB) snsend: snsend.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) $(ZLIB) snstore: snsend ln -s snsend snstore snfetch: snfetch.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snexpire: snexpire.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snmail: snmail.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snget: snget.o get.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) sngetd: sngetd.o get.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snnewgroup: snnewgroup.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) sndelgroup: sndelgroup.o sn.a $(LD) `cat cc-flags` $^ -o $@ $(LIBS) snlockf: snlockf.o $(LD) `cat cc-flags` $^ -o $@ $(LIBS) %: %.in sed-cmd sed -f sed-cmd $< >$@ read magic <$@; case $$magic in "#!"*) chmod 755 $@ ;; esac %.o: %.c cc-flags $(CC) -c `cat cc-flags` $< -o $@ clean: rm -f *.a *.o $(PROGS) $(SCRIPTS) a.out core \ *.1 *.5 *.8 *.ex cc-flags* sed-cmd* gmon.out cd lib; $(MAKE) clean strip: $(BINS) strip $^ install: all #$(SNROOT) $(BINDIR) $(MANDIR)/man8 # commented out so make -n install works without the dirs $(INSTALL) $(PROGS) $(BINDIR) $(INSTALL) *.8 $(MANDIR)/man8 -cd $(BINDIR); rm -f sncat; ln -s snscan sncat -cd $(BINDIR); rm -f sncancel; ln -s snscan sncancel -cd $(BINDIR); rm -f snstore; ln -s snsend snstore spoolclean: @echo -n "make $@ will wipe out your news spool! Sure? [y/n] " @read ans; case "$$ans" in "y") : ;; *) echo "Not cleaning"; exit 1 ;; esac rm -f $(SNROOT)/*/[0123456789]* $(SNROOT)/*/.times rm -f $(SNROOT)/{.table,.chain,.newsgroup} for i in $(SNROOT)/*; do echo 0 >$$i/.serial; done -./snprimedb -i echo-snroot: @echo $(SNROOT) echo-bindir: @echo $(BINDIR) _dep: Makefile cc-flags (sed '/^## DO NOT REMOVE ##/q' Makefile; echo; for i in $(OBJS); \ do $(CC) -MM -MG `cat cc-flags` `basename $$i .o`.c; done) >.dep mv .dep Makefile ## DO NOT REMOVE ## art.o: art.c config.h art.h artfile.h cache.h cache.o: cache.c cache.h group.o: group.c config.h cache.h group.h artfile.h times.o: times.c config.h times.h cache.h dh_find.o: dh_find.c config.h dhash.h allocate.h newsgroup.h dhash.o: dhash.c config.h allocate.h dhash.h newsgroup.h allocate.o: allocate.c config.h allocate.h newsgroup.o: newsgroup.c config.h hostname.o: hostname.c config.h store.o: store.c config.h times.h art.h artfile.h cache.h group.h \ body.h parameters.o: parameters.c args.o: args.c body.o: body.c art.h unfold.o: unfold.c path.o: path.c addr.o: addr.c addr.h valid.o: valid.c config.h parameters.h key.o: key.c key.h field.o: field.c snscan.o: snscan.c config.h artfile.h art.h group.h dhash.h hostname.h \ parameters.h times.h body.h snprimedb.o: snprimedb.c config.h dhash.h parameters.h sndumpdb.o: sndumpdb.c config.h allocate.h newsgroup.h dhash.h \ parameters.h snntpd.o: snntpd.c config.h art.h dhash.h group.h hostname.h \ parameters.h args.h lib/readln.h snntpd.h valid.h key.h list.o: list.c config.h art.h group.h args.h lib/readln.h snntpd.h \ key.h post.o: post.c snntpd.h args.h lib/readln.h unfold.h commands.o: commands.c config.h times.h dhash.h art.h group.h args.h \ lib/readln.h body.h snntpd.h key.h snexpire.o: snexpire.c config.h art.h group.h dhash.h times.h \ parameters.h valid.h snmail.o: snmail.c config.h parameters.h hostname.h unfold.h path.h \ addr.h field.h snget.o: snget.c config.h get.h parameters.h path.h valid.h sn-0.3.8/snsend.c0000600000175000001440000004554510101773324014254 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Send out or store articles in an article stream. */ #include #include #include #include #include #include #include #include #include #include #include #include "art.h" #include "unfold.h" #include "hostname.h" #include "group.h" #include "store.h" #include "times.h" #include "config.h" #include "addr.h" #include "dhash.h" #include "field.h" #include "parameters.h" #include #include #include #include #include #include static const char ver_ctrl_id[] = "$Id: snsend.c 58 2004-07-28 18:56:20Z patrik $"; static char path1[MAXPATHLEN + 1]; static char path2[MAXPATHLEN + 1]; typedef enum { NG_UNKNOWN, NG_LOCAL, NG_SPECIAL, NG_SPECIALX, NG_GLOBAL, NG_FIFO } ng_t; static struct ng { char *s; ng_t t; } *newsgroups; static struct readln input = { 0, }; static struct article art = { 0, }; static void nomem (void) { fail(2, "No memory"); } static char *me; static int melen; static int nrgroups; static char *messageid; static bool check_exist = FALSE; static int pid; static struct timeval now; static bool canalias = TRUE; static bool report = FALSE; int debug = 0; #define B_APPENDL(b,str) b_appendl((b), (str), strlen(str)) static ng_t newsgroup_type (char *group) { struct stat st; formats(path1, sizeof (path1) - 1, "%s/.outgoing", group); if (0 == stat(path1, &st)) { if (S_ISDIR(st.st_mode)) return NG_GLOBAL; if (S_ISREG(st.st_mode)) { if (S_IXUSR & st.st_mode) return NG_SPECIALX; else return NG_SPECIAL; } if (S_ISFIFO(st.st_mode)) return NG_FIFO; return NG_UNKNOWN; } if (ENOTDIR == errno) return NG_UNKNOWN; if (0 == stat(group, &st) && S_ISDIR(st.st_mode)) return NG_LOCAL; return NG_UNKNOWN; } /* ----------------------------------------------------- * "Methods" to send/store an article. */ static int writeart (int fd) { if (write(fd, art.head, art.hlen) > -1) if (write(fd, "\r\n", 2) > -1) if (art.body && write(fd, art.body, art.blen) > -1) if (write(fd, ".\r\n", 3) > -1) return 0; return -1; } static int store_global (char *group) { struct stat st; int fd, er; formats(path1, sizeof (path1) - 1, "%s/.outgoing/$%u.%u.%u", group, now.tv_sec, now.tv_usec, pid); if (0 == stat(path1, &st)) return 0; *strrchr(path1, '$') = '+'; fd = open(path1, O_WRONLY | O_CREAT | O_EXCL, 0644); if (-1 == fd) { if (EEXIST == errno) { LOG("store_global:tmp file %s exists", path1); return 0; /* darn unlikely */ } else fail(2, "store_global:open(%s):%m", path1); } er = writeart(fd); close(fd); if (0 == er) { strcpy(path2, path1); *strrchr(path2, '+') = '$'; if (-1 == link(path1, path2) && EEXIST != errno) { er = -1; LOG("store_global:link(%s,%s):%m", path1, path2); } } else LOG("store_global:write(%s):%m", path1); (void) unlink(path1); if (er) _exit(2); return 0; } static int store_local (char *group) { struct data d; d.messageid = messageid; if (group) if (check_exist) if (0 == dh_find(&d, FALSE)) if (0 == strcasecmp(group, d.newsgroup)) { LOG1("store_local:<%s> already exists in %s, not storing", messageid, group); return 1; } d.serial = sto_add(group ? group : JUNK_GROUP, &art); if (-1 == d.serial) { if (group) _exit(2); /* FIXME: safe to return -1 here? */ else return -1; /* =junk */ } times_append(group ? group : JUNK_GROUP, d.serial); if ((d.newsgroup = group)) { if (d.serial < 0) _exit(2); if (-1 == dh_insert(&d)) { if (EEXIST == errno) { LOG1("store_local:oops, stored <%s> but it already exists", messageid); } else LOG("store_local:can't insert in db for <%s> in %s:%d, %m?", messageid, group, d.serial); } } if (report) writef(1, "%s %u <%s>\n", group ? group : JUNK_GROUP, d.serial, messageid); return (group ? d.serial : 0); } static char *av[] = { "sh", 0, 0 }; static int special (char *group, int argv0) { int pid, p[2], er, s; formats(path1, sizeof (path1) - 1, "./%s/.outgoing", group); av[1] = path1; if (pipe(p)) fail(2, "special:pipe:%m"); if (-1 == (pid = fork())) fail(2, "special:fork:%m"); if (0 == pid) { formats(path2, sizeof (path2) - 1, "NEWSGROUP=%s", group); if (putenv(path2)) fail(2, "putenv:%m?"); close(p[1]); dup2(p[0], 0); execvp(av[argv0], av + argv0); fail(2, "exec(%s):%m", av[argv0]); } close(p[0]); er = writeart(p[1]); close(p[1]); while (-1 == waitpid(pid, &s, 0) && EINTR == errno) ; if (WIFEXITED(s)) { if (0 == WEXITSTATUS(s)) return 0; s = WEXITSTATUS(s); fail(s, "%s/.outgoing exited with %d", s, s); } if (WIFSIGNALED(s)) fail(WTERMSIG(s) + 128, "%s/.outgoing caught signal %d", WTERMSIG(s)); fail(9, "%s/.outgoing unknown wait status %d", group, s); _exit(1); /* dumb compiler */ } static int store_special (char *group) { return special(group, 0); } static int store_specialx (char *group) { return special(group, 1); } static int store_fifo (char *group) { int fd; formats(path1, sizeof (path1) - 1, "%s/.outgoing", group); fd = open(path1, O_WRONLY | O_NONBLOCK); if (-1 == fd) fail(2, "store_fifo:open(%s):%m", path1); if (-1 == fcntl(fd, F_SETFL, O_WRONLY)) fail(2, "store_fifo:fcntl(%s):%m", path1); lockf(fd, F_LOCK, 0); if (-1 == writeart(fd)) fail(2, "store_fifo:write(%s):%m", path1); close(fd); return 0; } #define for_each_newsgroup(n) \ for (n = 0; n < nrgroups; n++) \ if (!newsgroups[n].s) ; else static int localstore (void) { int i, serial, done; done = 0; for_each_newsgroup(i) if (NG_UNKNOWN != newsgroups[i].t) { serial = store_local(newsgroups[i].s); if (1 == serial) return 1; /* short circuit dups */ if (canalias && art.body) { art.body = 0; art.blen = 0; art.hlen = formats(art.head, art.hlen, "Message-ID: %s:%u<%s>\r\n", newsgroups[i].s, serial, messageid); } newsgroups[i].s = 0; done++; } return done; } /* ----------------------------------------------------- * snsend here. Order: do globals first because they are the easiest * for admin to undo. Locals are last because we are lazy and want * to overwrite the article with it's alias. */ static void junk_article (void) { static int have_junk_group = -1; LOG("article <%s> not for any newsgroup", messageid); if (0 == have_junk_group) return; if (store_local(0) < 0) { if (debug >= 1) { LOG(">>>"); writeart(2); LOG("<<<"); } have_junk_group = 0; } else have_junk_group = 1; } static void snsend (void) { int i, serial, done; done = 0; for_each_newsgroup(i) { switch ((newsgroups[i].t = newsgroup_type(newsgroups[i].s))) { default: case NG_UNKNOWN: case NG_LOCAL: continue; case NG_GLOBAL: serial = store_global(newsgroups[i].s); break; case NG_SPECIAL: serial = store_special(newsgroups[i].s); break; case NG_SPECIALX: serial = store_specialx(newsgroups[i].s); break; case NG_FIFO: serial = store_fifo(newsgroups[i].s); break; } if (report) writef(1, "%s %u <%s>\n", newsgroups[i].s, serial, messageid); newsgroups[i].s = 0; done++; } done += localstore(); if (!done) junk_article(); } /* ----------------------------------------------------- * snstore here. Ignore newsgroup type, just store it regardless. * We won't be checking .nopost like we did in previous versions, * because we don't have -1 option anymore. */ static void snstore (void) { int i; for_each_newsgroup(i) newsgroups[i].t = newsgroup_type(newsgroups[i].s); if (0 == localstore()) junk_article(); } /* ----------------------------------------------------- */ static void store___NOT (void) { if (-1 == writeart(1)) fail(2, "write:%m"); } /* ----------------------------------------------------- */ static void usage (void) { fail(1, "Usage: %s [-rcnva]\n" "-r input is in rnews batch format\n" "-c when storing to a local newsgroup, check first if it exists\n" "-n do nothing; echo articles to descriptor 1\n" "-v for each article, write to descriptor 1 where it went\n" "-a do not alias crossposts\n", progname); } /* ----------------------------------------------------- * Main program. */ int main (int argc, char **argv) { bool read_article(bool); int i; bool rnews; void (*dispatcher) (void); char *p; progname = ((p = strrchr(argv[0], '/')) ? p + 1 : argv[0]); if (0 == strcmp(progname, "snsend")) dispatcher = snsend; else if (0 == strcmp(progname, "snstore")) dispatcher = snstore; else { fail(1, "Eh?"); _exit(1); } rnews = FALSE; canalias = TRUE; while ((i = opt_get(argc, argv, "")) > -1) switch (i) { case 'd': debug++; break; case 'V': version(); _exit(0); case 'P': log_with_pid(); break; case 'r': rnews = TRUE; break; case 'v': report = TRUE; break; case 'c': check_exist = TRUE; break; case 'n': dispatcher = store___NOT; break; case 'a': canalias = FALSE; break; default: usage(); } if (opt_ind < argc) usage(); parameters(TRUE); if (chdir(snroot)) fail(2, "chdir(%s):%m", snroot); me = myname(); melen = strlen(me); pid = getpid(); if (readln_ready(0, 0, &input)) fail(2, "no memory"); if (-1 == group_init()) _exit(2); if (-1 == sto_init()) fail(2, "can't initialize store:%m?"); if (-1 == times_init()) fail(2, "can't initialize times:%m"); (void) dh_open(NULL, FALSE); for (gettimeofday(&now, 0); read_article(rnews); gettimeofday(&now, 0)) dispatcher(); _exit(0); } /* ----------------------------------------------------- * Article reading and fixing-up. */ /* symbols private to us */ static struct b msgid = { 0, }; static struct b path = { 0, }; static struct b body = { 0, }; static struct b head = { 0, }; static struct b htmp = { 0, }; static int body_lines; static bool have_date; static bool have_newsgroups; static char *rline; static int rlen; static void badread (void) { fail(2, "can't read article:%m"); } static void eof (void) { fail(3, "unexpected end of file"); } static void badrnews (void) { fail(3, "bad rnews line"); } static bool first_call; static void init (void) { messageid = 0; first_call = TRUE; body_lines = body.used = head.used = htmp.used = msgid.used = path.used = 0; have_date = have_newsgroups = FALSE; } static int getline (void) { int len; len = rlen = readln(&input, &rline, '\n'); if (0 == rlen) return 0; if (rlen < 0) badread(); rline[--rlen] = '\0'; if (rlen > 0) if ('\r' == rline[rlen - 1]) rline[--rlen] = '\0'; return len; } static void appendbody (void) { if (b_appendl(&body, rline, rlen) || B_APPENDL(&body, "\r\n")) nomem(); } /* Sets newsgroups[]; valid until next call */ static void parsenewsgroups (char *ng) { static int size = 0; static struct b ngbuf = { 0, }; char *p; if (0 == size) if (!(newsgroups = malloc((size = 10) * sizeof (struct ng)))) nomem(); while (' ' == *ng) ng++; ngbuf.used = 0; if (B_APPENDL(&ngbuf, ng)) nomem(); ng = ngbuf.buf; for (nrgroups = 0; (p = tokensep(&ng, ",")); nrgroups++) { while (' ' == *p) p++; if (!*p) continue; if (nrgroups + 1 >= size) { struct ng *tmp; if (!(tmp = malloc(size * 2 * sizeof (struct ng)))) nomem(); memcpy(tmp, newsgroups, size * sizeof (struct ng)); free(newsgroups); newsgroups = tmp; size *= 2; } while (p[strlen(p) - 1] == ' ') /* Strip trailing spaces */ p[strlen(p) - 1] = '\0'; newsgroups[nrgroups].s = p; newsgroups[nrgroups].t = NG_UNKNOWN; } } /* * Called by unfold() (which is called by read_*()) to hand us * an unfolded line. */ #define HEADER(lit) \ if (hlen == sizeof (lit) - 1 && \ 0 == strncasecmp(line, lit, sizeof (lit) - 1) && \ ':' == line[sizeof (lit) - 1]) #define MAXREFLEN 500 static int append (char *line, int len) { char *p; int c, hlen; if (first_call) { first_call = FALSE; if (0 == strncmp(line, "From ", 5)) return 0; } hlen = check_field(line, len); if (!hlen) goto badline; do { HEADER("References") { if (len > MAXREFLEN) { p = line + len - MAXREFLEN; c = MAXREFLEN; while (*p && ' ' != *p) { p++; c--; } if (!*p) break; if (B_APPENDL(&htmp, "References: ")) nomem(); if (b_appendl(&htmp, p + 1, c - 1)) nomem(); if (B_APPENDL(&htmp, "\r\n")) nomem(); return 0; } break; } HEADER("Path") { if (path.used) return 0; for (line += hlen + 1, len -= hlen + 1; ' ' == *line; line++, len--) ; if (b_appendl(&path, line, len)) nomem(); return 0; } HEADER("Newsgroups") { if (have_newsgroups) break; /* XXX ignore or fail? */ if (b_appendl(&htmp, line, len)) nomem(); if (B_APPENDL(&htmp, "\r\n")) nomem(); have_newsgroups = TRUE; parsenewsgroups(line + hlen + 1); return 0; } HEADER("X-sn-Newsgroups") { parsenewsgroups(line + hlen + 1); return 0; } HEADER("Message-ID") { int len; if (msgid.used) break; /* XXX */ for (p = line + hlen + 1; ' ' == *p; p++) ; if ('<' != *p || (len = addr_msgid(p)) <= 0) { LOG("append: will replace bad Message-ID \"%s\"", p); return 0; } if (b_appendl(&msgid, p + 1, len - 2)) nomem(); break; } if (0 == strncasecmp(line, "X-sn-", 5)) return 0; HEADER("Bytes") return 0; HEADER("Lines") return 0; HEADER("Xref") return 0; HEADER("Date") have_date = TRUE; } while (0); if (0) badline: LOG("append:bad header \"%s\"", line); if (b_appendl(&htmp, line, len)) nomem(); if (B_APPENDL(&htmp, "\r\n")) nomem(); return 0; } static int read_nntp (void) { int consumed; consumed = unfold(&input, append); if (0 == consumed) return 0; if (consumed < 0) badread(); for (body_lines = 0;;) { int len; if (0 == (len = getline())) eof(); consumed += len; if (1 == rlen) if ('.' == *rline) break; body_lines++; appendbody(); } return consumed; } static int read_rnews (void) { int bytes, c, consumed; if (0 == getline()) return 0; if ('#' != *rline++) badrnews(); if ('!' != *rline++) badrnews(); while (' ' == *rline) rline++; if (strncmp(rline, "rnews ", 6)) badrnews(); for (rline += 6; ' ' == *rline; rline++) ; for (bytes = 0; (c = *rline); rline++) { if (c < '0' || c > '9') badrnews(); bytes *= 10; bytes += (c - '0'); } consumed = unfold(&input, append); if (0 == consumed) eof(); if (consumed < 0) badread(); for (body_lines = 0; consumed < bytes; ) { if (0 == (c = getline())) eof(); consumed += c; if (consumed > bytes) fail(3, "rnews input overshot"); body_lines++; if ('.' == *rline) if (B_APPENDL(&body, ".")) nomem(); appendbody(); } return consumed; } static void appendhead (char *line, int len) { if (b_appendl(&head, line, len)) nomem(); } #define APPENDHEAD(str) appendhead((str), strlen(str)) #define PUT2(i,sep) c = i; *p-- = c%10 + '0'; *p-- = c/10 + '0'; *p-- = sep; bool read_article (bool rnews) { char buf[80]; int c, i; init(); if (rnews) c = read_rnews(); else c = read_nntp(); if (0 == c) return FALSE; /* no more */ /* fix the headers. We don't care if there were newsgroups. */ /* Path: */ APPENDHEAD("Path: "); appendhead(me, melen); if (path.used) { APPENDHEAD("!"); appendhead(path.buf, path.used); } else LOG3("created Path header..."); APPENDHEAD("\r\n"); appendhead(htmp.buf, htmp.used); /* Message-ID */ if (!msgid.used) { c = formats(buf, sizeof (buf) - 1, "%x.%x.%x@%s", pid, now.tv_sec, now.tv_usec, me); if (b_appendl(&msgid, buf, c)) nomem(); APPENDHEAD("Message-ID: <"); appendhead(buf, c); APPENDHEAD(">\r\n"); LOG1("created Message-ID \"<%s>\"", buf); } else LOG3("read article \"<%s>\"", msgid.buf); messageid = msgid.buf; /* Date. strftime(3) uses locale, we don't want that. */ if (!have_date) { static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; char *p; time_t t; struct tm *tmp; p = buf + sizeof (buf) - 1; *p-- = '\0'; time(&t); tmp = gmtime(&t); PUT2(tmp->tm_sec, ':') PUT2(tmp->tm_min, ':') PUT2(tmp->tm_hour, ' ') c = tmp->tm_year + 1900; for (i = 0; i < 4; i++) { *p-- = c % 10 + '0'; c /= 10; } *p-- = ' '; p -= 3; memcpy(p + 1, months[tmp->tm_mon], 3); *p-- = ' '; PUT2(tmp->tm_mday, ' ') *p-- = ','; p -= 2; memcpy(p, days[tmp->tm_wday], 3); APPENDHEAD("Date: "); APPENDHEAD(p); APPENDHEAD(" -0000\r\n"); LOG3("added Date to article \"<%s>\"", messageid); } if (0 == body_lines) { if (B_APPENDL(&body, "\r\n")) nomem(); body_lines++; } /* Bytes, Lines */ c = formats(buf, sizeof (buf) - 1, "Bytes: %u\r\nLines: %u\r\n", body.used, body_lines); appendhead(buf, c); /* done */ art.head = head.buf; art.hlen = head.used; art.body = body.buf; art.blen = body.used; return TRUE; } sn-0.3.8/snntpd.8.in0000600000175000001440000001007407746022423014617 0ustar patrikusers00000000000000.TH snntpd,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snntpd \- small news server .SH SYNOPSIS .B snntpd .RI [- t\ timeout ] .RI [- P ] .RI [- S ] .RI [ logger ...] .br .SH DESCRIPTION .B snntpd is a small news server. It needs to be run under inetd or tcpserver, as root or as the owner of !!SNROOT!!. .B snntpd does not fork into the background. It expects to read and write from and to the network on descriptors 0 and 1. .SH ARGUMENTS .IR logger ... (usually /usr/bin/logger) is taken to be a logging program, and all log output is piped to it. If .IR logger ... is not specified, log messages are directed to descriptor 2. .SH OPTIONS .TP .RI - t\ timeout specifies how long .B snntpd should wait for input before it gives up and exits. .I timeout is in seconds and defaults to 600. .TP .RI - P .B snntpd includes it's pid in log output. .TP .RI - S Suppress NNTP greeting on startup. This is useful if you want to perform authentication before running .BR snntpd , or want to provide your own greeting, from a wrapper. .SH POSTING AND POSTING PERMISSIONS Posts are usually handled externally by the .RB !!BINDIR!!/ SNPOST script, which is responsible for fine-grain posting control; handling of control messages; and the ultimate distribution of the posted article. .B snntpd permits or denies posting in a very simple manner: If .RB !!SNROOT!!/ .nopost exists, posting is not allowed. Otherwise, if the environment variable .B POSTING_OK is not set, posting is not allowed. Otherwise if .B POSTING_OK is set (to the empty string), posting is generally allowed, and all POSTed articles are piped to the .B SNPOST script, which has the final say in the matter. The value of .RB $ POSTING_OK is not currently used, but is reserved. .SH FILES .TP .RB !!SNROOT!!/ .fifo If this file exists, and is a fifo, .B snntpd will write the name of a newsgroup into it as that newsgroup becomes the current one. If the fifo does not exist .B snntpd will not create it. .TP .RB !!SNROOT!!/ .noservice If this file exists, .B snntpd will display its first line and exit. If the file can't be read or is empty, a default message is displayed. This is useful for temporarily disabling the news server while you perform any maintenance. .TP .RB !!SNROOT!!/ .nopost See .B POSTING PERMISSIONS above. .TP .RB !!SNROOT!!/ .SNPOST If this script or program exists, it is invoked instead of .BR SNPOST to accept a posted article. .TP .RB !!SNROOT!!/news.group.name/ .nopost These files really belong to .BR SNPOST , and it is unfortunate that .B snntpd has to check for their existence to determine the posting flag for the LIST command. See .RB !!BINDIR!!/ SNPOST . .TP .RB !!SNROOT!!/news.group.name/ .info If this file exists, its first line is taken as the description of that group for use with the LIST NEWSGROUPS command. .TP .RB !!SNROOT!!/news.group.name/ .times is a binary file containing entry times, to support the .I NEWNEWS command. .TP .RB !!SNROOT!!/news.group.name/ .created is an empty file retained for it's timestamp, to support the .I NEWGROUPS command. .SH SIGNALS If .B snntpd catches SIGHUP, the files .RB !!SNROOT!!/{ .fifo , .noservice , .nopost } (see below) are checked again, as they are during startup. Other signals have default behaviour. .SH ENVIRONMENT VARIABLES See also .RB !!BINDIR!!/ SNPOST for a list of environment variables exported by .BR snntpd . .TP .B PATH The PATH must be set such that .B snntpd can find .B SNPOST in order to accept postings. If .B PATH does not include !!BINDIR!!, !!BINDIR!! will be appended to it. .TP .B POSTING_OK This variable helps determine the site-wide posting policy. See .B POSTING PERMISSIONS above. .TP .B TCPREMOTEIP If this value is set, it is taken to be the dotted-quad IP address of the connecting client. If it is not set, .B snntpd attempts to derive it for itself, and then set its value. .TP .B TCPLOCALIP as above, but for the server's dotted-quad IP. .TP .B SNROOT If this is set and is not empty, the value is used everywhere in place of .BR !!SNROOT!! , the default news spool directory. .SH SEE ALSO snsend(8), !!BINDIR!!/SNPOST sn-0.3.8/parameters.h0000600000175000001440000000053310042066025015113 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef PARAMETERS_H #define PARAMETERS_H extern char *snroot; extern uid_t snuid; extern gid_t sngid; extern void parameters (bool wantwriteperms); #endif sn-0.3.8/parameters.c0000600000175000001440000000333310042571016015110 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include #include #include #include "config.h" static const char ver_ctrl_id[] = "$Id: parameters.c 29 2004-04-24 23:02:38Z patrik $"; char *snroot = SNROOT; uid_t snuid; gid_t sngid; void parameters (bool wantwriteperms) { struct stat st; if (!(snroot = getenv("SNROOT"))) snroot = SNROOT; if (-1 == stat(snroot, &st)) fail(2, "Can't find spool directory \"%s\":%m", snroot); if (!S_ISDIR(st.st_mode)) fail(2, "\"%s\" is not a directory", snroot); snuid = st.st_uid; sngid = st.st_gid; if (0 == geteuid()) { setgid(sngid); setuid(snuid); } if (wantwriteperms) if (snuid != geteuid() || sngid != getegid()) { struct passwd *pwuid; struct group *grgid; char euid_name[40], egid_name[40]; char snuid_name[40], sngid_name[40]; pwuid = getpwuid(geteuid()); grgid = getgrgid(getegid()); strncpy(euid_name, (pwuid ? pwuid->pw_name : "(unknown)"), 40); strncpy(egid_name, (grgid ? grgid->gr_name : "(unknown)"), 40); pwuid = getpwuid(snuid); grgid = getgrgid(sngid); strncpy(snuid_name, (pwuid ? pwuid->pw_name : "(unknown)"), 40); strncpy(sngid_name, (grgid ? grgid->gr_name : "(unknown)"), 40); fail(2, "Can't write in spool directory \"%s\" (I am %s:%s, must be %s:%s)", snroot, euid_name, egid_name, snuid_name, sngid_name); } } sn-0.3.8/SNPOST.in0000600000175000001440000000575210105424251014164 0ustar patrikusers00000000000000#!!!BASH!! # # This script is invoked by snntpd in response to the POST command. # Basically it forwards any cancel requests, and invokes snsend on # the others. It augments snsend by checking for newsgroups for # which posting is not permitted. This script's simple-minded idea # of access control is determined by the presence or absence of the # .nopost file in the newsgroup directories. Obviously there's a lot # of flexibility if you want to do more sophisticated access control. # # Invocation environment: # Article to be posted will be available on descriptor 0. # $NEWSGROUPS will always contain a non-empty space-separated list # of newsgroups to which the article is posted. # If the article is a control message, $CONTROL will be set to the # value of the Control field. # $TCPREMOTEIP and $TCPLOCALIP will be set to the posting client's IP # and the local server IP, if invoked from the network. # Current directory will always be the appropriate sn spool # directory, $SNROOT if set, otherwise !!SNROOT!!. # PATH will always contain !!BINDIR!!. # The exit status gets translated into the NNTP response code; # the output (if any) becomes the text of the NNTP response. # fail () { echo "$@"; exit 9; } # # Is it a control message? Notify owner and let her verify the # request. I won't make auto-cancellation the default. # If you don't want control messages identified, remove or comment # out the next if ... fi block. # if [ "x$CONTROL" != x ]; then parse () { CANCEL=`echo $1 |tr 'A-Z' 'a-z'`; MSGID=$2; } parse $CONTROL [ "x$CANCEL" = xcancel ] || fail "I only do \"cancel\" messages" tmp="/tmp/.SNPOST.$$" trap 'rm -f $tmp $tmp.1' 0 sncat -i "$MSGID" |sed -e 's/ //' -e '/^$/q' >$tmp [ -s $tmp ] || fail "I can't find \"$MSGID\"" set -e { echo "This is an automated message, don't reply." echo; echo "I received this cancel request:"; echo sed -e 's/ //' -e 's/^/> /' echo; echo "It refers to this article (headers only):"; echo sed 's/^/> /' $tmp echo "You can cancel it with \"!!BINDIR!!/sncancel -i '$MSGID'\"" } >$tmp.1 mail -s "Cancellation request" ${NEWSMASTER:-${LOGNAME:-!!DEFAULT_ADMIN_EMAIL!!}} <$tmp.1 || fail "Unable to forward your request" echo "Your request will be considered" exit 0 fi # # Is a normal posting. # postable= exists= for ng in $NEWSGROUPS; do mentioned=x$mentioned # Is it a newsgroup we have? [ -f $ng/.created ] || continue exists=x$exists # .nopost files "belong" to us; we alone control group post access [ -f $ng/.nopost ] && continue [ "x$postable" = x ] || postable="$postable," postable="$postable$ng" done [ "$exists" ] || fail "I don't have any of those newsgroups" [ "x$postable" = x ] && fail "Posting not allowed to those newsgroups" { echo "X-sn-Newsgroups: $postable" # VERIFIED_SENDER is inherited from the authenticating script that # invoked snntpd, if any. [ "x$VERIFIED_SENDER" = x ] || echo "Sender: $VERIFIED_SENDER" cat } | snsend -v || fail "Unable to post" echo "Posted to $postable" : Success sn-0.3.8/snmail.8.in0000600000175000001440000000672310044050613014565 0ustar patrikusers00000000000000.TH snmail,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snmail \- mail-to-news filter .SH SYNOPSIS .B snmail .RI [- sP ] .RI [ listname .RI [ prefix ]] .br .SH DESCRIPTION .B snmail reads a single email message on standard input, and writes the converted message to standard output. This output is suitable for feeding to .BR snstore . .B snmail does these conversions: If the first line is a UUCP .B From_ line, it is silently discarded; a .BR Newsgroups : line is added with the value of .I prefixlistname .RI ( prefix concatenated with .IR listname ), and other existing .BR Newsgroups : lines are removed; and a .BR Path : line is created from all .BR Received : lines, which are removed; all header lines are unfolded; if no message ID exists, one is created; message IDs in all .B References: lines and .BR In-Reply-To : lines are collected into a single .B References: line; all lines are rewritten to end in CRLF; and a lone .B . on a line is written at the end of the message. .B snmail will accept and pass on messages with invalid header fields. .B snmail is meant to be run from a .B .forward or the .B /etc/aliases file (if you're using sendmail or similar) or from a .B .qmail file (if you're using qmail). .B snmail may write status or error messages to standard error. .SH OPTIONS .TP .RI - s Pipe output to .B snstore directly, do not write to standard output. This is so the exit status is not lost in a shell pipeline. If you use this flag, it is also a good idea to also use .RI - c (tell .B snstore to check if article already exists). .TP Other options .B snmail also accepts options intended for .BR snstore , viz. .RI - c , .RI - v , and .RI - n , and these are passed on uninterpreted. .SH ARGUMENTS .TP .I prefix is the prefix of the newsgroup; if not specified, it defaults to .B local. (note the trailing .BR . ). .I prefix may not begin with a .B . (dot) but it may be empty. .TP .I listname completes the newsgroup name; if not specified, it is the local part of the address in the .B Return-Path: line if it exists and isn't empty; otherwise it is the local part of the messages' first .BR From: . So if the message originated from the list .RB < linux-lemmings @vger.rutgers.edu>, then the default newsgroup becomes .BR local.linux-lemmings . .I listname may be empty if .I prefix isn't. You may want to have a file .RB !!SNROOT!!/ prefixlistname /.nopost to deny posting from all and sundry, to prevent the mailing list newsgroup from being contaminated. .I prefix and .I listname may contain uppercase characters; these are converted to lower case. .SH USAGE There are two ways of forwarding mail to the sn news spool: directly from a user's mail address; or forwarded to a central address and then invoking .BR snmail . The central concern here is permissions; while anyone can run .BR snmail , not everyone may store mail in the spool. Mail setups vary a lot, so basically you're on your own here. .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .TP .B PATH If .RI - s was specified, determines where to look for .BR snstore , before looking in !!BINDIR!!. .SH EXIT STATUS .B snmail exits 0 if the message was successfully converted and written, .B or if the message ID already exists in the ID database and the message was not stored or converted. .B snmail exits with 1 on usage error, 2 on operational error, 3 if the mail message wasn't in the proper format. sn-0.3.8/list.c0000600000175000001440000000610710042571016013722 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Functions for the NNTP LIST command and its variants. * Assumes args[0] is "LIST", and args[1] is not null. */ #include #include #include #include #include #include "config.h" #include "art.h" #include "group.h" #include "args.h" #include "snntpd.h" #include "key.h" #include #include static const char ver_ctrl_id[] = "$Id: list.c 29 2004-04-24 23:02:38Z patrik $"; /* Same as LIST but may have an optional wildmat */ static void listactive (void) { struct group g; struct key *kp; int i, j; char *group; args_write(1, "215 list follows\r\n"); for_each_key(i, j, kp) { group = KEY(kp); if (!args[1] || !args[2] || wildmat(group, args[2])) if (0 == group_info(group, &g)) args_write(1, "%s %d %d %s\r\n", group, g.last, g.first, g.nopost ? "n" : "y"); } args_write(1, ".\r\n"); } /* Display format of overview database */ static void listoverviewfmt (void) { args_write(1, "215 ok\r\n"); args_write(1, "Subject:\r\n" "From:\r\n" "Date:\r\n" "Message-ID:\r\n" "References:\r\n" "Bytes:\r\n" "Lines:\r\n" "Xref:full\r\n.\r\n"); } static void listnewsgroups (void) { args_write(1, "215 Here you go\r\n"); if (nr_keys > 0) { char *group; int i, j; struct key *kp; for_each_key(i, j, kp) { group = KEY(kp); if (!args[2] || wildmat(group, args[2])) { char buf[128]; if (topline(group, ".info", buf, sizeof (buf)) <= 0) *buf = '\0'; args_write(1, "%s %s\r\n", group, buf); } } } args_write(1, ".\r\n"); } /* ZB was here */ static void listdistribpats (void) { args_write(1, "215 ok\r\n"); args_write(1, ".\r\n"); } void do_list (void) { if (args[1]) { if (0 == strcasecmp(args[1], "active")) listactive(); else if (0 == strcasecmp(args[1], "newsgroups")) listnewsgroups(); else if (0 == strcasecmp(args[1], "overview.fmt")) listoverviewfmt(); else if (0 == strcasecmp(args[1], "distrib.pats")) listdistribpats(); else args_write(1, "503 \"LIST %s\" command not implemented\r\n", args[1]); } else listactive(); } void do_listgroup (void) { struct article bogus; int i; if (args[1]) { if (alltolower(args[1]) > GROUPNAMELEN || make_current(args[1])) { args_write(1, "411 No such group\r\n"); return; } } else if (!currentgroup) { args_write(1, "412 No group selected\r\n"); return; } args_write(1, "211 Article numbers follow\r\n"); for (i = currentinfo.first; i <= currentinfo.last; i++) if (0 == art_gimme(currentgroup, i, &bogus)) writef(1, "%d\r\n", i); args_write(1, ".\r\n"); currentserial = currentinfo.first; } sn-0.3.8/snnewgroup.8.in0000600000175000001440000000256207564256733015536 0ustar patrikusers00000000000000.TH snnewgroup.v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snnewgroup \- create a new sn newsgroup .SH SYNOPSIS .B snnewgroup .I newsgroup .RI [ server ] .RI [ port ] .br .SH DESCRIPTION .B snnewgroup creates .I newsgroup assigning it an upstream NNTP server of .IR server : port . If .I port is not specified, defaults to 119. If .I server is also not specified, .I newsgroup is created as a local group which is fed only by articles POSTed to it. You will need to be root or own !!SNROOT!! in order to add new groups. .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .SH FILES CREATED .TP .RI !!SNROOT!!/ newsgroup Directory where articles will be stored. .TP .RI !!SNROOT!!/ newsgroup / .created Empty file for the newsgroup creation time. .TP .RI !!SNROOT!!/ newsgroup / .serial Where .B snget gets its idea of the new starting serial number for .I newsgroup on server .IR server : port . The value is initialized to 0. .TP .RI !!SNROOT!!/ newsgroup / .outgoing If .I server is specified, is a symlink to .BR .RI !!SNROOT!!/.outgoing/ server : port , which is a directory, created if it does not already exist. If .I server is not specified, no file .RI !!SNROOT!!/ newsgroup /.outgoing will be created. See also .BR snsend . .SH SEE ALSO sndelgroup, snsend sn-0.3.8/hostname.c0000600000175000001440000000215610042571016014565 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include #include "config.h" static const char ver_ctrl_id[] = "$Id: hostname.c 29 2004-04-24 23:02:38Z patrik $"; char *myname (void) { int fd; char *host = NULL; char buf[256]; int count; if ((fd = open(".me", O_RDONLY)) > -1) { if ((count = read(fd, buf, sizeof (buf) - 1)) > 0) { char *cp; buf[count] = '\0'; if ((cp = strchr(buf, '\n'))) *cp = '\0'; if ((cp = strchr(buf, '\r'))) *cp = '\0'; host = strdup(buf); } close(fd); } if (!host) { struct hostent *hp; if (0 == gethostname(buf, sizeof (buf))) { hp = gethostbyname(buf); if (hp) host = (char *) hp->h_name; /* silence cc, sometimes */ } } if (NULL == host) host = strdup("localhost"); return host; } sn-0.3.8/snscan.8.in0000600000175000001440000000616507564261106014605 0ustar patrikusers00000000000000.TH snscan,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snscan \- display information on articles in the spool .br sncat \- display articles in the spool .br sncancel \- cancel articles in the spool .SH SYNOPSIS .B snscan .RI [ options ] .IR articlespec ... .br .B sncat .RI [ options ] .IR articlespec ... .br .B sncancel .RI [ options ] .IR articlespec ... .br .IR options : .RI [- n ] .RI [- s .IR since ] .RI [- o .IR outputfile ] .br .SH DESCRIPTION .TP .B snscan displays information on all articles specified by .IR articlespec , in the form .br .I newsgroup serial id .br on a line, one line per article matched. This output is suitable for feeding to .BR snprimedb . .TP .B sncat displays the articles specified by .IR articlespec . .TP .B sncancel marks articles specified by .I articlespec as canceled, so they become unavailable to .B sn programs. .SH ARGUMENTS .I articlespec defines a broad pattern for articles. It takes two mutually exclusive forms. .TP .B Specify by newsgroup and serial number Here .I articlespec is .br .IR newsgroup [: range [, range ]...] .br where each .I range is .br .IR serial [-[ toserial ]] .br (where .I toserial is greater than .IR serial ). If .I toserial is omitted, then all articles after .I serial are matched. If both .I serial and .I toserial are empty, then only the first and last articles are matched. .TP .B Specify by articles message ID Here .I articlespec is .RI - i .IR id ... .br where each .I id is a message ID, angle brackets optional. .SH OPTIONS .TP .RB - n This option weeds out articles that are aliases. If an article is crossposted, there will be an original copy, the others will be aliases to it. This option prevents information being displayed if it is not from the original copy. .TP .IB - s since This option weeds out articles that were obtained before .IR since . .I since is in the format .IR year / month / day [: hour : minute ] where each is a number. .I year may be 2 or 4 digits, and .B / and .B : can be any non-digit character. .TP .IB - o outputfile Write output to .I outputfile instead of descriptor 1. Sometimes you just don't feel like using the shell. .SH EXAMPLES .TP sncancel linux.lemmings:12 Cancel article 12 from linux.lemmings. .TP snscan linux.lemmings:4,12- Display information on article 4, and also articles 12 to the most recent article, from linux.lemmings. .TP sncat -n -s 1998/4/1 linux.lemmings Display all articles that are not aliases, and which were entered after April 1 1998, from linux.lemmings. .TP snscan -i '123@myhost' Display information on the article whose ID is .BR <123@myhost> , in whichever newsgroup it is found. .SH FILES Each .I articlespec or .I id on the command line refers to articles in the news spool located in .BR !!SNROOT!!. .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .SH EXIT CODES .B snscan exits 0 on success, 1 on usage error, 2 on system error, and 3 on other errors. It is not an error to specify an article that does not exist, or which does not meet the .I since criterion. .SH SEE ALSO snprimedb(8) sn-0.3.8/hostname.h0000600000175000001440000000033007564256733014607 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ extern char *myname (void); sn-0.3.8/snprimedb.c0000600000175000001440000000727610042571016014742 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Feed the database with values. This could be useful if the * database has suffered corruption. Then delete all the database * files so they will be recreated. You can feed this program from * snscan. * * The database converts article-id to newsgroup:serial. * * Input is expected in the form one to a line, * newsgroup id serial * with spaces between the words. */ #include #include #include #include #include #include "config.h" #include "dhash.h" #include "parameters.h" #include #include #include #include #include static const char ver_ctrl_id[] = "$Id: snprimedb.c 29 2004-04-24 23:02:38Z patrik $"; int debug = 0; struct readln input; static void handler (int signum) { dh_close(); LOG("Caught signal %d, exiting", signum); _exit(3); } int nr = 0; int insert (char *newsgroup, char *id, int serial) { struct data d = { 0, }; char *cp; if (0 == strcmp(newsgroup, JUNK_GROUP)) return 0; if ('<' == *id) { d.messageid = id + 1; if ((cp = strchr(d.messageid, '>'))) *cp = '\0'; } else d.messageid = id; d.newsgroup = newsgroup; d.serial = serial; if (-1 == dh_insert(&d)) { if (EEXIST == errno) { LOG("insert:\"%s\" already exists in %s:%d", d.messageid, newsgroup, serial); return 1; } else LOG("insert:Can't insert record \"%s %s %d\":%m\n", newsgroup, d.messageid, serial); return -1; } nr++; return 0; } void usage (void) { LOG("Usage: %s [-i]\n", progname); LOG("(No arguments). Input is read from stdin in the form\n" "\"newsgroup id serial\"\n" "until end-of-file.\n"); _exit(1); } int main (int argc, char **argv) { char *line; char *newsgroup; char *id; char *serial; bool onlyinitialize = FALSE; char *cp; int len; int i; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); parameters(TRUE); while ((i = opt_get(argc, argv, "")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'i': onlyinitialize = TRUE; break; case 'd': debug++; break; case 'V': version(); _exit(0); default: usage(); } if (opt_ind != argc) usage(); if (-1 == chdir(snroot)) FAIL(2, "chdir(%s):%m", snroot); if (-1 == dh_open(NULL, FALSE)) FAIL(2, "Can't open database:%m?"); if (onlyinitialize) { dh_close(); _exit(0); } signal(SIGPIPE, handler); if (-1 == readln_ready(0, 0, &input)) fail(2, "readln_ready:%m"); while ((len = readln(&input, &line, '\n')) > 0) { char *p; int s; line[len - 1] = '\0'; if (!(newsgroup = tokensep(&line, " "))) continue; if (!(serial = tokensep(&line, " "))) continue; if (!(id = tokensep(&line, " "))) continue; for (p = newsgroup; *p; p++) if (*p >= 'A' && *p <= 'Z') *p -= 'a' - 'A'; if (p - newsgroup >= GROUPNAMELEN) { LOG("newsgroup name too long, skipping:%s", newsgroup); continue; } s = strtoul(serial, &cp, 10); if (s <= 0 || *cp) fail(3, "Bad value \"%s\" for serial number", serial); switch (insert(newsgroup, id, s)) { default: LOG("Eh?"); /* Fall Through */ case -1: dh_close(); _exit(2); case 1: ; case 0: ; } } dh_close(); LOG("%d insertions", nr); _exit(0); } sn-0.3.8/times.h0000600000175000001440000000123107564256733014113 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef TIMES_H #define TIMES_H #include struct times { int serial; time_t stored; }; extern int times_init (void); extern void times_fin (void); /* Return the earliest article serial number after given date */ extern int times_since (char *group, time_t earliest); extern int times_append (char *group, int serial); extern int times_expire (char *group, int until); #ifdef TIMES_DEBUG extern int times_show (char *group); #endif /* TIMES_DEBUG */ #endif sn-0.3.8/snscan.c0000600000175000001440000002737210042571016014243 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * If called as snscan: * Display the newsgroup, serial, and message id of all newsgroups * specified on command line. * If called as sncat: * Displays all articles mentioned. * See usage(). */ #include #include #include #include #include #include #include #include /* for time_t */ #include #include #include #include "config.h" #include "artfile.h" #include "art.h" #include "group.h" #include "dhash.h" #include "hostname.h" #include "parameters.h" #include "times.h" #include "body.h" #include #include #include #include #include int debug = 0; static const char ver_ctrl_id[] = "$Id: snscan.c 29 2004-04-24 23:02:38Z patrik $"; char *host; int errors = 0; int (*print) (struct article *, char *, char *, int); int (*gimme) (char *, int, struct article *); void usage (void) { fail(1, "Usage:\n" "%s [-n] [-s since] [-o file] newsgroup[:lo[-[hi][,...]]]\n" "%s [-n] [-s since] [-o file] -i id ...", progname, progname); } int snscan (char *newsgroup, int serial) { struct article a; char *id; char *e = NULL; if (gimme(newsgroup, serial, &a)) return 1; if (*a.head && !art_bodyiscorrupt(a.body, a.blen)) if ((id = art_findfield(a.head, "Message-ID"))) if (writef(1, "%s %d %s\n", newsgroup, serial, *id ? id : "<0>") > 0) return 0; else fail(2, "write error:%m"); else e = "Can't find %s:%d:%m?"; else e = "Corrupt article %s:%d"; LOG(e, newsgroup, serial); errors++; return -1; } static int print_native (struct article *ap, char *host, char *newsgroup, int serial) { struct iovec v[10] = { {0,}, }; int i = 0; char buf[256]; v[i].iov_base = ap->head; v[i].iov_len = ap->hlen; i++; if (host && newsgroup && serial) { formats(buf, sizeof (buf) - 1, "Xref: %s %s:%d\r\n", host, newsgroup, serial); v[i].iov_base = buf; v[i].iov_len = strlen(buf); i++; } v[i].iov_base = "\r\n"; v[i].iov_len = 2; i++; v[i].iov_base = ap->body; v[i].iov_len = ap->blen; i++; v[i].iov_base = ".\r\n"; v[i].iov_len = 3; i++; return writev(1, v, i); } static int print_batch (struct article *ap, char *host, char *newsgroup, int serial) { struct b b = { 0, }; struct iovec v[2]; char *cp; char *p; char buf[256]; int e; for (cp = ap->head; (p = strchr(cp, '\r')); cp = p + 2) { b_appendl(&b, cp, p - cp); if (-1 == b_appendl(&b, "\n", 1)) goto fail; } if (host && newsgroup && serial) { formats(buf, sizeof (buf) - 1, "Xref: %s %s:%d\n\n", host, newsgroup, serial); b_append(&b, buf); } else b_appendl(&b, "\n", 1); for (cp = ap->body; (p = strchr(cp, '\r')); cp = p + 2) { if ('.' == *cp) b_appendl(&b, ".", 1); b_appendl(&b, cp, p - cp); if (-1 == b_appendl(&b, "\n", 1)) goto fail; } formats(buf, sizeof (buf) - 1, "#! rnews %d\n", b.used); v[0].iov_base = buf; v[0].iov_len = strlen(buf); v[1].iov_base = b.buf; v[1].iov_len = b.used; e = writev(1, v, 2); free(b.buf); return e; fail: if (b.buf) free(b.buf); return -1; } int sncat (char *newsgroup, int serial) { struct article a; char *e = NULL; if (gimme(newsgroup, serial, &a)) return 1; if (!art_bodyiscorrupt(a.body, a.blen)) if (0 == body(&a.body, &a.blen)) if (print(&a, host, newsgroup, serial)) return 0; else fail(2, "write error:%m"); else e = "Can't decompress? %s:%d:%m?"; else e = "Corrupt body in %s:%d"; LOG(e, newsgroup, serial); errors++; return -1; } int sncancel (char *newsgroup, int serial) { struct file *fp; struct article a; struct data d; int slot, fd, er; char path[GROUPNAMELEN + 32]; char *id; char *p; if (-1 == art_gimmenoderef(newsgroup, serial, &a)) return 1; id = art_findfield(a.head, "Message-ID"); if (!id || !*id) { LOG1("sncancel:Can't find ID in %s:%d", newsgroup, serial); errors++; return -1; } if ('<' == *id) id++; if ((p = strrchr(id, '>'))) *p = '\0'; formats(path, sizeof (path) - 1, "%s/%d", newsgroup, serial / ARTSPERFILE); er = 1; if ((fd = open(path, O_RDWR)) > -1) { fp = (struct file *) mmap(0, sizeof (*fp), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fp && fp != MAP_FAILED) { if (0 == lockf(fd, F_LOCK, 0)) { slot = serial % ARTSPERFILE; fp->info[slot].hoffset = fp->info[slot].boffset = -1; d.messageid = id; if (dh_delete(&d)) LOG1("dh_delete(<%s>):%m", id); return 0; } else { LOG1("lockf(%s):%m", path); } munmap(fp, sizeof (*fp)); } else { LOG1("mmap(%s):%m", path); } close(fd); } else if (ENOENT != errno) LOG("sncancel:open(%s):%m", path); errors += er; return -1; } static int range (char *spec, int *hi, int *lo) { char *cp; if (*spec < '0' || *spec > '9') return -1; if ((*lo = strtol(spec, &cp, 10)) < 0) return -1; if (!*cp) return (*hi = 0); if ('-' != *cp) return -1; spec = cp + 1; if (!*spec) { *hi = -1; return 0; } if ((*hi = strtol(spec, &cp, 10)) < 0) return -1; return (*cp ? -1 : 0); } static time_t timefmt (char *str) { struct tm tm = { 0, }; char *p = str; if (*p < '0' || *p > '9') return -1; tm.tm_year = strtoul(p, &str, 10); if (!str || !*str) return -1; if (tm.tm_year > 1900) tm.tm_year -= 1900; p = ++str; tm.tm_mon = strtoul(p, &str, 10); if (!str || !*str) return -1; tm.tm_mon--; p = ++str; tm.tm_mday = strtoul(p, &str, 10); if (str && *str) { p = ++str; tm.tm_hour = strtoul(p, &str, 10); if (str && *str) { p = ++str; tm.tm_min = strtoul(p, &str, 10); if (str && *str) { p = ++str; tm.tm_sec = strtoul(p, &str, 10); } } } return mktime(&tm); } int getnoaliases (char *group, int serial, struct article *ap) { if (-1 == art_gimmenoderef(group, serial, ap)) return 1; if (ap->blen > 0) return 0; if (0 == strncasecmp(ap->head, "Message-ID:", 11)) return 1; return 0; } int earliest (char *ng, time_t since) { int start; if ((start = times_since(ng, since)) <= 0) { if (-1 == start) { LOG("times_since(%s):%m?", ng); errors++; } return 0; } return start; } int main (int argc, char **argv) { time_t since; int (*doit) (char *, int); int i, outfd; char *cp; bool useid = FALSE; struct group g; bool dhro; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); dhro = TRUE; if (0 == strcmp(progname, "sncancel")) { dhro = FALSE; doit = sncancel; } else if (0 == strcmp(progname, "snscan")) doit = snscan; else if (0 == strcmp(progname, "sncat")) doit = sncat; else { fail(1, "Eh?"); _exit(29); /* hush compiler */ } parameters(!dhro); if (-1 == chdir(snroot)) FAIL(2, "chdir(%s):%m", snroot); host = myname(); gimme = art_gimme; print = print_native; since = (time_t) 0; while ((i = opt_get(argc, argv, "so")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'd': debug++; break; case 'n': gimme = getnoaliases; break; case 'V': version(); _exit(0); case 'i': useid = TRUE; break; case 'r': print = print_batch; break; case 'o': if (!opt_arg) usage(); outfd = open(opt_arg, O_WRONLY | O_CREAT, 0644); if (-1 == outfd) fail(2, "open(%s):%m", opt_arg); if (-1 == dup2(outfd, 1)) fail(2, "dup:%m"); break; case 's': if (!opt_arg) usage(); if ((time_t) - 1 == (since = timefmt(opt_arg))) fail(1, "Bad time format"); if (times_init()) fail(2, "times_init:%m"); break; default: usage(); } if (opt_ind >= argc) usage(); if (-1 == group_init()) fail(2, "group_init:%m"); if (!dhro || useid) if (-1 == dh_open(NULL, dhro)) fail(2, "Can't open database"); if (useid) { for (; opt_ind < argc && argv[opt_ind]; opt_ind++) { struct data d = { 0, }; if ('<' == *argv[opt_ind]) { d.messageid = argv[opt_ind] + 1; if ((cp = strchr(d.messageid, '>'))) *cp = '\0'; } else d.messageid = argv[opt_ind]; if (-1 == dh_find(&d, dhro)) continue; if (-1 == group_info(d.newsgroup, &g)) fail(2, "group_info(%s):%m", d.newsgroup); if (since) { int start; if (0 == (start = earliest(d.newsgroup, since))) continue; if (d.serial < start) continue; } (*doit) (d.newsgroup, d.serial); } dh_close(); _exit(0); } if (opt_ind == argc) usage(); for (; opt_ind < argc && argv[opt_ind]; opt_ind++) { int hi, lo, start; char *colon; char *newsgroup; start = 0; newsgroup = argv[opt_ind]; /* No check to is_valid_name(), not our business. But check for fs subversions */ if ((colon = strchr(newsgroup, ':'))) *colon++ = '\0'; if (strchr(newsgroup, '/') || strstr(newsgroup, "..")) { LOG("Bad newsgroup name \"%s\"", newsgroup); errors++; continue; } if (-1 == group_info(newsgroup, &g)) { LOG("group \"%s\":%m?", newsgroup); errors++; continue; } if (since) if (0 == (start = earliest(newsgroup, since))) continue; if (colon) { char *comma; while ((comma = tokensep(&colon, ","))) { if ('-' == *comma && !comma[1]) { for (i = g.first; i <= g.last; i++) if (0 == (*doit) (newsgroup, i)) break; if (i > g.last) errors++; for (i = g.last; i >= g.first; i--) if (0 == (*doit) (newsgroup, i)) break; if (i < g.first) errors++; } else { if (range(comma, &hi, &lo)) { LOG("Bad range \"%s\"", comma); errors++; continue; } if (hi) { /* is a range */ if (-1 == hi || hi > g.last) hi = g.last; if (lo < g.first) lo = g.first; if (lo < start) lo = start; for (i = lo; i <= hi; i++) (*doit) (newsgroup, i); } else (*doit) (newsgroup, lo); } } } else for (i = (start ? start : g.first); i <= g.last; i++) (*doit) (newsgroup, i); } if (since) times_fin(); group_fin(); if (!dhro) dh_close(); _exit(errors ? 3 : 0); } sn-0.3.8/post.c0000600000175000001440000001315710042571016013737 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Accept a post from the network. * Does not check for .nopost, relies on SNPOST script for that. * Oddly, LIST checks .nopost, in group_info(). This is a bit * inconsistent, since if snntpd says can't post here, we should not * accept the article, but we do, then discard. In a later version I * might just have LIST always have the posting flag set to 'y', * since according to RFC it is almost meaningless. */ #include #include #include #include #include #include #include "config.h" #include "snntpd.h" #include "args.h" #include "unfold.h" #include #include #include #include static const char ver_ctrl_id[] = "$Id: post.c 29 2004-04-24 23:02:38Z patrik $"; static int tmpfd = -1; static bool have_newsgroups; static bool have_control; static int error; #define ER_READ 1 #define ER_WRITE 2 #define ER_FMT 3 #define ER_EOF 4 #define ER_MEM 5 static char *getval (char *field) { while (*field && ':' != *field) field++; field++; while (' ' == *field || '\t' == *field) field++; return field; } static int write_tmp (char *line, int len) { static char env_control[1000]; static char env_newsgroups[1000]; if (error) return 0; if (0 == strncasecmp(line, "X-sn-", 5)) return 0; else if (!have_control && 0 == strncasecmp(line, "Control:", 8)) { strcpy(env_control, "CONTROL="); strncat(env_control, getval(line), 991); if (putenv(env_control)) error = ER_MEM; else have_control = TRUE; } if (-1 == write(tmpfd, line, len)) error = ER_WRITE; if (-1 == write(tmpfd, "\r\n", 2)) error = ER_WRITE; if (!have_newsgroups && 0 == strncasecmp(line, "Newsgroups:", 11)) { char *p; for (line = p = getval(line); *p; p++) if (',' == *p) *p = ' '; strcpy(env_newsgroups, "NEWSGROUPS="); strncat(env_newsgroups, line, 988); if (putenv(env_newsgroups)) error = ER_MEM; else have_newsgroups = TRUE; } return error; } void do_post (void) { char buf[80]; char *line; int len, p[2], pid, s; if (!posting_ok) { args_write(1, "440 Posting not allowed\r\n"); return; } if (-1 == tmpfd) { formats(buf, sizeof (buf) - 1, "/tmp/.post.%d", getpid()); tmpfd = open(buf, O_RDWR | O_CREAT | O_EXCL, 0644); if (-1 == tmpfd) { LOG("do_post:open(%s):%m", buf); goto internal; } unlink(buf); } error = 0; have_control = have_newsgroups = FALSE; putenv("CONTROL="); putenv("NEWSGROUPS="); args_write(1, "340 Go ahead\r\n"); /* Writes everything into the tmp file */ lseek(tmpfd, 0, SEEK_SET); switch (unfold(&input, write_tmp)) { case -3: break; case -2: error = ER_FMT; break; case -1: LOG("do_post:read error:%m"); case 0: return; } if (!error) { if (client_ip && *client_ip) writef(tmpfd, "NNTP-Posting-Host: %s\r\n", client_ip); if (-1 == write(tmpfd, "\r\n", 2)) error = ER_WRITE; } while ((len = readln(&input, &line, '\n')) > 0) { if (!error) { if (--len > 0) if ('\r' == line[len - 1]) --len; if (-1 == write(tmpfd, line, len)) error = ER_WRITE; if (-1 == write(tmpfd, "\r\n", 2)) error = ER_WRITE; } if (1 == len && '.' == *line) break; } if (!have_newsgroups) { args_write(1, "441 No Newsgroups line\r\n"); return; } if (error) { switch (error) { case ER_READ: LOG("do_post: read error:%m?"); break; case ER_WRITE: LOG("do_post: write(tmp):%m"); break; case ER_MEM: LOG("do_post: no memory"); break; case ER_FMT: args_write(1, "441 Bad article format\r\n"); return; case ER_EOF: return; } internal: args_write(1, "441 Internal error\r\n"); return; } ftruncate(tmpfd, lseek(tmpfd, 0, SEEK_CUR)); if (-1 == (pid = pipefork(p))) { LOG("do_post:pipe/fork:%m"); goto internal; } if (0 == pid) { char *av[2]; close(p[0]); lseek(tmpfd, 0, SEEK_SET); dup2(tmpfd, 0); dup2(p[1], 1); av[1] = 0; *av = "./.SNPOST"; execv(*av, av); if (ENOENT == errno) { *av += 3; execvp(*av, av); } fail(2, "do_post:exec(%s):%m", *av); } close(p[1]); *buf = '\0'; do { struct readln prog; if (readln_ready(p[0], 0, &prog)) break; while ((len = readln(&prog, &line, '\n')) > 0) { line[--len] = '\0'; LOG("SNPOST:%s", line); if (len > 0) if ('\r' == line[len - 1]) --len; if (len > sizeof (buf) - 1) len = sizeof (buf) - 1; strncpy(buf, line, len)[len] = '\0'; } readln_done(&prog); } while (0); while (-1 == waitpid(pid, &s, 0) && EINTR == errno) ; if (WIFEXITED(s)) { if (0 == (s = WEXITSTATUS(s))) { args_write(1, "240 %s\r\n", *buf ? buf : "Yummy"); return; } LOG("do_post:SNPOST died with %d", s); } else if (WIFSIGNALED(s)) LOG("do_post:SNPOST caught signal %d", WTERMSIG(s)); else LOG("do_post:SNPOST unknown wait status %d", s); args_write(1, "441 %s\r\n", *buf ? buf : "Posting failed"); } sn-0.3.8/INSTALL.run0000600000175000001440000001716110105424251014437 0ustar patrikusers00000000000000You have to be root or the owner of SNROOT to do anything listed here. You should have already built and installed sn, see file INSTALL. "SNROOT" and "PREFIX" refer to values as you have set in the Makefile. In the following, remember that (almost) every file and directory under and including SNROOT must be owned be the same user and group. (In some cases the owner may be root instead.) sn won't complain about writeable files. 1) Add newsgroups: $ snnewgroup comp.os.linux.announce my.isps.server 119 $ snnewgroup alt.lemmings my.other.isps.server $ snnewgroup local.private 119 is the default port number, and you can omit it. If you leave out the news server address, the newsgroup will get created as a local one. (You can tell a global group by the symlink news.group.name/.outgoing, which points somewhere into one of SNROOT/.outgoing/*). A local group doesn't have an upstream feed; it gets articles when people post to it. Each newsgroup has at most one upstream feed. If my.isps.server on port PORT requires a username and password: $ cd SNROOT/.outgoing/my.isps.server:PORT $ echo my-username >username $ echo my-password >password $ chmod 600 username password where PORT is the servers port number, usually 119. You can add new newsgroups at any time. The sn package won't automatically create newsgroups for you. man snnewgroup(8), sndelgroup(8). Optionally request a list of newsgroups from your server, when it connects. "Optionally" because these lists are usually very long. If you do this, check your email after step 2. $ echo 'mail me@myhost' >/SNROOT/.outgoing/my.isps.server:119/request-list Replace "me@myhost" with your email address. You can also replace "mail me@myhost" with any other shell command that will read data from standard input. This file will be deleted when the command runs successfully. (Later on you can get a list of new groups by putting the command in request-newgroups instead.) 2) If you've added any global newsgroups, fetch some news: $ snget snget will collect articles from the subscribed-to groups (as in step 1) and store them under their respective group directories. Status messages will be written to standard error. On first run, expect "out of sync" messages; this is normal. If you expect lots of articles you can use $ snget -m 100 to limit it to 100 articles per newsgroup (the default is 200). The -m option is useful only when priming the newsgroups. man snget(8). 3) Enable news reading via NNTP. You can run snntpd under inetd/tcpd (most people will do this), or under tcpserver. (Or any other program which will let you connect a socket to stdin/stdout of snntpd.) "logger ..." is used to store snntpd log messages, which means the messages end up whereever /etc/syslog.conf says to put it. The example access control policy here is to permit reading and posting from local clients, and to deny all access to everyone else. The local network is assumed to be 192.168.9.0. For more complicated posting access control, see the PREFIX/sbin/SNPOST script, which can do fine-grain control. The coarse grain posting access here is determined by the POSTING_OK environment variable. Running under inetd/tcpd: In /etc/hosts.allow, insert a line: nntpd : localhost, 192.168.9. : setenv = POSTING_OK In /etc/hosts.deny, insert a line: nntpd : ALL See man 5 hosts_access for more complex configurations. See man snntpd for more on POSTING_OK. NOTE: the "setenv" capability is said to be unworkable in the tcpd that ships with certain versions of the Red Hat Linux distribution. Create a small shell wrapper for snntpd, call it "nntpd": #!/bin/sh # snntpd needs to find snstore if it is to accept news postings. # Replace PREFIX with the real value of PREFIX from the Makefile. PATH=PREFIX/sbin:$PATH export PATH # Enable posting? Can also do this from /etc/hosts.allow POSTING_OK=1; export POSTING_OK # yes # No: unset POSTING_OK # Run snntpd, and log errors in the system log: PREFIX/sbin/snntpd /usr/bin/logger -p news.info # Run snntpd, and log errors in a log file: # PREFIX/sbin/snntpd 2>>/where/you/want/the/snntpd.log # End of wrapper script. In /etc/inetd.conf, insert a line: nntp stream tcp nowait root.root /path/to/tcpd /path/to/nntpd You can change root.root to the owner and group of SNROOT; however, snntpd always drops root. Hup inetd and run your newsreader. The news spool can be read only via NNTP because of its unorthodox structure. man snntpd(8), inetd(8), tcpd(8), hosts_access(5) Running under tcpserver: If you're not running snntpd for the system, using tcpserver is the simplest option. Create the access control cdb file, for example: tcprules news.cdb tmp.cdb <&1 >/dev/null & If you're worried about snget taking up all your bandwidth, use the -h option to snget (see the man page) to throttle its connection. man snget(8), pppd(8). 6) Tuning. After some days or weeks of use, you can start tuning the expiration: $ echo '10d' >SNROOT/alt.lemmings/.expire expires alt.lemmings in 10 days instead of the default of 7. You can also say '2w' for 2 weeks, or '1m' for one month. man snexpire(8). If USE_ZLIB was set in the Makefile (this is the default), you can enable compression on selected newsgroups, which by now would be easy to identify: $ touch /SNROOT/comp.source-code.big/.compress says new articles entered in comp.source-code.big will be candidates for compression. Old articles won't be touched. Compression and decompression take place transparently. (You can see if zlib was compiled in: "ident snntpd |grep ZLIB"). man snstore(8). Deny posting on a per-newsgroup basis: $ touch /SNROOT/alt.usenet.noise.noise.noise/.nopost This disallows posting (via "POST" NNTP command) for this group, but permits new articles from it's upstream host. Effectively alt.usenet.noise.noise.noise becomes readonly to users. See the PREFIX/sbin/SNPOST script. man snntpd(8). 7) Boot script. In one of your system rc scripts, after the filesystems have been mounted and fsck has been run, execute the following: cd SNROOT rm .newsgroup .table .chain find . -type f -name "+*" -exec rm -f {} \; PREFIX/sbin/snprimedb -i PREFIX/sbin/snscan -n * | PREFIX/sbin/snprimedb; This ensures that sn's ID database is in sync with the news spool. You can read INSTALL.notes for other ways to use sn. If you are having problems with sn, please read FAQ to see if your question has already been answered, before asking the mailing list for assistance. sn-0.3.8/snfetch.c0000600000175000001440000002557110043770471014416 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Fetch articles with a degree of pipelining. */ #include #include #include #include #include #include #include "config.h" #include "args.h" #include "dhash.h" #include "parameters.h" #include "key.h" #include #include #include #include #include static const char ver_ctrl_id[] = "$Id: snfetch.c 40 2004-04-28 18:00:25Z patrik $"; struct readln input = { 0, }; bool newsbatch = FALSE; int pipelining = 0; int debug = 0; int bytesin = 0; int nrhave; int nrdup; static void nomem (void) { fail(2, "No memory"); } static void badresponse (char *cmd) { LOG("Bad response to %s, got \"%s\"", cmd, args_inbuf); _exit(3); } static void badoutput (char *cmd, char *line) { LOG("Bad output to %s, got \"%s\"", cmd, line); } static int doread (void) { int i; switch ((i = args_read(&input))) { case -1: fail(2, "network read error:%m"); case 0: fail(3, "invalid reply :%s", args_inbuf); } return i; } static struct art { int serial; char *id; } *arts = NULL; static int nr_arts = 0; static void addserial (int serial, char *messageid, int len) { static int size = 0; char *id; id = messageid; switch (key_add(&id, len)) { case -1: nomem(); case 1: nrdup++; return; case 0:; } if (nr_arts >= size) { struct art *tmp; int newsize; newsize = (size ? size * 2 : 30); if (!(tmp = malloc(sizeof (*arts) * newsize))) nomem(); if (size) { memcpy(tmp, arts, sizeof (*arts) * size); free(arts); } arts = tmp; size = newsize; } arts[nr_arts].serial = serial; arts[nr_arts++].id = id; } void sendstat (int from, int to) { int i, n, p, stopwriting, startreading; if (debug) LOG("sendstat:trying"); if ((p = to - from) > pipelining) p = pipelining; stopwriting = to; to += p; startreading = from + p; for (n = from; n <= to; n++) { if (n <= stopwriting) if (-1 == args_write(7, "STAT %d\r\n", n)) fail(2, "sendstat:args_write:%m"); if (n >= startreading) { char *cp; char *id; char *end; if (doread() < 3) badresponse("STAT"); i = strtoul(args[0], &end, 10); if (423 == i || 430 == i) continue; if (223 == i && !*end) if ('<' == args[2][0]) if ((cp = strchr(id = args[2] + 1, '>'))) { *cp = '\0'; i = strtoul(args[1], &end, 10); if (i > 0 && !*end) { addserial(i, id, cp - id); continue; } } badoutput("STAT", args_inbuf); } } } void sendxhdr (int from, int to) { char *fmt; if (-1 == to) fmt = "XHDR Message-Id %d-\r\n"; else fmt = "XHDR Message-Id %d-%d\r\n"; args_write(7, fmt, from, to); doread(); if (221 != strtoul(args[0], 0, 10)) { sendstat(from, to); return; } for (;;) { char *line; int len, serial; char *cp; char *id; len = readln(&input, &line, '\n'); if (len <= 0) fail(2, "readln:%m"); line[--len] = '\0'; if (len > 0) if ('\r' == line[len - 1]) line[--len] = '\0'; if (1 == len && '.' == line[0]) return; serial = strtoul(line, &cp, 10); if (serial > 0) if (cp && (' ' == *cp || '\t' == *cp)) if ((cp = strchr(cp, '<'))) if ((cp = strchr(id = cp + 1, '>'))) { *cp = '\0'; addserial(serial, id, cp - id); continue; } badoutput("XHDR", line); } } void readprint (void) { int len; char *line; static struct b b = { 0, }; char *endline; int endlinelen; bool seenbreak; if (newsbatch) { endline = "\n"; endlinelen = 1; } else { endline = "\r\n"; endlinelen = 2; } b.used = 0; seenbreak = FALSE; while ((len = readln(&input, &line, '\n')) > 1) { bytesin += len; line[--len] = '\0'; if (len > 0) if ('\r' == line[len - 1]) line[--len] = '\0'; if (0 == len) seenbreak = TRUE; if ('.' == *line) { if (1 == len) break; else if (newsbatch) { line++; len--; } } if (len) if (b_appendl(&b, line, len)) nomem(); if (b_appendl(&b, endline, endlinelen)) nomem(); } if (len <= 0) fail(4, "readprint:readln:%m"); if (len != 1) fail(3, "readprint:bad format"); if (!seenbreak) if (b_appendl(&b, endline, endlinelen)) nomem(); if (newsbatch) { char buf[64]; struct iovec v[2]; v[0].iov_len = formats(buf, sizeof (buf) - 1, "#! rnews %d\n", b.used); v[0].iov_base = buf; v[1].iov_base = b.buf; v[1].iov_len = b.used; len = writev(1, v, 2); } else { if (b_appendl(&b, ".\r\n", 3)) nomem(); len = write(1, b.buf, b.used); } if (-1 == len) fail(2, "readprint:write:%m"); } int fetch (int *nr) { int n, p, collected, last; last = collected = 0; p = pipelining; if (p > nr_arts) p = nr_arts; for (n = 0; n < nr_arts + p; n++) { if (n < nr_arts) { struct data d; d.messageid = arts[n].id; if (0 == dh_find(&d, FALSE)) { nrhave++; arts[n].serial = -1; } else args_write(7, "ARTICLE %d\r\n", arts[n].serial); } if (n >= p && arts[n - p].serial > -1) { char *end; if (doread() < 3) badresponse("ARTICLE"); switch (strtoul(args[0], &end, 10)) { default: badresponse("ARTICLE"); case 220: last = strtoul(args[1], &end, 10); readprint(); collected++; break; case 430: case 423: if (*end) fail(3, "fetch:Bad response to ARTICLE, got \"%s\"", args_inbuf); } } } *nr = collected; return last; } static int cat (char *fn, int *val) { char buf[64]; int i; char *end; int fd = open(fn, O_RDONLY); if (-1 == fd) return -1; i = read(fd, buf, sizeof (buf) - 1); close(fd); if (-1 == i) return -1; buf[i] = '\0'; *val = strtoul(buf, &end, 10); if (*end && *end != '\n') return -1; return 0; } void usage (void) { fail(1, "Usage:%s [-r] [-c depth] [-t timeout] newsgroup [serial [max]]", progname); } int main (int argc, char **argv) { time_t t; char *newsgroup; int i; char *cp; int serial = -1; int max = -1; int timeout = 60; int from, to, last, nr; bool logpid = FALSE; int fd, len; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); while ((i = opt_get(argc, argv, "tc")) > -1) switch (i) { case 'r': newsbatch = TRUE; break; case 'd': debug++; break; case 'V': version(); _exit(0); case 't': if (!opt_arg) usage(); timeout = strtoul(opt_arg, &cp, 10); if (timeout <= 0 || *cp) usage(); break; case 'c': if (!opt_arg) usage(); pipelining = strtoul(opt_arg, &cp, 10); if (pipelining < 0 || *cp) usage(); break; case 'P': logpid = TRUE; break; default: usage(); } if (opt_ind >= argc) usage(); newsgroup = argv[opt_ind++]; if (opt_ind < argc) { if ((serial = strtoul(argv[opt_ind++], &cp, 10)) < 0) fail(1, "serial must be positive"); else if (*cp) usage(); if (opt_ind < argc) { if ((max = strtoul(argv[opt_ind++], &cp, 10)) < 0) fail(1, "max must be positive"); else if (*cp) usage(); } } if ((cp = malloc(i = strlen(progname) + strlen(newsgroup) + 32))) { if (!logpid) formats(cp, i - 1, "%s:%s", progname, newsgroup); else formats(cp, i - 1, "%s[%u]:%s", progname, getpid(), newsgroup); progname = cp; } parameters(TRUE); if (-1 == chdir(snroot) || -1 == chdir(newsgroup)) fail(2, "chdir(%s/%s):%m", snroot, newsgroup); if (-1 == serial) { if (cat(".serial", &serial) < 0) { LOG("open(.serial):%m"); serial = 0; } else if (serial < 0) { LOG("Bad serial %d changed to 0", serial); serial = 0; } } if (-1 == max) { if (cat(".max", &max) < 0) max = -1; else if (max < 0) { LOG("Bad max %d changed to -1", max); max = -1; } } if (pipelining < 0) { LOG("Bad pipelining %d changed to 0", pipelining); pipelining = 0; } if (-1 == readln_ready(6, timeout, &input)) fail(2, "readln_ready:%m"); time(&t); if (-1 == args_write(7, "GROUP %s\r\n", newsgroup)) fail(2, "args_write:%m"); if (doread() < 5 || strtoul(args[0], &cp, 10) != 211 || *cp) badresponse("GROUP"); from = strtoul(args[2], &cp, 10); if (from < 0 || *cp) badresponse("GROUP"); to = strtoul(args[3], &cp, 10); if (to < 0 || *cp) badresponse("GROUP"); if (to <= from) fail(0, "Empty newsgroup:%s", args_inbuf); if (serial == to) fail(0, "No new articles"); last = 0; to += 2; /* look ahead a bit */ if (-1 == dh_open("../", FALSE)) LOG("WARNING:you will have duplicates"); if (serial > to || serial < from) LOG("out of sync, %d not between %d-%d", serial, last = from, to); else from = serial; /* normally taken */ if (max > 0) if (to - from > max) from = to - max - 1; /* -1 added to make the number of arts downloaded == max. */ nrhave = nrdup = 0; sendxhdr(from, to); if (!nr_arts || (last = fetch(&nr)) <= 0) fail(0, "Nothing to fetch"); dh_close(); LOG("%d articles (%d bytes) in %d seconds", nr, bytesin, time(NULL) - t); LOG1("new/dup/present = %d/%d/%d", nr, nrdup, nrhave); if (-1 == (fd = open(".serial.tmp", O_WRONLY | O_CREAT, 0644))) LOG("open(.serial.tmp):%m"); else if (-1 == (len = writef(fd, "%d\n", last))) LOG("write(.serial.tmp):%m"); else { ftruncate(fd, len); close(fd); key_free(); _exit(0); } LOG("Last serial fetched was %d", last); _exit(2); } sn-0.3.8/key.c0000600000175000001440000000411410043770471013542 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Generic hash table lookup. Store only keys, not data. All keys * must be unique. Cannot delete. This is used in 2 places: to * store a list of subscribed newsgroups in snntpd; and to remember * seen message IDs in snfetch. */ #include #include #include "key.h" static const char ver_ctrl_id[] = "$Id: key.c 40 2004-04-28 18:00:25Z patrik $"; struct buf { struct buf *next; char buf[1000]; }; static struct buf *head; static int avail = 0; static char *keybuf = NULL; struct key *key_table[KEY_TABLE_SIZE] = { 0, }; int nr_keys = 0; static unsigned hv; static void hash (char *key, int len) { hv = 5381; while (len) { --len; hv += (hv << 5); hv ^= (unsigned int) *key++; } hv %= 128; } char *key_exists (char *key, int len) { struct key *kp; hash(key, len); for (kp = key_table[hv]; kp; kp = kp->next) if (kp->len == len) if (0 == memcmp(KEY(kp), key, len)) return KEY(kp); return NULL; } int key_add (char **keyp, int len) { struct key *kp; char *key; int size; key = *keyp; if (key_exists(key, len)) return 1; size = len + sizeof (*kp); size += 4 - (size % 4); if (avail < size) { struct buf *x; if (!(x = malloc(sizeof (struct buf)))) return -1; x->next = head; head = x; avail = 1000; keybuf = head->buf; } kp = (struct key *) keybuf; kp->len = len; memcpy(*keyp = KEY(kp), key, len + 1); /* Bugfix: +1 so that it copies the '\0' also. */ kp->next = key_table[hv]; key_table[hv] = kp; keybuf += size; avail -= size; nr_keys++; return 0; } void key_free (void) { int i; for (i = 0; i < sizeof (key_table) / sizeof (*key_table); i++) key_table[i] = 0; while (head) { struct buf *tmp; tmp = head->next; free(head); head = tmp; } nr_keys = avail = 0; } sn-0.3.8/key.h0000600000175000001440000000164407564256733013572 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef KEY_H #define KEY_H /* * Generic hash table lookup. Store only keys, not data. All keys * must be unique. Cannot delete. This is used in 2 places: to * store a list of subscribed newsgroups in snntpd; and to remember * seen message IDs in snfetch. */ extern char *key_exists (char *key, int len); extern int key_add (char **keyp, int len); extern void key_free (void); struct key { struct key *next; int len; }; #define KEY(kp) ((char *)(kp) + sizeof(struct key)) #define KEY_TABLE_SIZE 128 extern struct key *key_table[KEY_TABLE_SIZE]; extern int nr_keys; #define for_each_key(i,n,kp) \ for (n = nr_keys, i = 0; i < KEY_TABLE_SIZE && n > 0; i++) \ for (kp = key_table[i]; kp; kp = kp->next, n--) #endif sn-0.3.8/store.c0000600000175000001440000003163510042571016014107 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Back end of snstore. * How it works: Every time an article is to be stored, we find the * next available slot to use, lock the fd, write the head and * the body, update the info, and * unlock. If the file is full, we reorder the file by writing a * new file and copying the heads, then the bodies. Reordering is * done from a call to cache_invalidate() which calls sto_free(). * This way, we are almost always guaranteed a valid article file, * even if the arrangement is sub-optimal. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "times.h" #include "art.h" /* Shut up compiler messages about discarding volatile */ #undef VOLATILE #define VOLATILE /* nothing */ #include "artfile.h" #include "cache.h" #include "group.h" #include #include #include static const char ver_ctrl_id[] = "$Id: store.c 29 2004-04-24 23:02:38Z patrik $"; #ifdef USE_ZLIB #include "body.h" #endif extern int rename (const char *old, const char *new); extern ssize_t writev(); /* Slackware 4 needs this */ struct storeobj { char *filename; int compressok; int fd; struct file *file; }; static int sto_cmp (void *a, void *b) { register struct storeobj *x = (struct storeobj *) a; register struct storeobj *y = (struct storeobj *) b; return strcmp(x->filename, y->filename); } static int nosigio (ssize_t(*op) (), int fd, char *buf, int len) { int er; while (-1 == (er = (*op) (fd, buf, len))) if (EINTR != errno) { LOG1("nosigio:%m"); break; } return er; } static int copyart (int tofd, int fromfd, int fromseek, int len) { char buf[1024]; if (-1 == lseek(fromfd, fromseek, SEEK_SET)) { LOG("copyart:lseek:%m"); return -1; } for (; len > 1024; len -= 1024) if (-1 == nosigio(read, fromfd, buf, 1024) || -1 == nosigio(write, tofd, buf, 1024)) return -1; if (-1 == nosigio(read, fromfd, buf, len) || -1 == nosigio(write, tofd, buf, len)) return -1; return 0; } static int checkindex (struct info *p) { if (p->hoffset > 0) { if (p->boffset > 0) if (p->hlen > 0) if (p->blen > 0) return 0; } else if (-1 == p->hoffset && -1 == p->boffset) return 1; return -1; } /* * Convention: First one to create the file has it "locked". It's * ok if we fail here, we can't do more damage than is already done. * The next call must be to invalidate the object. */ static void reorder (struct storeobj *sp) { char tmpname[GROUPNAMELEN + 32]; char *p; char *q; int fd; struct file f = { 0, }; int er, i; for (p = tmpname, q = sp->filename; (*p++ = *q++) != '/';) ; *p++ = '+'; while ((*p++ = *q++)) ; fd = open(tmpname, O_RDWR | O_CREAT | O_EXCL, 0644); if (-1 == fd) { if (EEXIST != errno) LOG("reorder:open(%s):%m", tmpname); return; } f.magic = FILE_MAGIC; if (-1 == nosigio(write, fd, (char *) &f, sizeof (f))) goto fail; /* * If an article has been cancelled, its info[] will be all -1; * other cases are an error. The info[] is still copied, but the * contents are not. Major checking going on. * Also we have to be careful that sncancel is not simultaneously * trying to cancel an article we are copying. We don't have to * try too hard, since it isn't likely, and the consequences of it * happening are not bad. */ if (-1 == lockf(sp->fd, F_TLOCK, 0)) { char *p; if (EAGAIN != errno) p = "reorder:can't lockf %s:%m"; else p = "reorder:article in %s being cancelled?"; LOG(p, sp->filename); goto fail; } for (i = 0; i < ARTSPERFILE; i++) switch (checkindex(sp->file->info + i)) { case 0: /* all ok */ if ((f.info[i].hoffset = lseek(fd, 0, SEEK_END)) > 0) { f.info[i].hlen = sp->file->info[i].hlen; f.info[i].blen = sp->file->info[i].blen; er = copyart(fd, sp->fd, sp->file->info[i].hoffset, f.info[i].hlen); if (0 == er) break; } else LOG("reorder:lseek(%s):%m", tmpname); goto fail; case 1: /* cancelled article */ f.info[i].boffset = f.info[i].hoffset = f.info[i].blen = f.info[i].hlen = -1; break; case -1: LOG("reorder:corrupt index in %s", sp->filename); goto fail; } for (i = 0; i < ARTSPERFILE; i++) { if (-1 == f.info[i].boffset) continue; if (-1 == (f.info[i].boffset = lseek(fd, 0, SEEK_END))) { LOG("reorder:lseek(%s):%m", tmpname); goto fail; } er = copyart(fd, sp->fd, sp->file->info[i].boffset, f.info[i].blen); if (-1 == er) goto fail; } if (-1 == lseek(fd, 0, SEEK_SET)) { LOG("reorder:lseek:%m"); goto fail; } if (-1 == nosigio(write, fd, (char *) &f, sizeof (f))) goto fail; if (-1 == rename(tmpname, sp->filename)) { LOG("reorder:rename:%m"); goto fail; } close(fd); return; fail: LOG("reorder:write failed for %s:%m", sp->filename); if (-1 == unlink(tmpname)) LOG("reorder:unlink(%s):%m", tmpname); close(fd); } static void sto_free (void *p) { struct storeobj *sp = (struct storeobj *) p; close(sp->fd); if (-1 == munmap((caddr_t) sp->file, sizeof (struct file))) LOG("sto_free:munmap:%m"); free(sp); } static int desc; /* We want a large cache, so we don't end up flushing it often. */ int sto_init (void) { desc = cache_init(8, sto_cmp, sto_free, NULL); if (-1 == desc) return -1; return 0; } void sto_fin (void) { if (debug >= 3) { int hit, miss; cache_stat(desc, &hit, &miss); LOG("sto_fin:cache requests:%dT=%dH+%dM", hit + miss, hit, miss); } cache_fin(desc); } /* * Create and initialize article file if it doesn't already exist. * If it does exist, it would have been initialized. */ static int tryopen (char *fn) { char fn2[GROUPNAMELEN + 32]; int fd, i; char *p; char *q; struct timeval tv; if ((fd = open(fn, O_RDWR)) > -1) { lockf(fd, F_LOCK, 0); return fd; } for (p = fn2, q = fn; (*p++ = *q++) != '/';) ; *p++ = '+'; *p++ = '+'; gettimeofday(&tv, 0); /* * tmp file is 30 second lock. What is the advantage of this over * using, say, the pid? Minor: creation more likely to fail early at * open() than later at link(). XXX should lock life be longer or * shorter than timeout? If longer snstore can fail prematurely; if * shorter garbage locks can accumulate. */ i = tv.tv_sec / 30; do *p++ = '0' + (i % 10); while ((i /= 10)); *p++ = '\0'; for (i = 0; i < 100; nap(0, 300 + i), i++) { static struct file f = { FILE_MAGIC, {{0,},}, }; if (14 == i % 15) LOG("tryopen:racing on %s", fn); if ((fd = open(fn2, O_RDWR | O_CREAT | O_EXCL, 0644)) > -1) { lockf(fd, F_LOCK, 0); if (sizeof (f) == write(fd, (char *) &f, sizeof (f))) if (0 == link(fn2, fn)) { unlink(fn2); return fd; } close(fd); unlink(fn2); } /* probably EEXIST due to another process */ if ((fd = open(fn, O_RDWR)) > -1) { lockf(fd, F_LOCK, 0); return fd; } } LOG("tryopen:timed out opening %s:%m", fn); return -1; } /* * Returns the storeobj already locked. The file may be full; caller * must know what to do. Caller must specify a file name in * sequence, no gaps allowed. */ static struct storeobj *getstore (char *filename) { struct storeobj *sp; struct storeobj s; int fd; struct file *fp; #ifdef USE_ZLIB char buf[GROUPNAMELEN + 32]; int c; #endif s.filename = filename; if ((sp = cache_find(desc, &s))) { lockf(sp->fd, F_LOCK, 0); return sp; } if (-1 == (fd = tryopen(filename))) return 0; /* file exists and is open, locked, and initialized. */ fp = (struct file *) mmap(0, sizeof (*fp), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (!fp || fp == MAP_FAILED) LOG("getstore:mmap(%s):%m", filename); else if (!(sp = malloc(sizeof (*sp) + strlen(filename) + 1))) { LOG("getstore:no memory"); munmap(fp, sizeof (*fp)); } else { sp->fd = fd; sp->file = fp; sp->filename = (char *) sp + sizeof (*sp); strcpy(sp->filename, filename); #ifdef USE_ZLIB { char *p; char *q; for (p = buf, q = filename; (*p++ = *q++) != '/';) ; strcpy(p, ".compress"); } if ((fd = open(buf, O_RDONLY)) > -1) { sp->compressok = 1024; if ((c = read(fd, buf, sizeof (buf) - 1)) > 0) { char *end; buf[c] = '\0'; c = strtoul(buf, &end, 10); if (c > 1024 && '\n' == *end) sp->compressok = c; } close(fd); } else #endif sp->compressok = 0; cache_insert(desc, sp); return sp; } close(fd); return 0; } /* * Start at file 1 (article 10), not 0. Thanks Marko. * We always start with the last file in a group, holes do not * affect us. */ int sto_add (char *newsgroup, struct article *ap) { struct storeobj *sp; struct info info; int filenum, slot, er; char *p; char filename[GROUPNAMELEN + 32]; struct group g; if (-1 == group_info(newsgroup, &g)) return -1; for (filenum = (g.last > 0 ? g.last / ARTSPERFILE : 1);; filenum++) { formats(filename, sizeof (filename) - 1, "%s/%d", newsgroup, filenum); if (!(sp = getstore(filename))) return -1; for (slot = 0; slot < ARTSPERFILE; slot++) if (0 == sp->file->info[slot].hoffset) break; if (slot < ARTSPERFILE) break; lseek(sp->fd, 0, SEEK_SET); lockf(sp->fd, F_ULOCK, 0); cache_invalidate(desc, sp); } /* Replace tabs, so art_makexover won't have to */ for (p = ap->head; *p; p++) if ('\t' == *p) *p = ' '; er = (int) (info.hoffset = lseek(sp->fd, 0, SEEK_END)); if (er > -1) { struct iovec iov[4]; char *zbuf; info.hlen = ap->hlen + 2; info.boffset = info.hoffset + ap->hlen + 2; info.blen = ap->blen + 2; iov[0].iov_base = ""; iov[0].iov_len = 1; iov[1].iov_base = ap->head; iov[1].iov_len = ap->hlen + 1; iov[2].iov_base = ""; iov[2].iov_len = 1; zbuf = 0; if (ap->body && ap->blen > 0) { #ifdef USE_ZLIB /* Failure here just means don't compress */ unsigned long zlen; if (sp->compressok && ap->blen > sp->compressok) { zlen = ap->blen + ap->blen / 1000 + 13; if ((zbuf = malloc(zlen + COMPRESS_MAGIC_LEN))) { memcpy(zbuf, COMPRESS_MAGIC, COMPRESS_MAGIC_LEN); er = compress((unsigned char *) zbuf + COMPRESS_MAGIC_LEN, &zlen, (unsigned char *) ap->body, (unsigned) ap->blen); if (Z_OK != er) { LOG1("sto_add:compression failed on %s:%d, \"Z_%s_ERROR\"", newsgroup, (filenum * ARTSPERFILE) + slot, (Z_BUF_ERROR == er) ? "BUF" : ((Z_MEM_ERROR == er) ? "MEM" : "[unknown]")); free(zbuf); zbuf = 0; } else zbuf[zlen + COMPRESS_MAGIC_LEN] = '\0'; } else LOG("sto_add:No memory to compress"); } if (zbuf) { iov[3].iov_base = zbuf; iov[3].iov_len = zlen + 1 + COMPRESS_MAGIC_LEN; } else #endif { iov[3].iov_base = ap->body; iov[3].iov_len = ap->blen + 1; } } else { iov[3].iov_base = ""; iov[3].iov_len = 1; } info.blen = iov[3].iov_len + 1; if ((er = nosigio(writev, sp->fd, (char *) iov, 4)) > -1) { sp->file->info[slot] = info; er = (filenum * ARTSPERFILE) + slot; } else LOG("sto_add:writev(%s):%m", sp->filename); if (zbuf) free(zbuf); } else LOG("sto_add:lseek(%s):%m", sp->filename); lseek(sp->fd, 0, SEEK_SET); lockf(sp->fd, F_ULOCK, 0); if (slot == ARTSPERFILE - 1) { reorder(sp); cache_invalidate(desc, sp); } return er; } sn-0.3.8/snsplit.c0000600000175000001440000002037510043507662014455 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Run a program on each article in an article stream. * We could use a pipe instead of tmp file, but user might want to * seek on it. Anyway profiling indicates the pipe method not to be * significantly faster. */ #include #include #include #include #include #include #include "config.h" #include "unfold.h" #include #include #include #include static const char ver_ctrl_id[] = "$Id: snsplit.c 36 2004-04-27 16:52:02Z patrik $"; static void nomem (void) { fail(2, "no memory"); } int debug; struct readln input = { 0, }; static int tmpfd; #define SEQUENCE "SEQUENCE=000000" #define MAXSEQ 999999 static char seqbuf[sizeof (SEQUENCE)]; static char bytebuf[40]; static char hlbuf[50]; static char blbuf[50]; static int head_lines; static int body_lines; static struct export { char *s; int len; char *name; } *exports = NULL; static int nrexports; static char **env = NULL; static int envstart, envused, envend; static int article_bytes; void clear_env (void) { int i; for (i = envstart; i < envend; i++) if (env[i]) { free(env[i]); env[i] = NULL; } for (i = 0; i < nrexports; i++) exports[i].s = 0; /* just freed */ env[envused = envstart] = NULL; } void put_env (int index, char *value) { struct export *ep; int len; char *p; char *q; ep = exports + index; ep->s = malloc(len = ep->len + 4 + 1 + strlen(value) + 1); if (!ep->s) nomem(); strcpy(ep->s, "FLD_"); for (p = ep->s + 4, q = ep->name; *q; q++, p++) if (*q >= 'a' && *q <= 'z') *p = *q - ('a' - 'A'); else if ('-' == *q) *p = '_'; *p++ = '='; strcpy(p, value); env[envused++] = ep->s; } extern char **environ; /* the real thing */ void setup_env (char **headers) { int i, nr; int seq; for (nr = 0; environ[nr]; nr++) ; seq = 0; for (i = 0; environ[i];) if (!seq && 0 == strncmp(environ[i], "SEQUENCE=", 9)) { seq = atoi(environ[i] + 9); environ[i] = environ[--nr]; environ[nr] = 0; } else if (0 == strncmp(environ[i], "FLD_", 4) || 0 == strncmp(environ[i], "BYTES=", 6) || 0 == strncmp(environ[i], "HEAD_LINES=", 11) || 0 == strncmp(environ[i], "BODY_LINES=", 11)) { environ[i] = environ[--nr]; environ[nr] = 0; } else i++; if (!(env = malloc((i + 6) * sizeof (char *)))) nomem(); for (nr = 0; nr < i; nr++) env[i] = environ[i]; env[nr++] = strcpy(seqbuf, SEQUENCE); if (seq > 0 && seq < MAXSEQ) { char *p; seq++; for (p = seqbuf + sizeof (SEQUENCE) - 2; seq > 0; p--, seq /= 10) *p += seq % 10; } env[nr++] = bytebuf; env[nr++] = hlbuf; env[nr++] = blbuf; env[nr] = 0; envused = envstart = nr; envend = nr + nrexports; if (!headers) return; exports = malloc(nrexports * sizeof (struct export)); if (!exports) nomem(); for (i = 0; i < nrexports; i++) { exports[i].s = 0; exports[i].len = strlen(exports[i].name = headers[i]); } } static void writetmp (char *buf, int len) { if (-1 == write(tmpfd, buf, len)) fail(2, "can't write to tmp file:%m"); article_bytes += len; } int write_file (char *line, int len) { int i; writetmp(line, len); writetmp("\n", 1); head_lines++; for (i = 0; i < nrexports; i++) { int n; n = exports[i].len; if (!exports[i].s && len >= n && ':' == line[n]) if (0 == strncasecmp(line, exports[i].name, n)) { char *p; for (p = line + n + 1; ' ' == *p; p++) ; put_env(i, p); break; } } return 0; } static void shortread (void) { fail(3, "unexpected EOF"); } static void badread (void) { fail(2, "can't read article:%m"); } static void badfmt (void) { fail(3, "bad rnews line"); } int read_rnews (void) { char *line; int len, c, bytes; len = readln(&input, &line, '\n'); if (0 == len) return 0; if (-1 == len) badread(); line[--len] = '\0'; if (len > 0) if ('\r' == line[len - 1]) line[--len] = '\0'; if ('#' != *line++) badfmt(); if ('!' != *line++) badfmt(); while (' ' == *line) line++; if (strncmp(line, "rnews ", 6)) badfmt(); for (line += 6; ' ' == *line; line++) ; for (bytes = 0; (c = *line); line++) if (c < '0' || c > '9') badfmt(); else { bytes *= 10; bytes += c - '0'; } len = unfold(&input, write_file); if (0 == len) shortread(); if (len < 0) badread(); bytes -= len; writetmp("\n", 1); do { if (-1 == (len = readln(&input, &line, '\n'))) badread(); if (0 == len) shortread(); if ((bytes -= len) < 0) shortread(); if (--len > 0) if ('\r' == line[len - 1]) --len; if (len) writetmp(line, len); writetmp("\n", 1); body_lines++; } while (bytes > 0); return 1; } int read_wire (void) { char *line; int len; switch (unfold(&input, write_file)) { case 0: return 0; case -1: badread(); case -2: badfmt(); } writetmp("\n", 1); while ((len = readln(&input, &line, '\n')) > 0) { if (--len > 0) if ('\r' == line[len - 1]) --len; if ('.' == *line) { if (0 == --len) return 1; line++; } if (len > 0) writetmp(line, len); writetmp("\n", 1); body_lines++; } if (0 == len) shortread(); badread(); /* Not Reached */ return 0; } void usage (void) { fail(1, "Usage: %s [-r] [header ... -] prog ...", progname); } static void incseq (void) { int i; for (i = sizeof (SEQUENCE) - 2; i > sizeof ("SEQUENCE=") - 2; i--) { seqbuf[i]++; if (seqbuf[i] <= '9') break; seqbuf[i] = '0'; } } int main (int argc, char **argv) { char buf[80]; char **xv; char **hv; char *p; int i, pid, s; int (*reader) (void); progname = ((p = strrchr(argv[0], '/')) ? p + 1 : argv[0]); reader = read_wire; while ((i = opt_get(argc, argv, "")) > -1) switch (i) { case 'r': reader = read_rnews; break; case 'd': debug++; break; case 'V': version(); _exit(0); default: usage(); } if (opt_ind >= argc) usage(); hv = argv + opt_ind; for (i = opt_ind; i < argc; i++) if ('-' == *argv[i] && !argv[i][1]) break; if (i < argc) { argv[i] = NULL; xv = argv + i + 1; } else { xv = hv; hv = NULL; } if (!*xv) usage(); if (readln_ready(0, 0, &input)) nomem(); formats(buf, sizeof (buf) - 1, "/tmp/.%s.%d", progname, getpid()); tmpfd = open(buf, O_RDWR | O_CREAT | O_EXCL, 0644); if (-1 == tmpfd) fail(2, "open(%s):%m", buf); unlink(buf); if (hv) nrexports = i - opt_ind; else nrexports = 0; setup_env(hv); for (;; incseq()) { article_bytes = head_lines = body_lines = 0; if (!reader()) break; formats(hlbuf, sizeof (hlbuf) - 1, "HEAD_LINES=%d", head_lines); formats(blbuf, sizeof (blbuf) - 1, "BODY_LINES=%d", body_lines); formats(bytebuf, sizeof (bytebuf) - 1, "BYTES=%d", article_bytes); pid = fork(); if (-1 == pid) fail(2, "fork:%m"); if (0 == pid) { lseek(tmpfd, 0, SEEK_SET); dup2(tmpfd, 0); environ = env; execvp(*xv, xv); fail(1, "exec(%s):%m", *xv); } while (-1 == waitpid(pid, &s, 0) && EINTR == errno) ; if (WIFEXITED(s)) { if ((s = WEXITSTATUS(s))) fail(s, "%s exited with %d", *xv, s); } else if (WIFSIGNALED(s)) { s = WTERMSIG(s); fail(128 + s, "%s caught signal %d", *xv, s); } else fail(128, "%s unknown wait status %d", *xv, s); lseek(tmpfd, 0, SEEK_SET); ftruncate(tmpfd, 0); if (hv) clear_env(); } _exit(0); } sn-0.3.8/store.h0000600000175000001440000000055107564256733014132 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef STORE_H #define STORE_H #include "art.h" extern int sto_init (void); extern int sto_add (char *newsgroup, struct article *ap); extern void sto_fin (void); #endif sn-0.3.8/snsplit.8.in0000600000175000001440000000516507564256733015025 0ustar patrikusers00000000000000.TH snsplit,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snsplit \- split an article stream into individual articles .SH SYNOPSIS .B snsplit .RI [- r ] .RI [ field ...\ -] .IR prog ... .br .SH DESCRIPTION .B snsplit reads an article stream from descriptor 0 and splits it into separate articles, invoking .IR prog ... on each, with the article avaiable on descriptor 0. This is useful as a quick-and-dirty way of filtering an article stream. The incoming article stream is expected to be in wire format, with lines ending in CRLF, leading dots doubled, and delimited by a dot on a line by itself. The article presented to .IR prog ... will have lines that end in a bare newline, will have all header lines unfolded, leading dots will be unstuffed, and the article will be terminated by end-of-file. .SH ARGUMENTS .TP .IR prog ... is the program (with arguments) to run on each article. If .IR prog ... exits with any kind of failure, .B snsplit aborts. .TP .IR field ...- are optional header field names. If these are specified, the value of the first header field of that name will be exported into the environment. This .IR field ... list must be terminated by the hyphen. See also .B ENVIRONMENT below. .SH OPTIONS .TP .RB - r Expect input articles in rnews batch format instead. .SH ENVIRONMENT .B snsplit sets some environment variables. If the environment already contains these variables, they will be overwritten. .TP .B SEQUENCE If already set to a positive value, it is incremented for the first article. If it isn't set, is set to one for the first article. Thereafter it is incremented for each subsequent article. The value is always a 6-digit number with leading zeroes, and it can roll over. .TP .B BYTES contains the size of the current article. .TP .B HEAD_LINES The number of lines in the head of the article, excluding the blank separator line. .TP .B BODY_LINES The number of lines in the body of the article, excluding the blank separator line. .TP .BI FLD_ FIELD If any .IR field s are specified on the command line, where .I field is the name of an article header field, then .BI FLD_ FIELD will be set to the value of .IR field , where .I FIELD is the same as .I field but with lower case characters changed to upper case, and all hyphens changed to underscores. Confusing? If .I field is .BR message-id , then .B FLD_MESSAGE_ID will be set to the value of the first Message-ID field in the current article, if there is one. .SH EXIT CODES .B snsplit exits 0 on success, 1 on usage error, 2 on system error, and 3 on article format error. If .IR prog ... exits with other than 0, .B snsplit will also exit that value. sn-0.3.8/snntpd.h0000600000175000001440000000125310042066025014256 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef SNNTPD_H #define SNNTPD_H extern bool posting_ok; extern char *client_ip; extern char *me; extern struct readln input; extern int fifo; extern int nrgroups; extern int alltolower (char *buf); extern int topline (char *group, char *fn, char *buf, int size); extern char *currentgroup; extern int currentserial; extern struct group currentinfo; extern int make_current (char *group); extern int glob (char *group, char *pattern); extern int pipefork (int p[2]); #endif /* SNNTPD_H */ sn-0.3.8/snntpd.c0000600000175000001440000002267510043507662014274 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * News reader daemon, NNTP protocol driver. */ #include #include #include #include #include #include #include #include #include #include /* inet_ntoa() */ #include /* getpeername */ #include #include "config.h" #include "art.h" #include "dhash.h" #include "group.h" #include "hostname.h" #include "parameters.h" #include "args.h" #include "snntpd.h" #include "valid.h" #include "key.h" #include "path.h" #include #include #include #include #include #include #include int debug = 0; static const char ver_ctrl_id[] = "$Id: snntpd.c 36 2004-04-27 16:52:02Z patrik $"; bool posting_ok = FALSE; char *client_ip; char *me; struct readln input; int fifo = -1; extern char *currentgroup; extern int currentserial; extern void do_article (void); extern void do_body (void); extern void do_group (void); extern void do_head (void); extern void do_help (void); extern void do_ihave (void); extern void do_last (void); extern void do_list (void); extern void do_listgroup (void); extern void do_mode (void); extern void do_newgroups (void); extern void do_newnews (void); extern void do_next (void); extern void do_post (void); extern void do_quit (void); extern void do_sendme (void); extern void do_slave (void); extern void do_stat (void); extern void do_xhdr (void); extern void do_xover (void); extern void do_xpat (void); struct cmd { char *name; void (*function) (void); char needs_args; /* Min. number of args required */ char needs_group; /* Needs a current group set */ char needs_article; /* Needs a current article set */ char needs_grouplist; }; #define S(func) #func #define DEF(func,args,group,art,gl) \ { S(func), do_ ## func, args, group, art, gl}, static struct cmd cmds[] = { DEF(article, 1, 0, 0, 0) DEF(body, 1, 0, 0, 0) DEF(group, 2, 0, 0, 1) DEF(head, 1, 0, 0, 0) DEF(help, 0, 0, 0, 0) DEF(ihave, 1, 0, 0, 0) DEF(last, 1, 1, 1, 0) DEF(list, 1, 0, 0, 1) DEF(listgroup, 1, 0, 0, 1) DEF(mode, 2, 0, 0, 0) DEF(newgroups, 3, 0, 0, 1) DEF(newnews, 4, 0, 0, 1) DEF(next, 1, 1, 1, 0) DEF(post, 1, 0, 0, 0) DEF(quit, 1, 0, 0, 0) DEF(sendme, 0, 0, 0, 0) DEF(slave, 0, 0, 0, 0) DEF(stat, 1, 0, 0, 0) DEF(xhdr, 2, 1, 0, 0) DEF(xover, 1, 1, 0, 0) DEF(xpat, 4, 1, 0, 0) }; #undef S #undef DEF static void cleanup (void) { if (debug >= 3) { int hit, miss; art_filecachestat(&hit, &miss); LOG("do_quit:cache requests:%dT=%dH+%dM", hit + miss, hit, miss); } dh_close(); group_fin(); if (nr_keys) key_free(); } void do_quit (void) { args_write(1, "205 bye\r\n"); cleanup(); _exit(0); } /* Not quite safe to do this... */ static bool checkservice = FALSE; static void handler (int signum) { if (SIGHUP == signum) { checkservice = TRUE; return; } dh_close(); group_fin(); LOG("Caught signal %d, exiting", signum); _exit(3); } static void openfifo (void) { struct stat st; if (fifo > -1) return; fifo = open(".fifo", O_RDWR | O_NONBLOCK); if (-1 == fifo) { LOG("open(fifo):%m(ok)"); return; } if (-1 == fstat(fifo, &st)) FAIL(2, "openfifo:stat(.fifo):%m"); if (!S_ISFIFO(st.st_mode)) { close(fifo); fifo = -1; LOG1(".fifo is not a fifo, ok"); } } static void docheckservice (void) { struct stat st; char *reason = "maintenance"; char buf[256]; char *p; if (0 == stat(".noservice", &st)) { if (st.st_size) if (-1 != topline(NULL, ".noservice", buf, sizeof (buf))) reason = buf; args_write(1, "400 Service going down: %s\r\n", reason); cleanup(); _exit(0); } if (0 == stat(".nopost", &st)) posting_ok = FALSE; else if ((p = getenv("POSTING_OK"))) posting_ok = TRUE; else posting_ok = FALSE; openfifo(); checkservice = FALSE; } static void init (void) { static struct { int (*f) (); char buf[40]; } con[] = { { getpeername, "TCPREMOTEIP" }, { getsockname, "TCPLOCALIP" } }; struct sigaction sa; struct sockaddr_in sin; int i, len; if (-1 == chdir(snroot)) FAIL(2, "chdir(%s):%m", snroot); (void) dh_open(NULL, FALSE); if (-1 == group_init()) { dh_close(); _exit(2); } for (i = 0; len = sizeof (sin), i < 2; i++) if (!getenv(con[i].buf)) if (0 == (*(con[i].f)) (0, (struct sockaddr *) &sin, &len)) putenv(strcat(strcat(con[i].buf, "="), inet_ntoa(sin.sin_addr))); client_ip = con->buf + sizeof ("TCPREMOTEIP") - 1; if ('=' == *client_ip) client_ip++; me = myname(); sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sigaction(SIGHUP, &sa, NULL); sa.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sa, NULL); docheckservice(); } static int getallgroups (void) { DIR *dir; struct dirent *dp; char *gr; if (!(dir = opendir("."))) { LOG("getallgroups:opendir(%s):%m", snroot); return -1; } for (nr_keys = 0; (dp = readdir(dir)); nr_keys++) if (is_valid_group(gr = dp->d_name)) if (-1 == key_add(&gr, strlen(gr))) fail(2, "No memory"); closedir(dir); return 0; } static void usage (void) { fail(1, "Usage:%s [-S] [-t timeout]", progname); } int main (int argc, char **argv) { int i; char *cp; bool greeting; int tmo = 600; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); parameters(TRUE); greeting = TRUE; while ((i = opt_get(argc, argv, "t")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'd': debug++; break; case 'V': version(); _exit(0); case 'S': greeting = FALSE; break; case 't': if (!opt_arg) fail(1, "Need value for \"t\""); if ((tmo = strtoul(opt_arg, &cp, 10)) < 0 || *cp) fail(1, "Timeout must be positive"); break; default: usage(); } if (opt_ind < argc) { int pid, lg[2]; if (-1 == (pid = pipefork(lg))) fail(2, "pipe/fork:%m"); if (0 == pid) { if (-1 == dup2(lg[0], 0)) fail(2, "dup:%m"); dup2(1, 2); close(lg[1]); execvp(argv[opt_ind], argv + opt_ind); LOG("exec(%s):%m", argv[opt_ind]); _exit(2); } close(lg[0]); if (-1 == dup2(lg[1], 2)) fail(2, "dup:%m"); } if (-1 == readln_ready(0, tmo, &input)) FAIL(2, "readln_ready:%m"); init(); set_path_var(); if (greeting) { args[1] = "READER"; do_mode(); } /* Main loop */ while (1) { int nr; switch ((nr = args_read(&input))) { case 0: args_write(1, "501 Bad command\r\n"); continue; case -1: do_quit(); /* Does not return */ } if (checkservice) docheckservice(); /* May not return */ for (i = 0; i < sizeof (cmds) / sizeof (struct cmd); i++) { if (strcasecmp(args[0], cmds[i].name)) continue; if (nr < cmds[i].needs_args) args_write(1, "501 Bad syntax\r\n"); else if (cmds[i].needs_group && !currentgroup) args_write(1, "412 No group selected\r\n"); else if (cmds[i].needs_article && -1 == currentserial) args_write(1, "420 No article selected\r\n"); else if (!nr_keys && cmds[i].needs_grouplist && -1 == getallgroups()) args_write(1, "503 No memory\r\n"); else (*cmds[i].function) (); break; } if (i >= sizeof (cmds) / sizeof (struct cmd)) args_write(1, "500 unimplemented\r\n"); } /* Not Reached */ } /* * Functions used in various places */ int alltolower (char *buf) { char *cp; for (cp = buf; *cp; cp++) if (isupper(*cp)) *cp = tolower(*cp); return (cp - buf); } int topline (char *group, char *fn, char *buf, int size) { int fd, i; char *cp; if (NULL == group) fd = open(fn, O_RDONLY); else fd = openf(0, O_RDONLY, "%s/%s", group, fn); if (-1 == fd) return -1; i = read(fd, buf, size - 1); close(fd); if (i <= 0) return i; buf[i] = '\0'; if ((cp = strchr(buf, '\n'))) { *cp = '\0'; return (cp - buf); } return i; } char *currentgroup = NULL; int currentserial = -1; /* this may be set by others */ struct group currentinfo = { 0, }; /* * Caller must downcase argment */ int make_current (char *group) { if (!currentgroup) { if (!(currentgroup = malloc(GROUPNAMELEN + 1))) { LOG("make_current:no memory"); return -1; } strcpy(currentgroup, "no.group.selected.yet"); /* Better than random crap... */ } /* prevent =junk from being selected */ if (!key_exists(group, strlen(group))) return -1; if (-1 == group_info(group, ¤tinfo)) return -1; strcpy(currentgroup, group); currentserial = -1; return 0; } int pipefork (int p[2]) { if (0 == pipe(p)) { int pid; if ((pid = fork()) > -1) return pid; close(p[0]); } return -1; } sn-0.3.8/sncancel.8.in0000600000175000001440000000002207564256733015102 0ustar patrikusers00000000000000.so man8/snscan.8 sn-0.3.8/snget.c0000600000175000001440000001064210043507662014075 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "get.h" #include "parameters.h" #include "path.h" #include "valid.h" #include #include #include #include static const char ver_ctrl_id[] = "$Id: snget.c 36 2004-04-27 16:52:02Z patrik $"; int debug = 0; void usage (void) { fail(1, "Usage:%s [-h Bps] " "[-p concurrency] [-c pipelinedepth] [-m max]", progname); } /* Main function - uses variables and functions from get.c */ int main (int argc, char **argv) { char optdebugbuf[7]; int n, mark; char *cp; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); while ((n = opt_get(argc, argv, "pthcmo")) > -1) switch (n) { case 'd': if (++debug < 6) { if (!optdebug) strcpy(optdebug = optdebugbuf, "-d"); else strcat(optdebug, "d"); } break; case 'V': version(); _exit(0); case 't': if (!opt_arg) usage(); LOG("option \"-t %s\" no longer supported", opt_arg); break; case 'p': if (!opt_arg) usage(); concurrency = strtoul(opt_arg, &cp, 10); if (concurrency > MAX_CONCURRENCY || concurrency <= 0 || *cp) fail(1, "Bad value for concurrency option -p"); break; case 'h': if (!opt_arg) usage(); throttlerate = strtoul(opt_arg, &cp, 10); if (throttlerate < 0) fail(1, "Bad value for throttle option -h"); break; case 'c': if (!(optpipelining = opt_arg)) usage(); if (strtoul(optpipelining, &cp, 10) < 0 || *cp) fail(1, "Bad value for pipeline option -c"); break; case 'm': if (!(optmax = opt_arg)) usage(); if (strtoul(optmax, &cp, 10) <= 0 || *cp) fail(1, "Bad value for max-prime-articles option -m"); break; case 'P': optlogpid = TRUE; log_with_pid(); break; default: usage(); } close(0); open("/dev/null", O_RDONLY); /* snag 6 and 7 so we can dup onto them */ if (-1 == dup2(2, 6) || -1 == dup2(2, 7) || -1 == dup2(2, 1)) fail(2, "Unable to dup standard error:%m"); parameters(TRUE); if (-1 == chdir(snroot)) fail(2, "chdir(%s):%m", snroot); init(); if (-1 == set_path_var()) fail(2, "No memory"); n = 0; if (opt_ind == argc) { DIR *dir; struct dirent *dp; struct stat st; char ch; if (!(dir = opendir("."))) fail(2, "opendir(%s):%m", snroot); while ((dp = readdir(dir))) if (is_valid_group(dp->d_name)) if (-1 == readlink(dp->d_name, &ch, 1)) /* no symlinks */ if (0 == statf(&st, "%s/.outgoing", dp->d_name)) if (S_ISDIR(st.st_mode)) if (add(dp->d_name) > -1) /* NB: add() from get.c */ n++; closedir(dir); } else { debug++; for (; opt_ind < argc; opt_ind++) if (add(argv[opt_ind]) > -1) n++; debug--; } if (n == 0) fail(0, "No groups to fetch"); for (mark = 0; jobs_not_done(); mark++) { struct timeval tv; fd_set rset; int max; while (sow() == 0) /* Start some jobs */ ; FD_ZERO(&rset); if (throttlerate) { max = throttle_setfds(&rset); if (sigusr) { sigusr = FALSE; LOG("throttling at %d bytes/sec", throttlerate); } } else max = -1; tv.tv_sec = 1; tv.tv_usec = 0; if (select(max + 1, &rset, NULL, NULL, &tv) > 0) if (throttlerate) throttle(&rset); if (sigchld || 1 == mark % 10) { sigchld = FALSE; while (reap() == 0) ; } } quit(); _exit(0); } sn-0.3.8/INSTALL0000600000175000001440000000420107565224250013640 0ustar patrikusers00000000000000If you're upgrading from a previous version of sn, please read INSTALL.upgrade first. To install sn: 0) Decide if you want the compression feature compiled in. It's safe to compile it in; you don't have to use it if you don't want to. If you do want compression, get and install libz.so or libz.a first. You'll also need zlib.h and zconf.h. These appear to be missing in some Linux distributions. The email address "postmaster" (on localhost, see also config.h) should already exist, and the email system, at least local email, should be operational. sn will use email for some error messages. The executable "/bin/mail" should exist. If you are installing on something other than Linux, (such as Solaris) remember to run the GNU utilities. I.e. when it says "make" below, you should run "gmake" if that is the name of GNU make on your system. 1) Edit Makefile. The make variables to look out for are SNROOT and PREFIX which are the news spool location and where to put the executables and man pages, respectively. Unset the ZLIB variable if you have decided to disable compression. In the following, SNROOT refers to the value of SNROOT as you have set it in the Makefile; likewise for PREFIX. SNROOT should be on a local filesystem, not NFS. 2) Edit config.h. Probably nothing to change, but follow the comments. 3) Build it. Type "make", then "make -n install" to see where things will be installed to; "make strip" optionally to strip the binaries. "make clean" if you want to start over. 4) Create the directories and the fifo: $ mkdir SNROOT $ mkdir PREFIX/sbin $ mkdir PREFIX/man/man8 $ mknod -m 600 SNROOT/.fifo p Change the ownership of SNROOT and SNROOT/.fifo to the user and group for which you want sn to run (for instance, news.news). This is important: the ownership of SNROOT determines sn's running privileges. Do NOT make the owner root. 5) If you're upgrading from a previous version of sn, please read INSTALL.upgrade next. Finally, "make install" in the source directory. Read INSTALL.run for a quick introduction on how to run sn. sn-0.3.8/sngetd.c0000600000175000001440000001654410042571016014241 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "get.h" #include "parameters.h" #include "path.h" #include "addr.h" #include "valid.h" #include #include #include #include #include static const char ver_ctrl_id[] = "$Id: sngetd.c 29 2004-04-24 23:02:38Z patrik $"; int debug = 0; static int fifofd; static int age = (10 * 60); /* don't fetch group within 10 min. of last fetch */ struct get { struct get *next; time_t t; char group[1]; }; static struct get *recently = NULL; static void readgroup (void) { static char inbuf[GROUPNAMELEN + 1]; static int start = 0; static int used = 0; time_t t; int c; if (used == start) used = start = 0; if ((c = read(fifofd, inbuf + used, (sizeof (inbuf) - 1) - used)) <= 0) return; used += c; time(&t); for (;;) { char *p; int end; for (end = start; end < used && '\n' != inbuf[end]; end++) ; if (used == end) { /* partial */ if (start > 0) { memmove(inbuf, inbuf + start, used - start); used -= start; } else used = 0; start = 0; return; } /* got a group name */ inbuf[end] = '\0'; p = inbuf + start; if (end - start < GROUPNAMELEN && '[' != *p) { struct get *gp; struct get *tmp; struct stat st; char ch; if (!is_valid_group(p)) { LOG1("%s is not a valid name", p); } else if (0 == readlink(p, &ch, 1)) { LOG1("%s is a symlink", p); } else if (-1 == statf(&st, "%s/.outgoing", p)) { LOG1("%s is a local group", p); } else if (!S_ISDIR(st.st_mode)) { LOG1("%s is not a global group", p); } else { for (tmp = 0, gp = recently; gp; tmp = gp, gp = gp->next) if ((unsigned long) gp->t + age < (unsigned long) t) { if (tmp) tmp->next = 0; else recently = NULL; do { tmp = gp->next; free(gp); } while ((gp = tmp)); gp = recently; break; } else if (0 == strcmp(gp->group, p)) break; if (!gp) { if (0 == add(p)) { LOG1("Will fetch \"%s\"", p); if ((gp = malloc(sizeof (struct get) + strlen(p)))) { strcpy(gp->group, p); gp->t = t; gp->next = recently; recently = gp; } else LOG("Oops, no memory to remember I'm fetching %s", p); } } else LOG1("Already fetched/fetching for %s", p); } } start = end + 1; } } void usage (void) { fail(1, "Usage:%s [-a age] [-h Bps] [-t timeout] " "[-p concurrency] [-c pipelinedepth]", progname); } int main (int argc, char **argv) { char optdebugbuf[7]; int n, mark; char *cp; fd_set rset; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); concurrency = 2; while ((n = opt_get(argc, argv, "apthcmo")) > -1) switch (n) { case 'a': if (!opt_arg) usage(); if ((age = strtoul(opt_arg, &cp, 10)) <= 0 || *cp) fail(1, "Refetch age must be positive"); break; case 'd': if (++debug < 5) { if (!optdebug) strcpy(optdebug = optdebugbuf, "-d"); else strcat(optdebug, "d"); } break; case 'V': version(); _exit(0); case 't': if (!opt_arg) usage(); LOG("option \"-t %s\" no longer supported", opt_arg); break; case 'p': if (!opt_arg) usage(); concurrency = strtoul(opt_arg, &cp, 10); if (concurrency > 4 || concurrency <= 0 || *cp) fail(1, "Bad value for concurrency option -p"); break; case 'h': if (!opt_arg) usage(); throttlerate = strtoul(opt_arg, &cp, 10); if (throttlerate < 0 || *cp) fail(1, "Bad value for throttle option -h"); break; case 'c': if (!(optpipelining = opt_arg)) usage(); if (strtoul(optpipelining, &cp, 10) < 0 || *cp) fail(1, "Bad value for pipeline option -c"); break; case 'm': if (!(optmax = opt_arg)) usage(); if (strtoul(optmax, &cp, 10) <= 0 || *cp) fail(1, "Bad value for max-prime-articles option -m"); break; case 'P': optlogpid = TRUE; log_with_pid(); break; default: usage(); } close(0); open("/dev/null", O_RDONLY); if (-1 == dup2(2, 6) || -1 == dup2(2, 7) || -1 == dup2(2, 1)) fail(2, "Can't dup standard error:%m"); parameters(TRUE); if (-1 == chdir(snroot)) fail(2, "chdir(%s):%m", snroot); fifofd = open(".fifo", O_RDWR); if (-1 == fifofd) { if (ENOENT == errno) fail(1, "%s/.fifo doesn't exist", snroot); else fail(2, "Can't open %s/.fifo:%m", snroot); } /* drain the fifo */ if (-1 == (n = fcntl(fifofd, F_GETFL)) || -1 == (n = fcntl(fifofd, F_SETFL, n | O_NONBLOCK))) fail(2, "fcntl(fifo):%m"); { char tmp[10]; while (read(fifofd, tmp, sizeof (tmp)) > 0) ; } fcntl(fifofd, F_SETFL, n); fcntl(fifofd, F_SETFD, 1); init(); if (-1 == set_path_var()) fail(2, "No memory"); /* * Alternative: leave sockets open, let server time us out. * Not very neighbourly... */ optnocache = TRUE; for (mark = 0;; mark++) { struct timeval tv; int max; if (jobs_not_done()) while (0 == sow()) ; if (sigchld || 1 == mark % 20) { /* Can't avoid signal loss */ sigchld = FALSE; while (0 == reap()) ; } FD_ZERO(&rset); FD_SET(fifofd, &rset); if (throttlerate) { max = throttle_setfds(&rset); if (max < fifofd) max = fifofd; if (sigusr) { sigusr = FALSE; LOG("throttling at %d bytes/sec", throttlerate); } } else max = fifofd; tv.tv_sec = 5; tv.tv_usec = 0; if (select(max + 1, &rset, NULL, NULL, &tv) > -1) { if (throttlerate) throttle(&rset); if (FD_ISSET(fifofd, &rset)) readgroup(); } } _exit(0); } sn-0.3.8/newsgroup.c0000600000175000001440000001435710074071653015015 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Rewritten using 2 hash tables (old one had race problems). * Caller must ensure newsgroup names passed are in all lower case. */ #include #include #include #include #include #include #include #include "config.h" #include #include static const char ver_ctrl_id[] = "$Id: newsgroup.c 49 2004-07-10 22:54:35Z patrik $"; struct ng { struct ng *gnext; struct ng *inext; int ident; char group[1]; }; #define HSIZE 128 static struct ng *bygroup[HSIZE]; static struct ng *byident[HSIZE]; static int highest = 0; static unsigned int hash (char *buf, int len) { unsigned int h; h = 5381; while (len) { --len; h += (h << 5); h ^= (unsigned int) *buf++; } return h; } static struct chunk { struct chunk *next; char *buf; } *chunks; static int avail = 0; static int nr = 0; #undef ALIGN /* FreeBSD apparently defines this */ #define ALIGN sizeof(char *) static int add (int ident, char *group, int len) { struct ng *np; struct ng *p; int want; unsigned h; len++; want = len + sizeof (struct ng) + ALIGN; want -= (len % ALIGN); len--; if (avail < want) { struct chunk *tmp; int size; size = (want > 240 ? want + 240 : 240); if (!(tmp = malloc(size))) return -1; tmp->next = chunks; chunks = tmp; chunks->buf = (char *) chunks + sizeof (struct chunk); avail = size - sizeof (struct chunk); } np = (struct ng *) chunks->buf; chunks->buf += want; avail -= want; strncpy(np->group, group, len); np->group[len] = '\0'; np->ident = ident; h = hash(group, len) % HSIZE; np->gnext = bygroup[h]; bygroup[h] = np; for (p = np->gnext; p; p = p->gnext) if (0 == strcmp(p->group, group)) return 1; h = ident % HSIZE; np->inext = byident[h]; byident[h] = np; for (p = np->inext; p; p = p->inext) if (p->ident == ident) return 1; if (ident > highest) highest = ident; nr++; return 0; } extern char dh_groupfile[]; static int groupfd = -1; static int oldsize = 0; static int mapsize = 0; static char *mapbuf = NULL; void ng_fin (void) { struct chunk *tmp; if (groupfd > -1) { close(groupfd); groupfd = -1; } if (chunks) do { tmp = chunks->next; free(chunks); } while ((chunks = tmp)); if (mapbuf) { munmap(mapbuf, mapsize); mapbuf = NULL; } highest = oldsize = mapsize = 0; } static int reload (void) { struct stat st; static int pagesize = 0; int newsize; /* * 2 things: if file has grown beyond our map, remap the map. * if the file has grown at all, reread the extra. */ if (-1 == fstat(groupfd, &st)) { LOG("reload:fstat:%m"); return -1; } newsize = st.st_size; if (!pagesize) pagesize = getpagesize(); if (newsize <= oldsize) return 0; if (newsize > mapsize || !mapsize) { if (mapbuf) munmap((caddr_t) mapbuf, mapsize); mapsize = st.st_size + pagesize - (st.st_size % pagesize); mapbuf = mmap(0, mapsize, PROT_READ, MAP_SHARED, groupfd, 0); if (!mapbuf || mapbuf == MAP_FAILED) { LOG("reload:mmap:%m"); return -1; } } /* XXX Could stand some error checking, but then how to handle? */ { char *p; char *lim; char *ip; char *gr; int ident, state; lim = mapbuf + newsize; ident = state = 0; ip = gr = 0; /* dumb compiler */ for (p = mapbuf + oldsize; p < lim; p++) switch (state) { case 0: if (*p >= '0' && *p <= '9') { ip = p; state++; } break; case 1: if (' ' == *p) { ident = atoi(ip); state++; } break; case 2: if (' ' != *p) { gr = p; state++; } break; case 3: if ('\n' != *p) break; if (-1 == add(ident, gr, p - gr)) { LOG("reload:no memory"); return -1; } state = 0; } } oldsize = newsize; return 0; } int ng_init (void) { if (-1 == (groupfd = open(dh_groupfile, O_RDWR | O_CREAT, 0644))) if (-1 == (groupfd = open(dh_groupfile, O_RDONLY))) { LOG("ng_init:open(%s):%m", dh_groupfile); return -1; } if (-1 == reload()) { close(groupfd); return (groupfd = -1); } return nr; } int ng_ident (char *group) { struct ng *np; for (np = bygroup[hash(group, strlen(group)) % HSIZE]; np; np = np->gnext) if (0 == strcmp(group, np->group)) return np->ident; return -1; } char *ng_newsgroup (int ident) { struct ng *np; for (np = byident[ident % HSIZE]; np; np = np->inext) if (ident == np->ident) return np->group; return NULL; } /* * Caller must ensure group is all lower case, and not too long. */ int ng_addgroup (char *group) { int ident; char *buf; if (-1 == lockf(groupfd, F_LOCK, 0)) { LOG("ng_addgroup:lockf(%s):%m", dh_groupfile); return -1; } ident = -1; if (0 == reload()) if (-1 == (ident = ng_ident(group))) { int len; len = strlen(group); if ((buf = malloc(len + 15))) { int c, seek; c = formats(buf, len + 14, "%u %s\n", highest + 1, group); if ((seek = lseek(groupfd, 0, SEEK_END)) > -1) if (c == write(groupfd, buf, c)) { if (0 == add(highest + 1, group, len)) ident = highest; /* add() incs */ else if (-1 == ftruncate(groupfd, seek)) LOG("ng_addgroup:ftruncate:%m"); else LOG("ng_addgroup:write fail for %d %s:%m?", highest + 1, group); } free(buf); } else LOG("ng_addgroup:malloc for %s:%m", group); } lseek(groupfd, 0, SEEK_SET); lockf(groupfd, F_ULOCK, 0); return ident; } sn-0.3.8/CHANGES0000600000175000001440000001660710106142506013603 0ustar patrikusers00000000000000-- 0.3.8 Tue Aug 10 15:15:34 EEST 2004 SNHELLO: CR chars had accidentally become LFs, fixed. sndumpdb: Print decimal numbers instead of x/100 fractions. Made the header field name check more lenient in order to avoid unnecessary warnings. snmail: Convert In-Reply-To: to References: only if the message has no References:. snmail: Use last <> enclosed string in In-Reply-To: as the message-id. snmail: removed Message-ID checking and generation - this is handled by snstore. Replaced Message-ID checking with looser implementation based on RFC 2822. -- 0.3.7 Mon Apr 26 01:11:42 EEST 2004 Added contrib directory for contributed tools/examples, and populated it with 2 authentication implementations + an auto subscribe script. Code changes to improve readability: use booleans instead of integers where appropriate, and NULL instead of 0 for pointers. Renamed log() -> log_() to avoid annoying warning in gcc 3.3. Fixed Segfault on PPC Linux systems. (Thanks to Chris Niekel) Renamed lockf to snlockf, POST to SNPOST and HELLO to SNHELLO in order to prevent conflicts with executables from other packages. -- 0.3.6 Thu Jan 16 18:07:03 EET 2003 Fixed XOVER for the case where a header is present but empty. Reorganized the Makefile a bit to make it simpler: added PREFIX variable. snsend: Fixed rnews batch reading for zero-length articles. -- 0.3.5 Sat Jul 6 18:37:50 EEST 2002 Added an FAQ, containg the solution to the infamous POST problem. NEWGROUPS and NEWNEWS now works properly during DST. (Was offset one hour.) Made "sncat -i foo" work for users with read-only access to the spool. Expiration ages can now be specified in years, e.g. "2y" = 2 years. Made snexpire use a month of 30.415 days instead of 28. With the old code, "12m" actually meant about 11 months. Fixed snget's -m option for the case where .serial can't be read. Made the XHDR command work for Xref headers. Removed the ugly "Terminated" messages from HELLO when using bash 2. -- 0.3.4a Tue Nov 13 00:17:20 EET 2001 Made HELLO work with bash version 2. Fixed a bug in request-newgroups. -- 0.3.4 Sun Nov 11 19:08:55 EET 2001 Reformatted the source with GNU indent to make it easier to read (for me). Made the request-list and request-newgroups features work again. snstore fixes: don't exit if =junk doesn't exist, and strip trailing spaces from newsgroup names "dup/present" were printed the wrong way around in snfetch. Bug in Message-ID checking disallowed . Made the "Can't write in spool" error a bit more informative. snget's -m option didn't work. Improved portability. (The biggest change was to replace all calls to flock() with lockf().) sn should now work on most UNIXish systems, as long as the GNU tools (gmake, gcc, bash, etc.) are installed. -- 0.3.3 Thu Nov 23 17:50:12 EET 2000 Fixed a bug related to article ranges outside of the available articles. Added new Makefile variable: DEFAULT_ADMIN_EMAIL New maintainer: Patrik Rådman takes over. snexpire bug related to =junk. writefifo has been failing for a few revs now! Fixed. -- 0.3.2 Fri Mar 3 22:33:02 SGT 2000 Changed ctime to mtime, when checking .created. Date conversion error, e.g. for NEWGROUPS and NEWNEWS. Doc error, command line example using tcpserver. Bug in command parsing causes slrn grief More log levels tweaked Format bugs in snsend. -- 0.3.1 Fri Oct 29 11:24:12 SGT 1999 Added -v option to snexpire. Bad logic when checking to map in newsgroup.c. snmail now silently discards leading From_ line if present. Empty article kludge^H^H^H^H^H^Hfix HELLO auth kludge post.c truncates lines from POST, fixed PATH made to include BINDIR in snntpd.c Log levels tweaked in snfetch, was too noisy -- 0.3.0 Tue Aug 10 12:01:14 SGT 1999 Bug in times.c causes expiration to fail on empty newsgroup. Bug in head.c causes rnews input to fail. Newsgroup names checking relaxed and centralized. Optional "=junk" rejected articles newsgroup. Bug in times expiration not properly deleting article entry times. sncancel new program. Options for snscan, sncat, sncancel merged and extended. Exec .outgoing directly if executable, to permit binaries. snmail no longer checks for a valid newsgroup, since snstore will want the option of storing the article under =junk. parameters.c now checks euid/egid instead of uid/gid. NEWGROUPS command misinterpreted date. INSTALL.notes error, missing snprimedb -i command for reconstructing database. Another race in snstore, when creating a new article file. snexpire now checks to make sure id being deleted is the right one. snfetch now checks for duplicate ID's within the same session. snsend new program supercedes snstore. snstore -n now writes to fd 1 not 2. snstore no longer checks for local/global group. It wasn't used anyway. Removed throttle and sn-words. -- 0.2.5 Sat Mar 27 13:07:05 SGT 1999 Rare race in newsgroup.c fixed. Message ID leakage in database. -- 0.2.4 Wed Feb 24 21:09:52 SGT 1999 (private) sngetd new program. snnewgroup, sndelgroup now C programs. snntpd LIST command output format error (again) snexpire doc error, change. Compression compile-time optional, rewrite, reorg. snmail understands In-Reply-To. snmail no longer checks /.nopost, not its job. snmail optionally runs snstore. snlogin understands new reply codes; code reorg. snstore can save skipped article to stderr. Programs no longer installed setuid. .nopost check error, was not functioning correctly. Divide-by-0 in sndumpdb Format for logging pid changed in snfetch. snexpire logic altered to not leave garbage if 0 expire age. snget (and others) takes -P, passes to snfetch. 0-length article bodies now legal in snntpd and snscan. -- 0.2.3 Mon Dec 7 12:44:03 SGT 1998 Option -m for snget to limit initial articles. snpost locking now performed. snmail EOL conversion error; rewrite. snntpd LIST command output format error. snfetch rnews format error. -- 0.2.2 Thu Oct 8 11:02:54 SGT 1998 -r option in snstore, snfetch, sncat. Rewrote snget as binary instead of script. Merged dh_dump() into sndumpdb.c, it didn't belong in dhash.c. Split dhash.c into readonly portion and readwrite. Rewrote snfetch: option to use pipelining. New IO lib with decent input line buffering, own dir, atomic logging. Env vars to allow separate users to own a different news spool. Incorrect interpretation of Xref: line fixed. System #includes. Scripts changed due to new/different bash behaviour. Bug in NEXT/LAST, not incrementing/decrementing. Bug in snstore in handling 8-bit data in head of article. Decent option processing, man pages updated. snlogin AUTHINFO sadness. Bug in do_post(), prevents posting to local group. Better snpost reporting. Off-by-one in art.c, resulting in random crashes. -- 0.2.1 Wed Aug 5 18:29:37 SGT 1998 Missing realloc in subscribed.c, reorg in newsgroup.c. -- 0.2.0 Sat Jul 11 13:34:03 SGT 1998 Replaced stdio calls. Added timeout. Mailing list support. Removed xover option in snfetch. Dubious value. Various bugs. -- 0.1.9 Mon Jun 29 23:05:07 SGT 1998 Mods for use with Netscape News, thanks Marko. XPAT command added. Added snmail. -- 0.1.8 Fri Jun 12 01:34:52 SGT 1998 Minor changes. I ought to just make a diff. Aw, hell. #include problem in snscan.c; bash problem workaround in snget. -- Bad data sync on if more than one process accesses the ID database. Option to snprimedb, -i. Wrong info in snstore.8, which said a newsgroup argument was needed. In fact there is no such argument. Wrong info in sn.8, in description of leafnode. -- 0.1.0 Sun May 17 16:38:43 SGT 1998 Private release. Thanks rth, janl, mlo. sn-0.3.8/art.c0000600000175000001440000002306410042571016013536 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * File and article module. The entry points in art_gimme() and * art_gimmenoderef() which return the specified article. */ #include #include #include #include #include #include #include #include #include "config.h" #include "art.h" #include "artfile.h" #include "cache.h" #include #include #include static const char ver_ctrl_id[] = "$Id: art.c 29 2004-04-24 23:02:38Z patrik $"; struct fileobj *file_map (char *path) { static size_t pagesize = 0; struct fileobj *fp; struct stat st; int fd; int len; if (!pagesize) pagesize = getpagesize(); fd = open(path, O_RDONLY); if (fd > -1) { if (fstat(fd, &st) > -1) { len = strlen(path); if ((fp = malloc(sizeof (struct fileobj) + len + 1))) { fp->size = ((st.st_size / pagesize) + 1) * pagesize; /* oversize */ fp->map = mmap(0, fp->size, PROT_READ, MAP_SHARED, fd, 0); if (fp->map && fp->map != MAP_FAILED) { if (FILE_MAGIC == *(int *) fp->map) { close(fd); fp->path = strcpy((char *) fp + sizeof (struct fileobj), path); return fp; } else LOG("file_map:%s has bad magic", path); } else LOG("file_map:mmap:%m"); munmap(fp->map, fp->size); } else LOG("file_map:malloc:%m"); free(fp); } else LOG("file_map:fstat:%m"); close(fd); } else if (ENOENT != errno) LOG("file_map:open(%s):%m", path); return NULL; } static void file_unmap (void *p) { struct fileobj *fp = (struct fileobj *) p; munmap(fp->map, fp->size); free(fp); } static int file_cmp (void *a, void *b) { struct fileobj *x = (struct fileobj *) a; struct fileobj *y = (struct fileobj *) b; return strcmp(x->path, y->path); } static int desc = -1; static int file_init (void) { desc = cache_init(8, file_cmp, file_unmap, NULL); if (-1 == desc) { LOG("file_init:%m"); return -1; } return 0; } static struct file *file_gimme (char *name, int *size) { struct fileobj f = { 0, }; struct fileobj *fp; f.path = name; if (NULL == (fp = cache_find(desc, &f))) { if (NULL == (fp = file_map(name))) return NULL; else cache_insert(desc, fp); } if (size) *size = fp->size; return ((struct file *) fp->map); } /************************************************************* Return an article. Can only request by newsgroup and serial. To request by id, first use dh_*() to get the newsgroup and serial number. Should dereference aliased articles also. *************************************************************/ static bool file_initialized = FALSE; int art_gimmenoderef (char *group, int serial, struct article *ap) { struct file *f; struct info info; char path[GROUPNAMELEN + 32]; int filenumber; int part; int filesize, lim; if (serial < 0) return -1; if (strlen(group) >= GROUPNAMELEN) return -1; if (!file_initialized) { if (-1 == file_init()) return -1; else file_initialized = TRUE; } filenumber = serial / ARTSPERFILE; part = serial % ARTSPERFILE; formats(path, sizeof (path) - 1, "%s/%d", group, filenumber); /* Check for extended file, thanks ZB. */ do { if (!(f = file_gimme(path, &filesize))) return -1; lim = f->info[part].boffset + f->info[part].blen; if (lim <= 0) { errno = ENOENT; return -1; } if (lim <= filesize) break; { struct fileobj fo; fo.path = path; cache_invalidate(desc, &fo); } if (!(f = file_gimme(path, &filesize))) return -1; lim = f->info[part].boffset + f->info[part].blen; if (lim <= 0) { errno = ENOENT; return -1; } if (lim <= filesize) break; LOG("art_gimmenoderef:body extends beyond file in %s:%d", group, serial); return -1; } while (0); /* Check for corruption. */ info = f->info[part]; if (info.hoffset <= 0 || info.boffset <= 0) { if (0 == info.hoffset) { /* unused */ if (info.boffset) LOG("art_gimmenoderef:corrupted index in %s:%d", group, serial); } else if (info.hoffset < 0) /* cancelled */ if (info.boffset >= 0) LOG("art_gimmenoderef:bad cancel in %s:%d", group, serial); return -1; } if (info.blen < 2 || info.hlen <= 11) { LOG("art_gimmenoderef:Corrupted index in %s:%d", group, serial); return -1; } ap->head = (char *) f + info.hoffset + 1; ap->hlen = info.hlen - 2; if (ap->head[-1] /* || ap->head[ap->hlen] */ ) { LOG("art_gimmenoderef:Bad header in %s:%d", group, serial); return -1; } ap->body = (char *) f + f->info[part].boffset + 1; ap->blen = f->info[part].blen - 2; return 0; } int art_gimme (char *group, int serial, struct article *ap) { int deref; char *g; int s; g = group; s = serial; for (deref = 0; ; deref++) { static char ngroup[GROUPNAMELEN + 32]; char *p; char *q; char *lim; int c; if (deref >= 10) { LOG1("Too many aliases under %s:%d", g, s); return -1; } if (-1 == art_gimmenoderef(group, serial, ap)) return -1; if (-1 == ap->blen) return -1; /* canceled */ /* * If its an alias, the body will be empty and the * head will contain only one field: * Message-ID: news.group:serial\r\n\0 * We ignore the messageid part. */ if (ap->blen > 0 && strncmp(ap->head, "Message-ID:", 11)) return 0; /* found */ /* is alias */ for (p = ap->head + 11; ' ' == *p; p++) ; lim = ngroup + sizeof (ngroup); for (q = ngroup; (*q = *p) != ':' && q < lim; q++, p++) ; if (':' != *p++) break; *q = '\0'; for (serial = 0;; serial *= 10, serial += c, p++) { c = *p - '0'; if (c > 9 || c < 0) break; } if ('<' != *p) break; group = ngroup; /* serial already set */ } LOG("art_gimme:bad alias under %s:%d", g, s); return -1; } void art_filecachestat (int *hit, int *miss) { cache_stat(desc, hit, miss); } static int getfield (char *buf, struct field *f) { register char *cp = buf; cp += strspn(cp, " \t\f"); /* I don't think \r\n is needed here, since sn unfolds all headers */ if (!*cp) { f->pointer = NULL; return 0; } f->len = strcspn(cp, "\r\n"); if (!f->len) return 0; f->pointer = cp; return ((cp + f->len) - buf); } /* * The "xref" field is not filled. Been removed! --harold */ #define CMP(string) strncasecmp(cp, string, sizeof(string) - 1) int art_makexover (struct article *ap, struct xover *xp) { struct xover x = { {0,}, }; char *cp; if (NULL == ap || NULL == ap->head || NULL == xp) return -1; cp = ap->head; goto start; while (*cp) { if ('\n' == *cp && cp[1]) { cp++; start: switch (*cp) { case 's': case 'S': if (!x.subject.pointer && 0 == CMP("subject:")) cp += getfield(cp + sizeof ("subject:") - 1, &x.subject); break; case 'f': case 'F': if (!x.from.pointer && 0 == CMP("from:")) cp += getfield(cp + sizeof ("from:") - 1, &x.from); break; case 'd': case 'D': if (!x.date.pointer && 0 == CMP("date:")) cp += getfield(cp + sizeof ("date:") - 1, &x.date); break; case 'm': case 'M': if (!x.messageid.pointer && 0 == CMP("message-id:")) cp += getfield(cp + sizeof ("message-id:") - 1, &x.messageid); break; case 'r': case 'R': if (!x.references.pointer && 0 == CMP("references:")) cp += getfield(cp + sizeof ("references:") - 1, &x.references); break; case 'b': case 'B': if (!x.bytes.pointer && 0 == CMP("bytes:")) cp += getfield(cp + sizeof ("bytes:") - 1, &x.bytes); break; case 'l': case 'L': if (!x.lines.pointer && 0 == CMP("lines:")) cp += getfield(cp + sizeof ("lines:") - 1, &x.lines); break; default: ; } } cp++; } *xp = x; return 0; } char *art_findfield (char *head, char *fieldname) { int len; char *cp = head; static struct b b = { 0, }; char c; len = strlen(fieldname); b.used = 0; c = *fieldname & (~040); goto enter; for (; *cp; cp++) { if ('\n' != *cp) continue; cp++; enter: if ((*cp & (~040)) == c && 0 == strncasecmp(cp, fieldname, len)) { char *start = cp + len; if (':' != *start) continue; start++; start += strspn(start, " \t\r\n\f"); for (cp = start; *cp && '\r' != *cp; cp++) ; b_appendl(&b, start, cp - start); return b.buf; } } return ""; } sn-0.3.8/snstore.8.in0000600000175000001440000000002207564256733015011 0ustar patrikusers00000000000000.so man8/snsend.8 sn-0.3.8/newsgroup.h0000600000175000001440000000130007564256733015020 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef NEWSGROUP_H #define NEWSGROUP_H /* int functions return -1 on error. char * functions return NULL on error. */ /* Free internal data structures */ extern void ng_fin (void); /* Initialize internal data structures */ extern int ng_init (void); /* Get newsgroupname given its ident */ extern char *ng_newsgroup (int ident); /* Get groups ident from its name */ extern int ng_ident (char *newsgroup); /* Add a new group, returns its new ident */ extern int ng_addgroup (char *newsgroup); #endif sn-0.3.8/art.h0000600000175000001440000000273507564256733013572 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef FILE_H #define FILE_H /* These functions return values that may disappear when control leaves the calling function, because caching has no garbage collection. So save the results if you want it to be permanent. */ struct article { char *head; int hlen; char *body; int blen; }; /* Simple check to ensure body of article is sane */ #define art_bodyiscorrupt(body,len) (body[-1] || body[len]) /* How to identify that an article's body has been compressed */ #define COMPRESS_MAGIC "c\03c\03" #define COMPRESS_MAGIC_LEN (sizeof(COMPRESS_MAGIC)-1) /* Returns 0 on success and *ap is filled with constant buffers. Returns -1 on error. Fields not found are left empty. */ extern int art_gimme (char *group, int serial, struct article *ap); extern int art_gimmenoderef (char *group, int serial, struct article *ap); struct field { char *pointer; int len; }; struct xover { struct field subject; struct field from; struct field date; struct field messageid; struct field references; struct field bytes; struct field lines; #if 0 struct field xref; #endif }; extern int art_makexover (struct article *ap, struct xover *xp); extern char *art_findfield (char *head, char *fieldname); extern void art_filecachestat (int *hit, int *miss); #endif sn-0.3.8/FAQ0000600000175000001440000000032707746022423013145 0ustar patrikusers00000000000000This file intentionally left blank :) ------------------------------------------------------------------------------- If your question wasn't answered above, ask on the mailing list. (See README for instructions.) sn-0.3.8/dh_find.c0000600000175000001440000001337210101504330014333 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Read-only portion of disk hashing. See dhash.c */ #include #include #include #include #include #include #include "config.h" #include "dhash.h" #include "allocate.h" #include "newsgroup.h" #include #include static const char ver_ctrl_id[] = "$Id: dh_find.c 56 2004-07-27 16:54:48Z patrik $"; int dh_fd = -1; char dh_tablefile[sizeof (DH_TABLEFILE) + 32]; char dh_chainfile[sizeof (DH_TABLEFILE) + 32]; char dh_groupfile[sizeof (DH_GROUPFILE) + 32]; char dh_tmp[sizeof (DH_GROUPFILE) + 32]; bool dh_isreadonly = FALSE; struct table *dh_table = NULL; /* General string hashing function, taken from djb */ unsigned dhhash (char *buf) { unsigned h; int len = strlen(buf); h = 5381; while (len) { --len; h += (h << 5); h ^= (unsigned) *buf++; } return (h % DH_SIZE); } /* * TODO: Maybe use a different kind of lock? Since most of the time * we expect to gain the lock, it might make sense to use a flag in * the map. But then we would for sure be faulting the first page, * even for readonly access. */ int dhlock (void) { int i; for (i = 100; i--;) { if (lockf(dh_fd, F_TLOCK, 0) == 0) return 0; if (errno != EAGAIN) return -1; nap(0, 500); } return -1; } int dhunlock (void) { lseek(dh_fd, 0, SEEK_SET); lockf(dh_fd, F_ULOCK, 0); return 0; } struct chain *dhlocatechain (char *messageid) { unsigned index; unsigned char *x; unsigned h; struct chain *chp; h = dhhash(messageid); x = dh_table->next + h * 3; index = char3toint(x); if (index == 0) return NULL; chp = allo_deref(index); if (chp == NULL) { LOG("dhlocatechain:bad allo deref"); } return chp; } static int initfile (void) { int fd; int integer = DH_MAGIC; int i; char foo[3] = { '\0', }; fd = open(dh_tablefile, O_RDWR | O_CREAT | O_EXCL, 0644); if (fd == -1) return -1; if (lockf(fd, F_TLOCK, 0) == -1) { do { /* wait until other process has initialized it */ if (errno == EAGAIN) nap(0, 200); else goto fail; } while (lockf(fd, F_TLOCK, 0) == -1); lockf(fd, F_ULOCK, 0); return fd; } if (write(fd, &integer, sizeof (int)) != sizeof (int)) goto fail; for (i = 0; i < DH_SIZE; i++) if (write(fd, foo, sizeof (foo)) != sizeof (foo)) goto fail; lseek(fd, 0, SEEK_SET); lockf(fd, F_ULOCK, 0); return fd; fail: if (fd > -1) close(fd); return -1; } int dh_find (struct data *dp, bool readonly) { struct chain *chp; unsigned char *x; if (!readonly) /* lockf() fails on read-only file, and locking not needed anyway for R/O? */ if (dhlock() == -1) return -1; chp = dhlocatechain(dp->messageid); if (chp == NULL) goto fail; while (strcmp(dp->messageid, chp->messageid) != 0) { unsigned index = chp->next; if (index == 0) goto fail; chp = allo_deref(index); if (chp == NULL) { LOG2("dh_find:bad allo deref"); goto fail; } } dp->serial = chp->serial; x = (unsigned char *) chp->newsgroup; dp->newsgroup = ng_newsgroup(char2toint(x)); if (dp->newsgroup == NULL) goto fail; if (!readonly) dhunlock(); return 0; fail: if (!readonly) dhunlock(); return -1; } int dh_open (char *pre, bool readonly) { struct stat st; int prot; int openflag; if (pre && *pre) { if (strlen(pre) > 27) { LOG("SNROOT too long"); return -1; } strcpy(dh_tablefile, pre); strcat(dh_tablefile, DH_TABLEFILE); strcpy(dh_chainfile, pre); strcat(dh_chainfile, DH_CHAINFILE); strcpy(dh_groupfile, pre); strcat(dh_groupfile, DH_GROUPFILE); } else { strcpy(dh_tablefile, DH_TABLEFILE); strcpy(dh_chainfile, DH_CHAINFILE); strcpy(dh_groupfile, DH_GROUPFILE); } prot = PROT_READ; if (!(dh_isreadonly = !!readonly)) { openflag = O_RDWR; prot |= PROT_WRITE; } else openflag = O_RDONLY; if ((dh_fd = open(dh_tablefile, openflag)) == -1) { if (errno == ENOENT) { if ((dh_fd = initfile()) == -1) { LOG("dh_open:Can't create %s: %m", dh_tablefile); return -1; } } else { LOG("dh_open:Can't open %s: %m", dh_tablefile); return -1; } } if (ng_init() == -1) goto fail; dh_table = (struct table *) mmap(0, sizeof (struct table), prot, MAP_SHARED, dh_fd, 0); if (dh_table == NULL || dh_table == MAP_FAILED) { LOG("dh_open:mmap:%m?"); dh_table = NULL; } else if (dh_table->magic != DH_MAGIC) LOG("dh_open:Bad magic"); else if (fstat(dh_fd, &st) == -1) LOG("dh_open:fstat:%m"); else if (st.st_size != sizeof (struct table)) LOG("dh_open:table has wrong size!"); else if (allo_init(dh_chainfile, openflag) == -1 && allo_init(dh_chainfile, openflag | O_CREAT) == -1) { LOG("dh_open:allo_init(%s):%m?", dh_chainfile); } else return 0; if (dh_table) { munmap(dh_table, sizeof (struct table)); dh_table = 0; } ng_fin(); fail: close(dh_fd); dh_fd = -1; return dh_fd; } int dh_close (void) { if (dh_fd > -1) { close(dh_fd); dh_fd = -1; } if (dh_table != NULL) { munmap((caddr_t) dh_table, sizeof (struct table)); dh_table = NULL; } allo_destroy(); ng_fin(); return 0; } sn-0.3.8/INSTALL.notes20000600000175000001440000000075007564256733015070 0ustar patrikusers00000000000000Some thoughts that haven't been tested, but should be workable. Why include it here? Because if you do this, I'd like to hear about it even if you have no problems. Aliasing of newsgroup names. If group.alias is a symlink to group.real, which is a newsgroup directory, then all functions that apply to group.real (posting, fetching, etc.) apply equally to group.alias as group.real. EXCEPT in reading articles, when the newsgroup name is group.alias, whereas it should be group.real. sn-0.3.8/group.c0000600000175000001440000001403610042571016014103 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Information on how many articles are in a group. */ #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "cache.h" #include "group.h" #undef VOLATILE #include "artfile.h" #include #include static const char ver_ctrl_id[] = "$Id: group.c 29 2004-04-24 23:02:38Z patrik $"; #ifdef DONT_HAVE_DIRFD #define dirfd(dirp) ((dirp)->dd_fd) #endif static int desc = -1; struct dir { DIR *dir; int first; int last; time_t read; }; struct lastf { int name; struct file *f; int slotsfilled; }; struct groupobj { char *groupname; struct dir dir; struct lastf lastf; bool nopost; }; static int cmpgroup (void *a, void *b) { struct groupobj *x = (struct groupobj *) a; struct groupobj *y = (struct groupobj *) b; return strcmp(x->groupname, y->groupname); } static void freegroup (void *p) { struct groupobj *gop = (struct groupobj *) p; if (gop->groupname) free(gop->groupname); if (gop->dir.dir) closedir(gop->dir.dir); if (gop->lastf.f) munmap((caddr_t) gop->lastf.f, sizeof (struct file)); free(p); } int group_init (void) { if (-1 != desc) return 0; /* already initialized */ desc = cache_init(4, cmpgroup, freegroup, NULL); if (-1 == desc) return -1; return 0; } /* * Do we need to re-read the directory? */ static int refresh (struct groupobj *gop, char *groupname) { struct stat st; struct dir *d; struct lastf *lp; bool needreread = FALSE; bool needrefile = FALSE; bool needreslot = FALSE; bool isemptygroup = FALSE; d = &gop->dir; lp = &gop->lastf; if (groupname) { memset(gop, 0, sizeof (struct groupobj)); gop->groupname = strdup(groupname); if (NULL == gop->groupname) { LOG("refresh:strdup:%m"); return -1; } gop->dir.first = -1; d->dir = opendir(groupname); if (NULL == d->dir) { LOG("refresh:opendir:%m"); return -1; } lp->name = -1; needrefile = needreread = needreslot = TRUE; } else { if (-1 == d->first || 0 == d->last || !d->read) needreread = TRUE; else if (-1 == fstat(dirfd(d->dir), &st)) { LOG("refresh:fstat:%m"); return -1; } else if (d->read < st.st_mtime) needreread = TRUE; if (!lp->f) needrefile = needreslot = TRUE; if (!needrefile) if (!lp->f) needrefile = TRUE; if (!needrefile) if (-1 == lp->name) needrefile = TRUE; } if (needreread || needreslot) { struct dirent *dp; int i, first, last; char *end; first = -1; last = 0; rewinddir(d->dir); dp = readdir(d->dir); if (NULL == dp) { LOG("refresh:readdir:%m"); return -1; } gop->nopost = FALSE; do { if (!isdigit(*dp->d_name)) { if (!gop->nopost && '.' == *dp->d_name) if (0 == strcmp(dp->d_name, ".nopost")) gop->nopost = TRUE; continue; } if ((i = strtoul(dp->d_name, &end, 10)) <= 0 || *end) continue; if (first > i || -1 == first) first = i; if (last < i) last = i; } while ((dp = readdir(d->dir))); d->first = first; d->last = last; if (-1 == first && 0 == last) isemptygroup = TRUE; if (last != lp->name) needrefile = TRUE; else lp->name = last; time(&d->read); } if (needrefile || needreslot) { int fd; if (lp->f) { munmap((caddr_t) lp->f, sizeof (struct file)); lp->f = NULL; } if (isemptygroup) return 0; fd = openf(0, O_RDONLY, "%s/%d", gop->groupname, d->last); if (-1 == fd) { LOG("refresh:open:%m"); return -1; } lp->f = (struct file *) mmap(0, sizeof (struct file), PROT_READ, MAP_SHARED, fd, 0); close(fd); if (lp->f == MAP_FAILED) { /* Bugfix: <= 0 fails for large addresses! (High bit set) */ LOG("refresh:mmap:%m"); return -1; } lp->slotsfilled = 0; needreslot = TRUE; } if (needreslot) { struct info *ip = lp->f->info; int slot; for (slot = ARTSPERFILE - 1; slot > -1; slot--) if (ip[slot].hoffset) break; /* pretend cancelled arts are there */ lp->slotsfilled = slot + 1; } return 0; } int group_info (char *newsgroup, struct group *gp) { struct groupobj *gop; { struct groupobj g = { 0, }; g.groupname = newsgroup; gop = cache_find(desc, &g); } if (NULL == gop) { gop = malloc(sizeof (struct groupobj)); if (NULL == gop) { LOG("group_info:malloc:%m"); return -1; } if (-1 == refresh(gop, newsgroup)) { freegroup(gop); LOG("group_info(%s)", newsgroup); return -1; } cache_insert(desc, gop); } else if (-1 == refresh(gop, NULL)) { LOG("group_info(%s)", newsgroup); cache_invalidate(desc, gop); return -1; } gp->nopost = gop->nopost; if (-1 == gop->dir.first) return (gp->first = gp->last = gp->nr_articles = 0); gp->first = gop->dir.first * ARTSPERFILE; gp->last = gop->lastf.slotsfilled + (gop->dir.last * ARTSPERFILE) - 1; gp->nr_articles = gp->last - gp->first + 1; /* ZB was here */ if (ARTSPERFILE == gop->lastf.slotsfilled) { munmap((caddr_t) gop->lastf.f, sizeof (struct file)); gop->lastf.f = NULL; } return 0; } void group_fin (void) { if (desc > -1) cache_fin(desc); desc = -1; } sn-0.3.8/sncat.8.in0000600000175000001440000000002207564256733014424 0ustar patrikusers00000000000000.so man8/snscan.8 sn-0.3.8/group.h0000600000175000001440000000112610042066025014103 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef GROUP_H #define GROUP_H #include "config.h" /* This doesn't do much */ struct group { int nr_articles; int first; int last; bool nopost; }; extern int group_init (void); extern void group_fin (void); extern int group_info (char *groupname, struct group *gp); /* returns the group names available. All buffers returned are malloc()ed. */ extern char **group_all (int *nr); #endif sn-0.3.8/valid.c0000600000175000001440000000333310042571016014044 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Check if a newsgroup name is valid. * Thanks pradman. */ #include #include #include "config.h" #include "parameters.h" static const char ver_ctrl_id[] = "$Id: valid.c 29 2004-04-24 23:02:38Z patrik $"; #define C(x) case x: bool is_valid_name (char *name) { register char *p; if (!*name) return FALSE; for (p = name; *p; p++) switch (*p) { C('-') C('.') if (p == name || *p == p[1] || !p[1]) return FALSE; C('a') C('b') C('c') C('d') C('e') C('f') C('g') C('h') C('i') C('j') C('k') C('l') C('m') C('n') C('o') C('p') C('q') C('r') C('s') C('t') C('u') C('v') C('w') C('x') C('y') C('z') C('A') C('B') C('C') C('D') C('E') C('F') C('G') C('H') C('I') C('J') C('K') C('L') C('M') C('N') C('O') C('P') C('Q') C('R') C('S') C('T') C('U') C('V') C('W') C('X') C('Y') C('Z') C('_') C('+') C('0') C('1') C('2') C('3') C('4') C('5') C('6') C('7') C('8') C('9') continue; default: return FALSE; } if (p - name > GROUPNAMELEN) return FALSE; return TRUE; } bool is_valid_group (char *name) { char path[GROUPNAMELEN + sizeof ("/.created")]; struct stat st; if (!is_valid_name(name)) return FALSE; strcat(strcpy(path, name), "/.created"); if (0 == stat(path, &st)) if (S_ISREG(st.st_mode)) if (0 == stat(name, &st)) if (snuid == st.st_uid) if (sngid == st.st_gid) return TRUE; return FALSE; } sn-0.3.8/snget.8.in0000600000175000001440000000747107746022423014440 0ustar patrikusers00000000000000.TH snget,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snget,sngetd \- fetch news from upstream sites and store them locally. .SH SYNOPSIS .B snget .RI [ options ] .RI [ news.group ...] .br .B sngetd .RI [ options ] .br .IR options : .RI [- d ] .RI [- h\ Bps ] .RI [- p\ nparallel ] .RI [- c\ depth ] .RI [- m\ max ] .SH DESCRIPTION .B snget fetches articles for the .IR news.group s on the command line (or all non-local newsgroups if none are specified) from their respective upstream feeds, and stores them in the news spool !!SNROOT!!. If a newsgroup could not be fetched for, it may be attempted again. .B sngetd does the same, except it reads newsgroup names from .RB !!SNROOT!!/ .fifo and does not exit. .B sngetd does not fork into the background. .B snget does not guarantee to fetch the groups in the order specified on the command line. .B snget and .B sngetd will write errors and status messages to descriptor 2. .B snget and .B sngetd are both scheduling wrappers that call .RB !!BINDIR!!/ SNHELLO , .BR snfetch , and .BR snstore . You must own !!SNROOT!! or be root in order to run .B snget and .BR sngetd . .SH OPTIONS .TP .RI - d Enable verbosity, may be specified multiple times. This option is also propagated to .B snfetch and .BR snstore . .TP .RI - p\ nparallel Attempt to fetch for .I nparallel newsgroups at once. Default is 4, maximum is 8. .TP .RI - h\ Bps Throttle the sum of bandwidth used by all .BR snfetch es to .I Bps bytes per second. This option is used to prevent .B snget or .B sngetd from hogging the network. By default there is no throttling. .TP .RI - c\ depth .I depth is passed to .B snfetch as the depth of the command pipeline. .TP .RI - m\ max The very first time contacting the news server, retrieve no more than .I max articles per newsgroup, default is 200. This option is useful only if there are unprimed newsgroups and has no effect otherwise. The .B .max file in each newsgroup directory still applies (see .BR snfetch (8)). .TP .RI - t\ timeout This option is not documented because it is ignored and will disappear in a future release. See .B .timeout in .B FILES next. .SH FILES .TP .B Server Directories These are the directories .RI !!SNROOT!!/.outgoing/ server.name:port , which are symlinked from .RI !!SNROOT!!/ news.group /.outgoing. If the latter isn't a (symlink to a) directory, .B snget won't fetch for .IR news.group . .TP .RI !!SNROOT!!/.outgoing/ server.name:port /.timeout If this file exists and contains a number, this is taken to be the timeout in seconds in all dealings with .IR server.name:port . Default is 120 seconds. .TP .RI !!SNROOT!!/.outgoing/ server.name:port /.SNHELLO If this program file exists, it is invoked instead of the default (usually .RB !!BINDIR!!/ SNHELLO ) when an NNTP connection is first made to .I server.name:port in order to read the greeting and upload posted articles. If .I server.name:port requires a username and password, you would copy the default here and edit that information in. .TP .RI !!SNROOT!!/ news.group /{.serial,.max} .B snget and .B sngetd read these files on behalf of .BR snfetch . .SH SIGNALS Other signals have default behaviour. .TP .B SIGUSR1 If .RI - h\ Bps was specified, .I Bps is halved, else is ignored. .TP .B SIGUSR2 If .RI - h\ Bps was specified, .I Bps is doubled, else is ignored. .SH ENVIRONMENT VARIABLES See also .RB !!BINDIR!!/ SNHELLO for the list of environment variables exported by .BR snget / sngetd . .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .TP .B PATH To find .B SNHELLO (if this server does not have a .BR .SNHELLO ), .BR snfetch , and .BR snstore . If .B PATH does not contain !!BINDIR!! as one of it's components, !!BINDIR!! is appended to it. .SH SEE ALSO .BR snfetch (8), .BR snstore (8), .RB !!BINDIR!!/ SNHELLO sn-0.3.8/valid.h0000600000175000001440000000046610042066025014054 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef VALID_H #define VALID_H extern bool is_valid_name (char *name); extern bool is_valid_group (char *name); #endif sn-0.3.8/cache.c0000600000175000001440000001227310042571016014013 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Generic caching module. Not very sophisticated, but very heavily * used to avoid repeated object construction/deconstruction. */ #include #include #include #include "cache.h" static const char ver_ctrl_id[] = "$Id: cache.c 29 2004-04-24 23:02:38Z patrik $"; /* Max number of caches (of possibly different types of object) that we can handle */ #ifndef CACHE_MAX #define CACHE_MAX 4 #endif struct entry { void *object; struct entry *prev; struct entry *next; }; struct table { int maxsize; int (*cmp) (void *, void *); void (*freeobj) (void *); int (*isstale) (void *); struct entry *entries; struct entry *freelist; struct entry *freeblocks; int hit; int miss; }; struct table table[CACHE_MAX] = { { 0, }, }; int cache_init (int max, int (*cmp) (void *, void *), void (*freeobj) (void *), int (*isstale) (void *)) { int i; int j; if (max <= 1 || NULL == cmp || NULL == freeobj) { errno = EINVAL; return -1; } for (i = 0; i < CACHE_MAX; i++) if (0 == table[i].maxsize) break; if (CACHE_MAX == i) { errno = ENFILE; return -1; } table[i].freeblocks = calloc(max, sizeof (struct entry)); if (NULL == table[i].freeblocks) return -1; for (j = 0; j < max - 1; j++) table[i].freeblocks[j].next = &table[i].freeblocks[j + 1]; table[i].freelist = table[i].freeblocks; table[i].maxsize = max; table[i].cmp = cmp; table[i].freeobj = freeobj; table[i].isstale = isstale; /* This is permitted to be NULL */ table[i].hit = table[i].miss = 0; return i; } /* Either get a free entry from the freelist, or remove the last of the entries list */ static struct entry *new_entry (int desc) { struct entry *ep; if ((ep = table[desc].freelist)) table[desc].freelist = ep->next; else { for (ep = table[desc].entries; ep->next; ep = ep->next) ; ep->prev->next = NULL; table[desc].freeobj(ep->object); } ep->prev = ep->next = NULL; ep->object = NULL; return ep; } void cache_fin (int desc) { struct entry *ep; struct entry *tmp; void (*freeobj) (void *) = table[desc].freeobj; for (ep = table[desc].entries; ep; ep = tmp) { (*freeobj) (ep->object); tmp = ep->next; } free(table[desc].freeblocks); memset(&table[desc], 0, sizeof (struct table)); } void *cache_find (int desc, void *object) { register struct table *tp = &table[desc]; int (*cmp) (void *, void *) = tp->cmp; int (*isstale) (void *) = tp->isstale; struct entry *head = table[desc].entries; register struct entry *ep; for (ep = head; ep; ep = ep->next) if (isstale && (*isstale) (ep->object)) { if (ep->prev) ep->prev->next = ep->next; else tp->entries = ep->next; if (ep->next) ep->next->prev = ep->prev; ep->next = NULL; (*tp->freeobj) (ep->object); ep->next = tp->freelist; tp->freelist = ep; } else if (0 == (*cmp) (object, ep->object)) { /* Once found, move it to the head of the list */ if (ep != tp->entries) { ep->prev->next = ep->next; if (ep->next) ep->next->prev = ep->prev; tp->entries->prev = ep; ep->next = tp->entries; tp->entries = ep; ep->prev = NULL; } tp->hit++; return tp->entries->object; } tp->miss++; return NULL; } void cache_stat (int desc, int *hit, int *miss) { if (hit) *hit = table[desc].hit; if (miss) *miss = table[desc].miss; } /* Return the first item in the cache. To be used as a hint. Should we be checking for staleness here? */ void *cache_top (int desc) { return table[desc].entries->object; } /* Since new_entry() uses only pre-allocated memory, this function cannot fail. */ void cache_insert (int desc, void *object) { register struct entry *ep; ep = new_entry(desc); ep->object = object; ep->next = table[desc].entries; if (table[desc].entries) table[desc].entries->prev = ep; table[desc].entries = ep; } void cache_invalidate (int desc, void *objp) { struct entry *ep; struct entry *tmp; for (ep = table[desc].entries; ep; ep = tmp) if (0 == (*table[desc].cmp) (objp, ep->object)) { if (ep->next) ep->next->prev = ep->prev; if (ep->prev) ep->prev->next = ep->next; else table[desc].entries = ep->next; (*table[desc].freeobj) (ep->object); tmp = ep->next; ep->object = NULL; ep->prev = NULL; ep->next = table[desc].freelist; table[desc].freelist = ep; break; } else tmp = ep->next; } #ifdef CACHE_DEBUG void cache_dump (int desc, void (*printer) (void *)) { struct entry *ep; for (ep = table[desc].entries; ep; ep = ep->next) (*printer) (ep->object); } #endif /* CACHE_DEBUG */ sn-0.3.8/cache.h0000600000175000001440000000164007564256733014041 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef CACHE_H #define CACHE_H /* * Generic caching module. We will be using this to cache article * files as well as group structures. */ /* cache_init() fails only when args are invalid or if memory fails */ extern int cache_init (int max, int (*cmp) (void *, void *), void (*freeobj) (void *), int (*isstale) (void *)); extern void *cache_find (int desc, void *object); extern void *cache_top (int desc); extern void cache_insert (int desc, void *object); extern void cache_invalidate (int desc, void *object); extern void cache_fin (int desc); extern void cache_stat (int desc, int *hit, int *miss); #ifdef CACHE_DEBUG extern void cache_debug (void (*printer) (void *)); #endif /* CACHE_DEBUG */ #endif sn-0.3.8/sndumpdb.c0000600000175000001440000000661610101465341014567 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Dump database. */ #include #include #include #include #include #include "config.h" #include "allocate.h" #include "newsgroup.h" #include "dhash.h" #include "parameters.h" #include #include #include static const char ver_ctrl_id[] = "$Id: sndumpdb.c 55 2004-07-27 14:46:57Z patrik $"; int debug = 0; int outfd = 1; int dh_dump (int next) { int i; int nr = 0; unsigned index; struct chain *chp; int empties = 0; int total = 0; int maxlen = 0; int bytes = 0; char tmp_str[20]; for (i = 0; i < DH_SIZE; i++) { unsigned char *x = dh_table->next + i * 3; index = char3toint(x); if (!index) empties++; else { int len = 0; for (; index; index = chp->next) { char *group; int ident; chp = allo_deref(index); if (NULL == chp) { LOG("dh_dump:allo_deref returned bad ref"); return (0 - nr); } ident = char2toint(chp->newsgroup); group = ng_newsgroup(ident); if (NULL == group) { LOG("dh_dump:bad newsgroup for ident %d", ident); return (0 - nr); } if (!next) writef(outfd, "%s %d <%s>\n", group, chp->serial, chp->messageid); else writef(outfd, "[%d,%d]%s %d <%s>\n", index, chp->next, group, chp->serial, chp->messageid); nr++; len++; bytes += strlen(chp->messageid); } total += len; if (len > maxlen) maxlen = len; } } LOG("%d chains of %d empty", empties, DH_SIZE); LOG("Total %d entries", total); if (empties < DH_SIZE) { snprintf(tmp_str, 20, "%.2f", ((double) total) / (DH_SIZE - empties)); LOG("for average chain length of %s", tmp_str); } LOG("with max chain length of %d", maxlen); if (total) { snprintf(tmp_str, 20, "%.2f", ((double) bytes) / total); LOG("Average length of ID is %s", tmp_str); } return nr; } #define usage() fail(1, "Usage: %s [-i] [-o file]", progname) int main (int argc, char **argv) { int nr; char *cp; int withnext = 0; int i; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); parameters(FALSE); if (-1 == chdir(snroot)) FAIL(2, "chdir(%s):%m", snroot); while ((i = opt_get(argc, argv, "o")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'd': debug++; break; case 'V': version(); _exit(0); case 'i': withnext = 1; break; case 'o': if (!opt_arg) usage(); outfd = open(opt_arg, O_WRONLY | O_CREAT, 0644); if (-1 == outfd) fail(2, "open(%s):%m", opt_arg); break; default: usage(); } if (opt_ind < argc) usage(); if (-1 == dh_open(NULL, TRUE)) _exit(2); nr = dh_dump(withnext); if (nr < 0) { LOG("Error (%m?) while reading record number %d", 0 - nr); dh_close(); _exit(2); } dh_close(); _exit(0); } sn-0.3.8/sn.8.in0000600000175000001440000001371707564256733013753 0ustar patrikusers00000000000000.TH sn,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME sn \- The sn news system. .SH DESCRIPTION If you think of a news spool as a black box, then .B sn is a bunch of programs to put articles in the box .RB ( snstore ), to view articles in it .RB ( snntpd , .BR sncat ), remove old articles .RB ( snexpire , sncancel ), generate a list of what's in it .RB ( snscan, .BR snprimedb ), or display that list .RB ( sndumpdb ). In addition it can go find the articles to put in the box .RB ( snget ). The .B sn system is designed for small sites, spooling a few dozen to a few hundred newsgroups. It is not meant for sites carrying a full news feed. .B sn is suitable as a replacement for .B leafnode (ftp.troll.no, by Arnt Gulbrandsen). It doesn't have .BR leafnode 's intelligence when it comes to retrieving upstream articles, but is less demanding on the filesystem and takes up less disk space. .B sn supports local news groups. .B sn tries to create .I aliases for crossposts instead of storing the article multiple times. .B sn can compress articles to save disk space. Compressing and uncompressing take place transparently. .B sn doesn't use configuration files. Instead, you write small shell scripts to customize .BR sn 's behaviour. .SH REQUIREMENTS .B sn has been tested only against Linux ver. 2.0. It makes heavy use of mmap(2) in MAP_SHARED and MAP_WRITE mode, so it won't work on pre-1.3.45 or so. I (harold-sn@nb.com.sg) am interested in bug reports, comments, and suggestions. .SH NEWS SPOOL STRUCTURE The news spool is .IR !!SNROOT!! , which all .B sn programs need in order to run. It contains the group directories in .IR news . group . name format rather than the traditional .IR news / group / name . Each such directory contains article files, named serially from .BR 1 , .BR 2 , etc. Each article file consists of up to 10 articles, which reduces the number of files and inodes required. These article files are mmap(2)ed before use, and they are cached to reduce search time. .B sn tries to keep the headers of these articles bunched together, to reduce the memory footprint. In addition to the article files, each newsgroup directory also contains files which control the behaviour of the .B sn system. All control files have filenames beginning with a dot, and they all reside in the group directory in which they are to have effect. In particular, if the group directory does not have a subdirectory .I .outgoing in it, it is considered a local news group. .SH BUGS When retrieving articles, there is a window of time between when an article is first marked as wanted and when it is committed to the news spool. Within this window it is possible to have the same article committed by a separate process. This defeats the aliasing feature so you could end up with more than one copy of an identical article. The ID database will only store one instance of an articles {ID,group,serial} tuple. After the second copy is committed to the spool, .B snstore will try to commit it in the ID database, and will fail. So it is possible that after the article has been expired, another copy will still remain, but be inaccessible if requested by its ID. .B sn doesn't sync the ID database after each write, so it could get corrupted. If you suspect it is corrupted, delete the files .RI !!SNROOT!!/ .table , .RI !!SNROOT!!/ .chain , and .RI !!SNROOT!!/ .newsgroup , making sure .B snntpd and .B snget aren't running or scheduled to do so. Then run .br cd !!SNROOT!!; snscan -n * |snprimedb .br This will recreate the ID database. .SH PROGRAMS .TP .B sncat prints out the specified articles. This is useful if you want to transport the spool to another system. .TP .B sndumpdb dumps the ID database. It is not normally used. .TP .B snexpire expires articles in the spool. .TP .B snfetch is the back end of the article fetcher. You can use .B suck (a separate package) instead. .TP .B snget is the front end of the article fetcher. .TP .B snmail converts a normal email message into a news article. It can be used to turn a mailing list into a local newsgroup. .TP .B snnewgroup is a script to create newsgroup directories. .TP .B sndelgroup is a script to delete newsgroup directories and flush the ID database of references to those articles. .TP .B snntpd is the news server (an nnrpd). .TP .B snprimedb feeds the ID database from formatted standard input. This database is what permits articles to be specified by their ID. This program is not used in normal operation. .TP .B snscan scans the specified articles and outputs a line consisting of the article's ID, newsgroup, and serial number within the newsgroup. This output is suitable for feeding to .BR snprimedb . This program is not used in normal operation. .TP .B snsend takes articles on standard input and distributes them according to the type of newsgroup. .TP .B snstore Like .BR snsend , but considers all newsgroups as local, so stores all articles locally only. .TP .B sncancel cancels articles, so they are no longer available locally. .SH CONVENTIONS .TP .B Exit codes All .B sn programs exit 0 on success, .I 1 on invocation error; .I 2 on system error; .I 3 on format or protocol error. Other exit codes are possible. .TP .B Options .B sn programs take .RI - d to enable verbose messages (may be repeated), and .RI - V , which displays the version and exits. Where a network timeout is appropriate, this is specified with .RI - t\ timeout , in seconds. .RI - P indicates the pid should be included in any status output. Other options are possible. .TP .B Environment .B sn programs will take the contents of the .B SNROOT environment variable as the news spool rather than !!SNROOT!!. If a program will need to run another, it appends !!BINDIR!! to its .B PATH variable if !!BINDIR!! is not already present. .SH SEE ALSO .BR sncat (8), .BR sndumpdb (8), .BR snexpire (8), .BR snfetch (8), .BR snget (8), .BR snmail (8), .BR snnewgroup (8), .BR sndelgroup (8), .BR snntpd (8), .BR snprimedb (8), .BR snscan (8), .BR snstore (8), .BR snsend (8) sn-0.3.8/INSTALL.upgrade0000600000175000001440000001655410106142506015270 0ustar patrikusers000000000000000.3.8: Bugfix release. No new features. -- 0.3.7: POST is now SNPOST and HELLO is SNHELLO. If you have customized scripts you need to make the same name change. (.POST -> .SNPOST and .HELLO -> .SNHELLO) -- 0.3.6: Bugfix release. The Makefile has been slightly simplified with the introduction of a PREFIX variable, which should be set to the parent directory of sn's manual and binary directories. (If you wish to have these directories in separate locations, BINDIR and MANDIR can be set as before.) -- 0.3.5: The way that snexpire calculates the length of a month has changed so that an expire time of "12m" now actually means about 12 months, and not 11. (Now you can also specify years, for example "2y".) -- 0.3.4 fixes bugs and improves portability. No new features. -- 0.3.3: The default e-mail address for the administrator used to be "postmaster", now it's determined by the DEFAULT_ADMIN_EMAIL variable in the Makefile. -- 0.3.2 is a bug fix release over 0.3.1. No incompats. -- 0.3.1 is a bug fix release over 0.3.0. No incompats. -- 0.3.0 has a lot of code level changes, but sn still behaves much the same. Most of these changes have to do with cleaning up the structure by drawing a clearer line between what is sn and what is site policy. This has resulted in a bunch of stuff being moved from the core into editable and overrideable scripts. For those who have been using 0.2.5 in a "normal" way - global and local newsgroups, and mailing lists - you needn't change anything except maybe the NEWSMASTER macro. This macro was used to set the email address of the site admin, now that address is configured in the POST and HELLO scripts. The default is $NEWSMASTER environment variable if set, otherwise $LOGNAME if set, otherwise "postmaster". For other sites that have been using the spool in a novel way, read on. Programs removed: The "throttle" and "sn-words" programs have been removed. You can use those from an old distribution if you need them. snntpd invocation: snntpd now optionally invokes a logger to send log messages to, and the INSTALL files show that option being used. This means the "nntpd" wrapper script is no longer needed, but if you're happy with the existing setup, there is no need to change the invocation. snstore: snstore no longer takes the -1 option. It was a mistake to document it anyway. If you're using a custom storage script, it may be easier to use snsend for your purpose now. The junk newsgroup: If you create a newsgroup "=junk", snstore and snsend (new program) will put articles here that don't belong anywhere else, rather than dropping them. If you want the old behaviour, which is to dump them to descriptor 2 or drop them, don't create "=junk". The junk newsgroup can be expired like any other news group, but it isn't visible under snntpd. Control messages: By default now cancel control messages get mailed to you for action, whereas before, sn didn't understand control messages at all. If you don't like this, copy BINDIR/POST into SNROOT/.POST and edit it to remove the first stanza. snmail checking: Old snmail used to check these things: valid newsgroup name; existence of the newsgroup; presence of the message ID in the database; and that SNROOT/.nopost does not exist. New snmail doesn't. You can still request these checks by specifying snmail -scn (send to [-s]nstore and tell it to also [-c]heck the database but do [-n]ot store it). sncancel (new) and inaccurate snntpd listing: Some news readers complain if the news server's GROUP output lists a certain number of articles, but fewer than those actually exist. This problem will reappear in snntpd if you use sncancel, since snntpd merely checks the lowest and highest articles, and assumes there are no holes in between. I'm not likely to "fix" this, because a news reader can't really expect the article count to not change, even within a session. .outgoing files (local newsgroups): Please check that IF the .outgoing file has the executable bit set, that it also has the "#!" interpreter line. snntpd now runs the file directly (without sh) if it is executable, so that .outgoing can be a binary. Otherwise if the file is not executable, snntpd will still invoke it via sh. In the previous versions, snntpd always invoked .outgoing via sh, and always disregarded the file mode. -- 0.2.5 has some large code changes, and sn's behaviour has changed in a few incompatible ways. Aside from these it is safe to use these binaries on your old spool. Permissions: sn binaries are no longer setuid by default. Instead they get their idea of running permissions from the permissions on SNROOT (or $SNROOT environment variable). This means sn programs no longer check $SNUID and $SNGID environment variables. If you've been using snstore in a custom news spool injector, or have scripts that set $SNUID/$SNGID, you'll want to recheck them. Changes in snmail: If no newsgroup is specified on the command line, snmail now looks at the Return-Path: line before the From: line to get it's idea of the destination newsgroup. If you've been letting snmail figure the newsgroups on its own, check the invocation again or mail might start going into the wrong newsgroups. snpost: snpost now mails failed postings to $LOGNAME if set, then falls back to NEWSMASTER in config.h. Compression: Compression is now compile-time optional. If you opt not to compile compression, you can't use the binaries on a spool with compressed articles. sn won't warn you. -- 0.2.3 is a maintenance release. If you're installing this version on top of a version prior to 0.2.2, the previous snget script has been replaced by an snget binary, and you may want to save the script before "make install". Invocation is backwardly compatible but some options have been added. The throttle program is no longer used, but is retained since it can be useful if you decide to use a different method to obtain news. If you're installing 0.2.3 on top of a version prior to 0.1.7, and if you've used the compression facility, you'll need to dump and restore the news spool because the compression code has changed. See below. If you're installing 0.2.3 on top of 0.1.8 or before and you want to retain your current news spool, you MAY want to dump and restore the spool (as above) IF you have had trouble using sn with Netscape News. If you do so, your articles will be renumbered; whereas before they started at 0, now they'll start at 10. See below. Dumping and restoring a spool ----------------------------- Do this using the OLD version of sn: $ cd SNROOT # the old SNROOT, if it's different from the new one. $ BINDIR/sncat -n * >/tmp/arts or: $ BINDIR/sncat -n * |gzip -c >/tmp/arts.gz You've now got a multi-megabyte file called /tmp/arts (or /tmp/arts.gz) which contains your news spool. Don't forget the "-n". If your SNROOT has changed since the old installation, just delete the old spool (rm -rf SNROOT). Build and install as normal. But if you're keeping the same spool directory, you'll want to clean out the old spool. This retains your options, but removes your old article files and deletes the database (which will be recreated). Build and install as normal, then: $ make spoolclean and type 'y'. Then restore into the new news spool, using the NEW sn binaries: $ BINDIR/snstore . What it is ---------- sn is a small news system for small sites serving perhaps a few dozen newsgroups, and with a slow connection to the internet. It is similar to Leafnode (). The target user is a home or SOHO with a single modem connection to the Internet, maybe running IP masq or similar, and serving a few workstations. sn is different --------------- sn is fast. The news fetcher implements command pipelining and parallel fetches, so high modem latency doesn't cost you. (You can also throttle sn down, if it's too fast). sn is flexible. You can subscribe to newsgroups on several upstream sites, and you can create local newsgroups that are not propagated upstream. sn can make mailing lists look like newsgroups transparently. sn is non-intrusive. It has no configuration files, only control files with simple semantics, so it's suited to being run in an automated fashion. sn is small. It batches 10 (default) articles per file, so conserving inodes and reducing disk block fragments. sn can also compress the article bodies. It aliases cross posts rather than making separate copies. It does not maintain a bulky overview database; instead it gets overview information directly from the articles. Spool metadata is 5% to 10% of the total spool size, even with compression. You don't need to build a separate file system just to spool news. sn doesn't have to be set up for the system. You can use it privately, and your users can maintain their own news spool without building sn. sn has limitations ------------------ Because the news spool is in an unconventional format, news readers can read news locally only via NNTP. IHAVE is not supported. Use POST instead. sn has no idea of distributions. sn can't handle thousands of newsgroups. A hundred is comfortable. The method sn uses to alias cross posts is similar to filesystem symlinks rather than hard links, in that if the original article is expired, the alias breaks. sn doesn't know how to filter spam. Newsgroup names are subject to the same length limitations as any filename. Requirements ------------ sn is beta. It should work on most UNIXish systems, as long as the GNU tools are installed. (Tested on Linux (main development platform), Solaris and FreeBSD.) If it runs on your machine, let me know. Or if it doesn't run. You need zlib () if you decide to compile in the compression feature. You'll need inetd or tcpserver () to run the daemon. If you use inetd you will probably also want tcpd (tcp wrappers). I am interested in bug reports, comments, and suggestions. Installation ------------ Read file INSTALL.upgrade first if you're upgrading from a previous version. Otherwise read file INSTALL, then INSTALL.run, then INSTALL.notes. Mailing list ------------ There is a mailing list (sn@infa.abo.fi) available. If you have questions or comments concerning sn, please join the list and post them there. To subscribe, send "subscribe sn" in the body of an e-mail to majordomo@infa.abo.fi. (Or "subscribe sn me@my-isp.somewhere" to use the specified e-mail address.) License ------- Distribution of sn is covered by the GNU GPL. See file COPYING. Use of sn is not restricted. No express or implied warranty of fitness for any purpose (but I'm still interested in bug reports and comments). Known problems -------------- It's possible that duplicate articles will be entered in the spool, if more than one article stream is fed into it simultaneously. This probably will never be "fixed". History ------- sn was created by Harold Tay and maintained by him until version 0.3.2. From 0.3.3 onwards, sn is maintained by Patrik Rådman . For details see the CHANGES file. sn-0.3.8/INSTALL.notes0000600000175000001440000001712510105424251014763 0ustar patrikusers00000000000000If you're using sn in a novel way, I'd like to hear from you. Otherwise, here are some possibilities. Look over the man pages or much of this won't make sense. I haven't provided detailed instructions on these; systems vary too much. To speed up news fetching. Experiment with the -c option to snget. See the man page to snfetch and heed the warning. It works when talking to INN. To use "suck" to feed the news spool. suck is a separate package that can be used to fetch news. It has article filtering capabilities not present in snfetch that you may want. Versions of sn up to 0.2.1 made use of either suck or tcpclient with snfetch to feed the news spool. This was run from snget, which was at that time a shell script. The script still remains as snget.sh even though it is no longer used, and you can look at to see how it can be done. Suck will not know how to check if an article is already present, so use the -c option to snstore. To let users use sn for a personal news system. If you're the system admin, instruct your users to set their shell environment to include these: SNROOT=$HOME/sn # or whereever export SNROOT # sh-type shells Be sure their .qmail or .forward or .procmailrc and any crontab commands sets these too. If they are to use this to store mail, they may also want to add a command "umask 044" to ensure files created are not world readable. Then they can use the sn programs just as you can, only it applies to their own spool. The advantage to doing this is that it permits them another way to store their mailing list messages (see below). You don't really want them to run another system news spool there. Don't forget to tell them how to expire. To turn a mailing list into a site-wide newsgroup: Create a local newsgroup first, call it say "local.mailing-list": $ snnewgroup local.mailing-list Set up your mail system (.forward, .qmail, /etc/aliases or whatever) to pipe each incoming message to snmail, with the command line similar to: PREFIX/sbin/snmail -sc mailing-list local. (note trailing dot). Make sure permissions are correct and $SNROOT is set correctly. If you're using sendmail, you may run into permissions problems. It's a good idea to make sure the return path of the incoming mail is the mailing list bounce address before feeding it to snmail, or you might get all manner of crud accumulating in the group. This takes care of the incoming end of the mailing list. If you also want to transparently forward articles posted into that newsgroup to the mailing list, do the following: Copy the file dot-outgoing.ex into SNROOT/local.mailing-list/.outgoing and edit it (follow the comments). This script gets executed by snsend as "news.group.name/.outgoing" each time someone posts to that newsgroup, and the article is made available on its standard input. You'll need to set the mailing list address in it and tell it where to find sendmail. You will probably also want to set the return address on it, so bounces come back to the true subscriber to the mailing list and so the list recognizes the sender. Details on this vary according to your MTA. DO THIS CAREFULLY! This is in effect a remailer, and you are permitting your users to use your mail address. You can always set/unset POSTING_OK via tcpd so snntpd will control posting according to the posting host, or edit PREFIX/sbin/SNPOST for even greater control. To prevent snget from hogging your modem when you dial up: Use the -h option to snget. See the snget man page. To change the upstream server of a (non-local) newsgroup: You have to do this manually, as the owner of SNROOT or root. Tell sn about the new upstream server, if it doesn't already know: $ cd SNROOT $ mkdir .outgoing/new.news.server:port Use all lower case for the server name. "port" is the numeric port number, usually 119. Delete the old symlink: $ rm news.group.name/.outgoing Re-create the same symlink to point to the new server: $ ln -s ../.outgoing/new.news.server:port news.group.name/.outgoing Zero out the newsgroups .serial file so it will re-sync on the next fetch: $ echo '0' >news.group.name/.serial Make sure permissions are correct if you did this as root: $ chown news.news news.group.name/.serial \ .outgoing/new.news.server:port Replace "news.news" with whatever the ownership is on SNROOT. To create a moderated local newsgroup: As with a newsgroup gated into a mailing list, create a .outgoing file: $ echo 'mail moderator@myhost' >SNROOT/news.group.name/.outgoing (you can run sendmail directly instead of mail if you wish) where "moderator@myhost" is the email address of the moderator. Then all new articles posted to that group end up being sent to him, where, if approved, he can run snstore manually to insert the article. Don't use "cat >>filename"; you will probably run into permissions and locking problems. To do news-on-demand: Use sngetd. To recreate the article ID database: This action may become necessary, because the database can, under degenerate conditions, grow to enormous sizes. The database also isn't written synchronously, so it could become corrupt after a system crash. First, ensure that for the duration of the rebuild, snget or suck or whatever it is you're using to pull in news, and snntpd, are not running and will not be run. $ cd SNROOT $ touch .noservice $ rm .newsgroup .table .chain $ find . -type f -name "+*" -exec rm -f {} \; $ snprimedb -i $ snscan -n * | snprimedb; rm -f .noservice Don't forget the "-n" to snscan. This will rebuild the database completely. Its a good idea to place these lines in your system rc files so this is done on every reboot, after fsck. To see which newsgroups are popular: Run a daemon or shell script that reads newsgroup names from SNROOT/.fifo and records them. snntpd writes the name of the newsgroup that someone is reading into this fifo. You can use this information to decide which newsgroups to drop, or which to alter expiration for, or to run a dynamic feed. To save homeless articles: Create a local newsgroup with the special name "=junk". snstore will save articles here that don't belong anywhere else. Such a situation is rare if you never use snstore directly, but it is possible if for example you are gating a mailing list into it, or if you are feeding the spool other than through snget, sngetd, or snntpd. snntpd will not recognize "=junk" as a valid newsgroup even if it exists, so you cannot POST to it. The default expiration age for "=junk" is also 7 days. To pull a newsgroup from 2 servers: Some people have written asking about this, because their primary server isn't very reliable or complete, so they want to augment the articles with those obtained from somewhere else. I haven't gotten around to writing this, but the idea is to wrap snget with a script that rotates the .outgoing symlink among all the servers each time it is called, so that each fetch is from a different server. The .serial file will have to be similarly rotated. To authenticate connecting clients: snntpd doesn't do any authentication at all. You do that by running a script or program of your devising, that speaks limited NNTP, that performs your choice of authentication. If successful, have it set the environment to inform PREFIX/sbin/SNPOST, then exec snntpd with the -S (suppress greeting) flag. For a simple script to do this, see contrib/simple_authentication. If you'd prefer PAM-based authentication, see contrib/pam_authentication. sn-0.3.8/snprimedb.8.in0000600000175000001440000000413707564256733015312 0ustar patrikusers00000000000000.TH snprimedb,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snprimedb \- feed the sn database. .SH SYNOPSIS .B snprimedb .RB [- i ] (no arguments) .br .SH DESCRIPTION .B snprimedb reads records from its standard input and enters them into the database. The database is appended to; if you want to start a new database, you would delete the database files .IR .chain , \ .table , \ .newsgroup before running this program. The main purpose of .B snprimedb is to rebuild the ID database. Then its input is usually obtained from .BR snscan . If the option .RB - i is given, simply initialize the database files if they do not exist, and exit. Input lines are of the format .br .I newsgroup id serial where .I newsgroup is the newsgroup the article may be found in, .I id is its message id, and .I serial is the local serial number of that article in that newsgroup. If .I newsgroup is the special name .BR =junk , then the whole line is ignored. You will need to be root or the owner of !!SNROOT!! to do this. .SH BUGS .B snprimedb does not check to see that the articles really exist. Also the size of the hash table is fixed at compile time. Under degenerate conditions, the hash table file .I .chain (see below) could grow very large, with reclaimed space remaining unused. This is due to the very simple file space allocator, which doesn't know how to coalesce adjacent free areas. It doesn't know how to split them either. The allocator assumes that the shape of the distribution of record lengths remains quite constant over time. The hash table database doesn't make any attempt to reduce its footprint. This should be acceptable, since the database is shared. .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .SH FILES .TP .RI !!SNROOT!! /.table .B snprimedb uses this file as the index of the hash table, and ... .TP .RI !!SNROOT!! /.chain as the hash chains, and ... .TP .RI !!SNROOT!! /.newsgroup to attach an integer identifier to each newsgroup name. This file is a human-readable flat text file. sn-0.3.8/README.files0000600000175000001440000001127307564256733014611 0ustar patrikusers00000000000000List of files that the sn package uses. Filenames here are relative to SNROOT File Type Read by Written by Description ---- ---- ------- ---------- ----------- /G/.compress ?plain text snstore you Whether compression should be applied to this group, and for article bodies of what size. /G/.times !bin array snntpd snstore/ For NEWNEWS, times_*() snexpire. /G/.outgoing ?symlink/dir snpost, snntpd/ If dir, contains or file snntpd do_post() articles to be sent upstream. Else is script or program, run to dispatch POSTed articles. /G/.expire ?plain text snexpire you (optional) Contains max age. /G/.serial !plain text snget snget Next serial for G's server. /G/.serial.tmp +plain text snget, snfetch If snfetch exits ok, sngetd snget renames this to .serial /G/.max ?plain text snfetch, you Max. number of snget, articles that can sngetd be fetched at a time. /G/.nopost ?empty snstore you Deny posting to this group /G/.created empty snntpd snnewgroup Creation time. /G/.info ?plain text snntpd you Description of group /.nopost ?empty snstore, you Disallows sitewide snntpd posting. /.table !struct dh_*() allo_*() Hash chain index /.chain !binary dh_*() allo_*() Hash chains and message IDs. Like dbm's .pag and .dir /.newsgroup !plain text dh_*() ng_*() Each line is a group name and group ident. /.control ?file snntpd you Program or script to handle control messages POSTed. /.outgoing dir snnewgroup Contains the linked-to spool dirs of individual groups. /.outgoing/server.add.ress:port dir snpost snnewgroup Symlinked from /G/.outgoing; spooling directory for this server. /.outgoing/server.add.ress:port/$* ?+plain text snpost snntpd Outgoing articles. /.outgoing/server.add.ress:port/request-* ?+plain text you/cron Files containing arbitrary commands for this server. /.me ?plain text snntpd, you What the system's snstore, name is. Otherwise, snscan,sncat looks it up. Notes: All paths are relative to SNROOT, the spool directory. /G/ represents a newsgroup directory. For Type: ? is optional; + is transitory; ! is created if it doesn't exist; otherwise, is required. If /G/.outgoing is a (symlink to a) directory, implies G is a global newsgroup, and articles posted to it are saved in this directory before being sent upstream. If /G/.outgoing is a (symlink to a) regular file, implies G is a private newsgroup, and posting to it invokes "sh G/.outgoing" and the article is NOT entered into the news spool. If /G/.outgoing does not exist, implies G is a local newsgroup, and all posts to it are entered into the spool immediately. All metadata (everything but the articles) file names begin with a dot. /.nopost is meant to disable entry of all articles into the spool, perhaps because the spool is under reconstruction. /G/.nopost is meant to disallow the NNTP POST command for that group. sn-0.3.8/sngetd.8.in0000600000175000001440000000002107564256733014577 0ustar patrikusers00000000000000.so man8/snget.8 sn-0.3.8/field.c0000600000175000001440000000152210101226540014021 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Check the field name of a header line. */ static const char ver_ctrl_id[] = "$Id: field.c 52 2004-07-26 16:12:16Z patrik $"; int check_field (char *field, int len) { int i; for (i = 0; i < len; i++) { /* Allow printable US-ASCII characters, except colon */ if (field[i] >= '!' && field[i] <= '9') continue; if (field[i] >= ';' && field[i] <= '~') continue; if (field[i] == ':') return i; if (field[i] == ' ' || field[i] == '\t') break; return 0; } do i++; while (field[i] == ' ' || field[i] == '\t'); if (field[i] != ':') return 0; return i; } sn-0.3.8/get.c0000600000175000001440000004765310043507662013550 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Code common to snget and sngetd. Schedules each newsgroup to * fetch, controls throttling. */ #include #include #include #include #include #include #include #include #include #include #include #include #if 0 /* XXX */ #include /* ward off __u32 errors XXX */ #endif #include #include "config.h" #include "get.h" #include "addr.h" #include "valid.h" #include #include #include #include static const char ver_ctrl_id[] = "$Id: get.c 36 2004-04-27 16:52:02Z patrik $"; struct job { int pid; int sd; int infd; /* for throttling */ int outfd; /* either goes to snstore, or a file */ int attempts; /* these are constant for the lifetime of the job */ char *server; unsigned long addr; unsigned short port; char group[1]; }; /* opt* values are propagated to children */ char *optdebug = NULL; char *optpipelining = NULL; char *optmax = NULL; /* only for priming a group (serial == 0) */ bool optlogpid = FALSE; bool optnocache = FALSE; /* internal option */ int throttlerate = 0; int concurrency = 4; extern int rename(const char *, const char *); static char path1[(GROUPNAMELEN + sizeof ("/.serial.tmp")) + 1]; static char path2[(GROUPNAMELEN + sizeof ("/.serial.tmp")) + 1]; static char *argv_snstore[4] = { "snstore", }; static int argc_snstore; static char *argv_snfetch[14] = { "snfetch", }; static int argc_snfetch; #define MAX_CONCURRENCY 8 #define ASSERT(cond) /* nothing */ static int atou (char *buf) { int i, c; for (i = 0; *buf; buf++) { c = *buf - '0'; if (c > 9 || c < 0) return -1; i *= 10; i += c; } return i; } /* -------------------------------------------------- * Handling store descriptors. This is either a pipe to snstore * or an fd to a (common) file. */ static int stores[MAX_CONCURRENCY]; static int nstores = 0; static int store_get (void) { if (!nstores) { int pid; pid = cmdopen(argv_snstore, 0, stores); if (pid <= 0) { if (-19 == pid) LOG("store_get:exec snstore:PATH set?"); else if (-1 == pid) LOG("store_get:exec snstore:%m?"); else LOG("store_get:snstore exited with %d", 0 - pid); return -1; } fcntl(*stores, F_SETFD, 1); nstores++; LOG3("pipe %d is snstore pid %d", *stores, pid); } return stores[--nstores]; } static void store_put (int fd) { if (nstores < MAX_CONCURRENCY) stores[nstores++] = fd; else close(fd); } /* -------------------------------------------------- * Handling and reusing socket descriptors. Constructs and reuses, * but does not destroy; caller destroys sockets. */ struct sock { char *server; unsigned long addr; unsigned short sd; unsigned short port; }; static struct sock socks[MAX_CONCURRENCY]; static int nsocks = 0; static int sock_get (char *server, int port, unsigned long *addrp, int *reused) { int i, sd; for (i = 0; i < nsocks; i++) if (port == socks[i].port) if (0 == strcasecmp(server, socks[i].server)) { *reused = 1; sd = socks[i].sd; *addrp = socks[i].addr; nsocks--; if (i != nsocks) socks[i] = socks[nsocks]; return sd; } /* XXX We could be passing a socket that isn't fully connected yet. */ *reused = 0; if ((sd = socket(AF_INET, SOCK_STREAM, 0)) > -1) { struct hostent *hp; if ((hp = gethostbyname(server))) { struct sockaddr_in sa; int fl; sa.sin_family = AF_INET; sa.sin_port = htons(port); sa.sin_addr.s_addr = *(unsigned long *) (hp->h_addr_list[0]); *addrp = ntohl(sa.sin_addr.s_addr); fl = fcntl(sd, F_GETFL); fcntl(sd, F_SETFL, fl | O_NONBLOCK); i = connect(sd, (struct sockaddr *) &sa, sizeof (sa)); if (i > -1 || EINPROGRESS == errno) { fcntl(sd, F_SETFL, fl); fcntl(sd, F_SETFD, 1); return sd; } else LOG("sock_get:connect to %s:%m?", server); } else LOG("sock_get:resolve \"%s\"", server); close(sd); } else LOG("sock_get:socket to %s:%m", server); return -1; } static void sock_put (int sd, char *server, unsigned long addr, int port) { if (nsocks < MAX_CONCURRENCY) { socks[nsocks].sd = sd; socks[nsocks].server = server; socks[nsocks].port = port; socks[nsocks].addr = addr; nsocks++; } else close(sd); } /* -------------------------------------------------- * Maintaining queues for jobs running and not yet done. * * queue: 1=============2===========3----4 * 1: start of run q (index 0) * 2: end of run q, start of todo q (index toff) * 3: end of todo q, start of free space (index tend) * 4: end of space (qsize) */ static struct job **queue = NULL; static int toff = 0; static int tend = 0; static int qsize; static int queue_add (struct job *jp) { if (tend >= qsize) { struct job **tmp; int newsize; newsize = (queue ? (qsize * 2) : 20); if (!(tmp = malloc(newsize * sizeof (struct job *)))) return -1; if (queue) { memcpy(tmp, queue, qsize * sizeof (struct job *)); free(queue); } queue = tmp; qsize = newsize; } queue[tend++] = jp; return 0; } static void swap (int a, int b) { struct job *tmp; if (a == b) return; tmp = queue[a]; queue[a] = queue[b]; queue[b] = tmp; } static void queue_todo2run (int j) { ASSERT(j >= toff && j < tend) swap(toff++, j); } static void queue_run2todo (int j) { ASSERT(j > -1 && j < toff) swap(--toff, j); } static void queue_rm (int j) { ASSERT(tend > 0) if (j < toff) { queue_run2todo(j); j = toff; } swap(j, --tend); free(queue[tend]); } static int similarity (char *g1, char *g2) { char *c1; int inc1; char *c2; int inc2; int score; score = 0; for (inc1 = 7; inc1 && *g1; inc1--) { inc2 = 7; for (c2 = g2; inc2 && *c2; inc2--) { for (c1 = g1; *c1 == *c2 && *c1 && '.' != *c1; c1++, c2++) score++; if (*c1 == *c2) /* both dots or \0 */ score += inc1 + inc2; /* bonus */ do if (!*c2) break; while ('.' != *c2++) ; } do if (!*g1) break; while ('.' != *g1++) ; } return score; } /* * Choose a new group to run, unlike any other currently running. */ static int queue_getnext (void) { int max; int bestscore, bestgroup; int t, r; switch ((max = tend - toff)) { case 0: return -1; case 1: return toff; default: max = 4; case 2: case 3: case 4:; } bestscore = 10000; bestgroup = toff; for (t = toff; t < toff + max; t++) { /* todo */ int score; score = 0; for (r = 0; r < toff; r++) /* running */ score += similarity(queue[r]->group, queue[t]->group); if (score < bestscore) { bestscore = score; bestgroup = t; } } return bestgroup; } /* -------------------------------------------------- * Adding throttling. Caller hands us 2 descriptors, fromfd is the * network socket, tofd is a pipe. We don't care who it belongs to. */ int throttle_setfds (fd_set * rs) { int max, i; max = -1; for (i = 0; i < toff; i++) if (queue[i]->infd > -1) { FD_SET(queue[i]->sd, rs); if (queue[i]->sd > max) max = queue[i]->sd; } return max; } /* * Select on the sockets (read), not the pipes (write), * because we assume snstore won't block us (for long). */ void throttle (fd_set * rs) { static struct timeval last = { 0, }; char buf[300]; /* mustn't be too big, for pipes */ static int bytes = 0; int i, c; /* Pay for the bytes transferred last time. */ if (bytes > 0) { struct timeval tv; long sec, msec, usec; gettimeofday(&tv, 0); sec = tv.tv_sec - last.tv_sec; usec = tv.tv_usec - last.tv_usec; if (usec < 0) { usec += 1000000; sec--; } msec = (bytes * 1000) / throttlerate; if (msec > 0) { sec = (msec / 1000) - sec; usec = ((msec % 1000) * 1000) - usec; if (usec < 0) { usec += 1000000; sec--; } tv.tv_sec = sec; tv.tv_usec = usec; if (sec >= 0) select(0, NULL, NULL, NULL, &tv); } bytes = 0; } bytes = 0; for (i = 0; i < toff; i++) if (queue[i]->infd > -1 && FD_ISSET(queue[i]->sd, rs)) /* ignore socket read errors */ if ((c = read(queue[i]->sd, buf, sizeof (buf))) > 0) { if (-1 == write(queue[i]->infd, buf, c)) { LOG("throttle:pipe to %s:%m", queue[i]->group); close(queue[i]->infd); queue[i]->infd = -1; } else bytes += c; } gettimeofday(&last, 0); } /* -------------------------------------------------- * Choosing groups to run and running programs to fetch for them. */ static void readfile (char *dir, char *fn, char buf[20]) { int fd, c, i; formats(path1, sizeof (path1) - 1, "%s/%s", dir, fn); if ((fd = open(path1, O_RDONLY)) > -1) { c = read(fd, buf, 19); close(fd); if (c > 0) { buf[c] = '\0'; for (i = 0; i < c; i++) { if ('\n' == buf[i]) { buf[i] = '\0'; break; } if (buf[i] < '0' || buf[i] > '9') { LOG("readfile:bad value in %s/%s", dir, fn); *buf = '\0'; return; } } return; } else LOG("readfile:read %s:%m", path1); } else if (ENOENT != errno) LOG("readfile:open %s:%m", path1); *buf = '\0'; } int reap (void) { int pid, ex, j; struct job *jp; if (toff <= 0 || (pid = waitpid(-1, &ex, WNOHANG)) <= 0) return 1; for (j = toff - 1; j > -1 && pid != queue[j]->pid; j--) ; if (-1 == j) return 1; jp = queue[j]; if (WIFEXITED(ex)) { if ((ex = WEXITSTATUS(ex))) LOG("reap:job %s exited %d", jp->group, ex); } else if (WIFSIGNALED(ex)) { LOG("reap:job %s caught signal %d", jp->group, ex = WTERMSIG(ex)); ex = 0 - ex; } else { ASSERT(0) return 1; } /* * If child failed in any way, close socket, otherwise save it. * If child failed from signal or system error, close store pipe, * otherwise it's still useable so save it. */ if (ex || optnocache) close(jp->sd); else sock_put(jp->sd, jp->server, jp->addr, jp->port); if (2 == ex || ex < 0 || optnocache) (void) close(jp->outfd); else store_put(jp->outfd); if (jp->infd > -1) close(jp->infd); if (ex && jp->attempts <= 2) { jp->infd = jp->sd = jp->outfd = jp->pid = -1; queue_run2todo(j); return 1; } if (0 == ex) { formats(path1, sizeof (path1) - 1, "%s/.serial.tmp", jp->group); formats(path2, sizeof (path2) - 1, "%s/.serial", jp->group); if (-1 == rename(path1, path2)) if (ENOENT != errno) LOG("reap:rename %s, %s:%m", path1, path2); } else LOG("reap:Giving up on %s", jp->group); queue_rm(j); return 0; } bool sigchld = FALSE; static void chldhand (int x) { sigchld = TRUE; } bool sigusr = FALSE; static void usrhand (int u) { sigusr = TRUE; if (SIGUSR1 == u) { if (throttlerate > 1) throttlerate /= 2; } else if (SIGUSR2 == u) throttlerate *= 2; } /* called only after fork */ static void fixfd (int from, int to) { if (-1 == dup2(from, to)) fail(2, "Can't dup %d to %d:%m", from, to); } int sow (void) { int j, reused, p[2], tmo; struct job *jp; char maxbuf[20], serialbuf[20], timeoutbuf[20]; char envgroup[GROUPNAMELEN + sizeof ("NEWSGROUP=")]; char envserver[256]; char envport[20 + sizeof ("TCPREMOTEPORT=")]; char envip[20 + sizeof ("TCPREMOTEIP=")]; char envtimeout[20 + sizeof ("TIMEOUT=")]; if (toff >= concurrency) return 1; if (-1 == (j = queue_getnext())) return 1; jp = queue[j]; jp->attempts++; do { if ((jp->outfd = store_get()) > -1) { if ((jp->sd = sock_get(jp->server, jp->port, &jp->addr, &reused)) > -1) { if (!throttlerate || pipe(p) > -1) { if ((jp->pid = fork()) > -1) { if (0 == jp->pid) break; if (throttlerate) { close(p[0]); jp->infd = p[1]; } else jp->infd = -1; queue_todo2run(j); return 0; } else LOG("sow:fork for %s:%m", jp->group); } else LOG("sow:pipe for %s:%m", jp->group); close(jp->sd); /* Can't reuse, not logged in */ jp->sd = -1; } store_put(jp->outfd); jp->outfd = -1; } return -1; } while (0); /* child */ if (chdir(jp->group)) fail(2, "Can't chdir(%s):%m", jp->group); readfile(".outgoing", ".timeout", timeoutbuf); argv_snfetch[argc_snfetch++] = "-t"; if ((tmo = atou(timeoutbuf)) <= 0) { tmo = 120; strcpy(timeoutbuf, "120"); } LOG3("%s on socket %d, pipe %d", jp->group, jp->sd, jp->outfd); argv_snfetch[argc_snfetch++] = timeoutbuf; argv_snfetch[argc_snfetch++] = jp->group; formats(envgroup, sizeof (envgroup) - 1, "NEWSGROUP=%s", jp->group); putenv(envgroup); formats(envtimeout, sizeof (envtimeout) - 1, "TIMEOUT=%s", timeoutbuf); putenv(envtimeout); formats(envserver, sizeof (envserver) - 1, "SERVER=%s", jp->server); putenv(envserver); formats(envport, sizeof (envport) - 1, "TCPREMOTEPORT=%d", (int) jp->port); putenv(envport); strcpy(envip, "TCPREMOTEIP="); { unsigned char ch; unsigned long a; char buf[32]; char *p; a = jp->addr; p = buf + 31; *p-- = '\0'; ch = a & 0xff; do *p-- = '0' + (ch % 10); while ((ch /= 10)); *p-- = '.'; ch = (a >> 8) & 0xff; do *p-- = '0' + (ch % 10); while ((ch /= 10)); *p-- = '.'; ch = (a >> 16) & 0xff; do *p-- = '0' + (ch % 10); while ((ch /= 10)); *p-- = '.'; ch = (a >> 24) & 0xff; do *p-- = '0' + (ch % 10); while ((ch /= 10)); strcat(envip, p + 1); } putenv(envip); fixfd(jp->sd, 7); fixfd(throttlerate ? p[0] : jp->sd, 6); fixfd(jp->outfd, 1); if (!reused) { int pid, s; fd_set set; sigchld = FALSE; if (-1 == (pid = fork())) fail(2, "Can't fork for SNHELLO:%m"); if (0 == pid) { char *v[2]; if (-1 == chdir(".outgoing")) fail(2, "chdir(%s/.outgoing):%m", jp->group); fixfd(2, 1); v[1] = 0; *v = "./.SNHELLO"; execv(*v, v); *v = "SNHELLO"; execvp(*v, v); fail(2, "Can't exec %s:%m", *v); } FD_ZERO(&set); while (!sigchld) { struct timeval tv; tv.tv_sec = tmo; tv.tv_usec = 0; FD_SET(jp->sd, &set); if (select(jp->sd + 1, &set, NULL, NULL, &tv)) { tv.tv_sec = 2; select(0, NULL, NULL, NULL, &tv); } else kill(pid, SIGALRM); } while (-1 == waitpid(pid, &s, 0) && EINTR == errno) ; /* * XXX Just because our child died doesn't mean it's children * have died, so it's possible that a bastard grandchild has * stolen our connection. */ if (WIFEXITED(s)) { if ((s = WEXITSTATUS(s))) fail(2, "SNHELLO(%s) exited %d", jp->group, s); } else if (WIFSIGNALED(s)) fail(2, "SNHELLO(%s) caught signal %d", jp->group, WTERMSIG(s)); else fail(2, "SNHELLO(%s) unknown wait status %d", jp->group, s); } readfile(".", ".serial", serialbuf); argv_snfetch[argc_snfetch++] = serialbuf; if (*serialbuf == '\0') { LOG3("sow:couldn't get a value from %s/.serial:%m?", jp->group); strcpy(serialbuf, "0"); } if (strcmp(serialbuf, "0") == 0) argv_snfetch[argc_snfetch++] = (optmax ? optmax : "200"); else { readfile(".", ".max", maxbuf); argv_snfetch[argc_snfetch++] = (*maxbuf ? maxbuf : 0); } argv_snfetch[argc_snfetch] = 0; execvp("snfetch", argv_snfetch); fail(19, "sow:can't exec snfetch for %s:%m", jp->group); /* Not Reached */ return -1; } void init (void) { struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_handler = chldhand; sigaction(SIGCHLD, &sa, 0); sa.sa_handler = usrhand; sigaction(SIGUSR1, &sa, 0); sa.sa_handler = usrhand; sigaction(SIGUSR2, &sa, 0); sa.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sa, 0); argc_snstore = argc_snfetch = 1; if (optlogpid) argv_snstore[argc_snstore++] = argv_snfetch[argc_snfetch++] = "-P"; if (optdebug) argv_snstore[argc_snstore++] = argv_snfetch[argc_snfetch++] = optdebug; if (optpipelining) { argv_snfetch[argc_snfetch++] = "-c"; argv_snfetch[argc_snfetch++] = optpipelining; } argv_snstore[argc_snstore] = 0; } int add (char *group) { struct s { struct s *next; unsigned short port; char server[1]; }; static struct s *servers = NULL; struct job *jp; int c, port; char *p; formats(path1, sizeof (path1) - 1, "%s/.outgoing", group); if ((c = readlink(path1, path2, sizeof (path2) - 1)) > 0) { path2[c] = '\0'; if ((p = strrchr(path2, ':')) && (port = atou(p + 1)) > 0) { *p = '\0'; if ((p = strrchr(path2, '/'))) { struct s *sp; for (p++, sp = servers;; sp = sp->next) if (!sp) { if ((sp = malloc(sizeof (struct s) + strlen(p)))) { sp->port = port; strcpy(sp->server, p); sp->next = servers; servers = sp; } break; } else if (port == sp->port) if (0 == strcasecmp(sp->server, p)) break; if (sp && (jp = malloc(sizeof (struct job) + strlen(group) + 1))) { jp->port = port; jp->server = sp->server; strcpy(jp->group, group); jp->sd = jp->outfd = jp->pid = jp->infd = -1; jp->attempts = 0; if (0 == queue_add(jp)) return 0; free(jp); } LOG("add:No memory"); } else LOG("add:%s has bad server symlink", group); } else LOG("add:%s server symlink missing port", group); } else LOG("add:Can't read symlink in %s:%m", group); return -1; } int jobs_not_done (void) { return tend; } /* * Rather cavalier about not reaping snstore processes. * When not shutting down, reap() will wait for any. */ void quit (void) { if (nstores) do close(stores[--nstores]); while (nstores); if (nsocks) do close(socks[--nsocks].sd); while (nsocks); } sn-0.3.8/snsend.8.in0000600000175000001440000001210707564256733014615 0ustar patrikusers00000000000000.TH snsend,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snsend \- distribute articles .br snstore \- store articles locally .SH SYNOPSIS .B snsend .RI [- rvcna ] .br .B snstore .RI [- rvcna ] .SH DESCRIPTION .B snsend reads articles from descriptor 0 and distributes each one into each newsgroup they are posted to, like .BR inews . .B snstore does the same but stores them all locally. The input articles are expected to be in wire format (lines end with CRLF, leading dots are doubled, and articles are terminated with a lone dot). Control messages are not treated specially. The newsgroups list is taken from the .B X-sn-Newsgroups field if it exists; otherwise it is taken from the .B Newsgroups field, which must exist or the article will be .BR junked . All fields whose names begin with .B X-sn- (case insensitive) are always removed. If .B Date or .B Message-ID are not present, these are created. The local hosts name is prepended to the .B Path field. If an article is to be .BR junked , it is sent to the junk newsgroup if it exists, otherwise it is discarded. .SH ROUTING This applies only to .BR snsend . .B snstore does not route; it treats all .I news.groups as though they were local (see .B Nonexistent newsgroup and .B Local newsgroup below). If any of the following fails, .B snsend aborts: For each .I news.group an article is (cross-) posted to, .B snsend routes the article as follows, aborting if any action fails: .TP .B Nonexistent newsgroup If .RI !!SNROOT!!/ news.group is not a directory, .B snsend ignores this .IR news.group . If all .IR news.group s are thus ignored, the article is .BR junked . .TP .B Global newsgroup If .RI !!SNROOT!!/ news.group /.outgoing is a (symlink to a) directory, .B snsend stores the article into a file in that directory, if the same article does not already exist there (so .IR news.group 's upstream feed doesn't get multiple copies of the same article.) Such in-transit article files are given names that begin with a .B $ sign. .TP .B Local processing via script Otherwise if .RI !!SNROOT!!/ news.group /.outgoing is a regular file, it is taken to be a script or program and is run with the article available on its input. See .RB !!SNROOT!!/ dot-outgoing.ex . .TP .B Local processing via fifo Otherwise if .RI !!SNROOT!!/ news.group /.outgoing is a fifo, the article is written into it. It is an error if nothing is reading the fifo. .TP .B Local newsgroup Otherwise if .RI !!SNROOT!!/ news.group /.outgoing does not exist, the article is stored into .IR news.group . .SH OPTIONS Options apply to .B snsend and .B snstore equally. .TP .RB - r The article stream is in rnews batch format, rather than wire format. Only the .B #! rnews form is understood. .TP .RB - c If an article already exists in the local newsgroup it is destined for, don't store it there. For .BR snsend , this option has no effect on newsgroups that are not local. .TP .RB - a (Aliases not allowed.) When storing to multiple local newsgroups, do not alias subsequent copies to the first, instead, make a copy. Aliasing saves disk space, but when the original expires, so do all aliases to it. This option has no effect on newsgroups that are not local. .TP .RB - n Don't actually do anything with the article, just dump it back onto descriptor 1. .TP .RB - v For each article stored in each newsgroup, output a line to descriptor 1 similar to what .B snscan would emit, except that for non-local newsgroups the serial number will always be 0. .SH FILES .TP .RI !!SNROOT!!/ news.group.name / This includes .RB !!SNROOT!!/ =junk newsgroup if it exists. Each such directory represents the newsgroup of the same name, and articles are stored in files .BR 1 , .BR 2 , .BR 3 , etc. beneath it. Each such file contains 1 or more articles. This is contrary to the traditional form of .RI !!SNROOT!!/ news / group / name . .IR news.group.name . .TP .RI !!SNROOT!!/ news.group /.outgoing See also .B ROUTING above. The presence of this directory indicates that .I news.group is global, and articles posted to .I news.group end up here in files named .BR $ *. These files are linked in already complete, so all such files are ready to be uploaded. .TP .RI !!SNROOT!!/ news.group /.compress If this file exists, articles stored in .I news.group are candidates for compression. The content of the file is a number representing a minimum article body size below which compression won't be applied. If the file is empty this threshold defaults to 1024 bytes. .TP .RB !!SNROOT!!/ .me If this file exists, it's contents are taken to be the local hosts name for purposes of the .B Path field. Otherwise the name is obtained from .BR gethostname (2). .SH ENVIRONMENT VARIABLES See also .RB !!BINDIR!!/ dot-outgoing.ex for other variables exported when .B snsend invokes a .outgoing program. .TP .B SNROOT If this is set, its value is used in place of !!SNROOT!!. .SH EXIT CODES .B snsend and .B snstore exit 0 on success, 1 on usage error, 2 on system error, 3 on article format error, and 9 if .RI !!SNROOT!!/ news.group /.outgoing .RB ( snsend only) exits with other than 0. .SH SEE ALSO !!BINDIR!!/dot-outgoing.ex, snscan(1) sn-0.3.8/snmail.c0000600000175000001440000001733210101773324014236 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Rewrite an rfc822 mail message into one suitable for news: * Replace all Received: lines by a single Path: line. * Construct a Newsgroups: line, using either a given newsgroup * name, or from the From: header. * Unfold all folded header lines. * Convert In-Reply-To: and add to References: (Thanks ZB). * * Usenet message ID's are more restrictive than mail ID's. I don't * care. * * Exec snstore and pass the altered message to it. * The message is read on fd 0. * * End-of-line expected is normal Unix style '\n', which gets * converted to '\r\n'. */ #include #include #include #include #include #include #include #include "config.h" #include "parameters.h" #include "hostname.h" #include "unfold.h" #include "path.h" #include "addr.h" #include "field.h" #include #include #include #include #include #include int debug = 0; static const char ver_ctrl_id[] = "$Id: snmail.c 58 2004-07-28 18:56:20Z patrik $"; char *suffix = NULL; char *prefix = "local."; char *in_reply_to = NULL; struct b path = { 0, }; struct b references = { 0, }; struct readln input = { 0, }; void memerr (void) { fail(2, "No memory"); } /* * Construct a References: line */ void appendtoreferences (char *buf) { char *dst; char *p; dst = p = buf; while ((p = addr_qstrchr(p, '<'))) { int len; if ((len = addr_msgid(p)) <= 0) { p++; continue; } if (-1 == b_appendl(&references, " <", 2)) memerr(); if (-1 == b_appendl(&references, buf, addr_unescape(p + 1, buf, len - 2))) memerr(); if (-1 == b_appendl(&references, ">", 1)) memerr(); p += len; } } /* * Construct a Path: line from Received: lines */ void appendtopath (char *rcvd) { for (;; rcvd++) { int len; if (!(rcvd = addr_qstrchr(rcvd, ' '))) return; if (strncasecmp(rcvd, " by ", 4)) continue; for (rcvd += 4; ' ' == *rcvd || '\t' == *rcvd; rcvd++) if (!*rcvd) return; if ((len = addr_domain(rcvd)) > 0) { if (-1 == b_appendl(&path, "!", 1)) memerr(); if (-1 == b_appendl(&path, rcvd, len)) memerr(); } } } /* * To construct the tail part of a Newsgroups: line */ void savelocal (char *frm) { int len; if ((frm = addr_qstrchr(frm, '<'))) if ((len = addr_localpart(++frm)) > 0) { if (!(suffix = malloc(len + 1))) memerr(); addr_unescape(frm, suffix, len); } } /* * Decide what to do with each header line. */ struct b head = { 0, }; static int check_from_; int switchline (char *line, int len) { char *p; int i; if (check_from_) { check_from_ = 0; if (0 == strncmp(line, "From ", 5)) return 0; } if (!check_field(line, len)) LOG("accepting bad header \"%s\"", line); switch (*line) { case 'f': case 'F': if (!suffix) if (0 == strncasecmp(line, "From:", 5)) savelocal(line + 5); break; case 'r': case 'R': if (0 == strncasecmp(line, "Received:", 9)) { appendtopath(line + 9); return 0; } if (0 == strncasecmp(line, "References:", 11)) { appendtoreferences(line + 11); return 0; } if (0 == strncasecmp(line, "Return-Path:", 12)) savelocal(line + 12); /* overrides From: line */ break; case 'i': case 'I': if (0 == strncasecmp(line, "In-Reply-To:", 12)) { char *tmp; /* Find the last '<' */ for (p = tmp = line + 12; (tmp = addr_qstrchr(tmp, '<')); p = tmp++) ; i = strlen(p); if(!(in_reply_to = malloc(i + 1))) memerr(); strncpy(in_reply_to, p, i); /* Remember for later use if msg has no References: */ return 0; } break; case 'n': case 'N': if (0 == strncasecmp(line, "Newsgroups:", 11)) return 0; break; } if (-1 == b_appendl(&head, line, len)) memerr(); if (-1 == b_appendl(&head, "\r\n", 2)) memerr(); return 0; } void usage (void) { fail(1, "Usage:%s [-scanv] [listname [prefix]]", progname); } int main (int argc, char **argv) { char *fullnewsgroup; char *newsgroup; char *cp; int i; int fd; char storeopts[12]; int s; int storepid; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); parameters(FALSE); if (-1 == chdir(snroot)) FAIL(2, "Can't chdir(%s):%m", snroot); newsgroup = 0; s = 1; storepid = 0; if (argc > 1) { while ((i = opt_get(argc, argv, "")) > -1) { switch (i) { case 'd': debug++; storeopts[s++] = 'd'; break; case 'V': version(); _exit(0); case 's': storepid = -1; break; case 'P': log_with_pid(); storeopts[s++] = 'P'; break; case 'c': case 'a': case 'n': case 'v': storeopts[s++] = i; break; default: usage(); } if (s >= sizeof (storeopts)) fail(1, "Too many options"); } if (opt_ind < argc) newsgroup = argv[opt_ind++]; if (opt_ind < argc) prefix = argv[opt_ind++]; if (opt_ind != argc) usage(); } if (-1 == readln_ready(0, 0, &input)) FAIL(2, "readln_ready:%m"); if (storepid != 0) { char *args[3]; storeopts[s] = '\0'; args[0] = "snstore"; i = 1; if (s > 1) { args[i++] = storeopts; *storeopts = '-'; } args[i] = 0; set_path_var(); if ((storepid = cmdopen(args, 0, &fd)) <= 0) fail(2, "Can't exec snstore:%m?"); } else fd = 1; check_from_ = 1; switch (unfold(&input, switchline)) { case 0: _exit(0); case -1: fail(2, "Read or memory error:%m"); case -2: case -3: fail(3, "Bad message format"); } if (references.buf == NULL && in_reply_to != NULL) { appendtoreferences(in_reply_to); /* Convert In-Reply-To: into References: */ free(in_reply_to); } if (!newsgroup) if (!(newsgroup = suffix)) FAIL(1, "No newsgroup specified"); i = strlen(newsgroup) + strlen(prefix); if (!(fullnewsgroup = malloc(i + 1))) memerr(); strcat(strcpy(fullnewsgroup, prefix), newsgroup); for (cp = fullnewsgroup; *cp; cp++) if (*cp <= 'Z' && *cp >= 'A') *cp += 'a' - 'A'; if (path.buf != NULL) { if (-1 == writef(fd, "Path: %s\r\n", path.buf + 1)) goto writeerr; free(path.buf); } if (-1 == write(fd, head.buf, head.used)) goto writeerr; free(head.buf); if (references.buf != NULL) { if (-1 == writef(fd, "References: %s\r\n", references.buf + 1)) goto writeerr; free(references.buf); } if (-1 == writef(fd, "Newsgroups: %s\r\n\r\n", fullnewsgroup)) goto writeerr; while ((i = readln(&input, &cp, '\n')) > 0) { cp[--i] = '\0'; if (i && '\r' == cp[i - 1]) cp[--i] = '\0'; if ('.' == *cp) write(fd, ".", 1); if (-1 == writef(fd, "%s\r\n", cp)) goto writeerr; } if (-1 == write(fd, ".\r\n", 3)) goto writeerr; if (!storepid) _exit(0); i = close(fd); if (-1 == (i = cmdwait(storepid))) fail(2, "error in waitpid:%m"); if (i) LOG("snstore failed"); _exit(i); writeerr: fail(2, "output write error:%m"); _exit(1); } sn-0.3.8/get.h0000600000175000001440000000132610042066025013530 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef GET_H #define GET_H #define MAX_CONCURRENCY 8 extern char *optdebug; extern char *optpipelining; extern char *optmax; extern bool optlogpid; extern bool optnocache; extern int throttlerate; extern int concurrency; #include extern int throttle_setfds (fd_set * rs); extern void throttle (fd_set * rs); extern int reap (void); extern int sow (void); extern bool sigchld; extern bool sigusr; extern void init (void); extern int add (char *group); extern int jobs_not_done (void); extern void quit (void); #endif sn-0.3.8/snexpire.8.in0000600000175000001440000000342407564256733015162 0ustar patrikusers00000000000000.TH snexpire,v!!VERSION!! 8 "Harold Tay" "N.B." \" -*- nroff -*- .SH NAME snexpire \- expire news under the .B sn system. .SH SYNOPSIS .B snexpire .RI [- v ] .RI [- exp ] .IR newsgroup .RI [[- exp ] .IR newsgroup ]... .br .SH DESCRIPTION .B snexpire expires news in the .B sn news system, one article file (=10 articles, compile time default) at a time. .B snexpire always leaves behind at least one article file, so that newly entered articles will have numbers not overlapping previously used numbers. As an exception if the expiration age is set to 0, then even this last file will be expired and subsequent article numbers will start from the beginning. You will need to be root or own !!SNROOT!! to run .BR snexpire . .SH OPTIONS .tp .RB - v Output a line for each article expired. The format of this line is the same as for .BR snscan . .SH ARGUMENTS .B snexpire expires all .IR newsgroup s named on the command line. Each newsgroup may be preceded by .RI - exp to control the expiration age. .I exp is of the form .IR # [ hdwmy ] where .I # is a number, followed by one of the characters .IR h , d , w , m , y representing hours, days, weeks, months and years. The default for .RI - exp is a compile-time fixed. Each .RI [- exp ] applies to the next newsgroup only, any newsgroups after that one get default treatment unless overridden by another .RI [- exp ]. .SH FILES MODIFIED .RI !!SNROOT!!/ newsgroup /.times, .RI !!SNROOT!!/ newsgroup /[0123456789]*, !!SNROOT!!/{.chain,.table}. .SH ENVIRONMENT VARIABLES .TP .B SNROOT If this is set and is not empty, the value is used in place of .BR !!SNROOT!! , the default news spool directory. .SH FILES .TP .RI !!SNROOT!!/ newsgroup / .expire If this exists, its contents are taken to be .I exp if not overridden by .RI - exp on the command line. sn-0.3.8/dhash.c0000600000175000001440000001116210101504330014022 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Disk based persistent hash table, like gdbm. * * This is specifically for the news system. The keys are message * ids, the values are {newsgroup, serial number} tuples. * * The table is static in size. There a 3 files: ".table" is the hash * table proper. It contains an array of chain "pointers". The * other 2 files are handled by allo_*(). ".chain" is an arena of * chain structures; ".newsgroup" contains all newsgroups. * * Supported opertions are insert, delete, and search. * * Requires allocate.c to allocate space on a disk file (".chain") * for data. */ #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "allocate.h" /* Save ids and chains */ #include "dhash.h" #include "newsgroup.h" /* Save newsgroup names */ #include #include static const char ver_ctrl_id[] = "$Id: dhash.c 56 2004-07-27 16:54:48Z patrik $"; extern char dh_tablefile[]; extern char dh_chainfile[]; extern char dh_groupfile[]; extern struct table *dh_table; extern unsigned dhhash (char *); extern struct chain *dhlocatechain (char *); extern bool dh_isreadonly; extern int dhlock(void); extern int dhunlock(void); static char *inttochar2 (int integer) { static unsigned char buf[2]; buf[0] = (integer & 0xffff) >> 8; buf[1] = integer & 0xff; return ((char *) buf); } static char *inttochar3 (unsigned integer) { static unsigned char buf[3]; integer >>= 2; /* last 2 bits will be 0 */ buf[0] = (integer & 0xffffff) >> 16; buf[1] = (integer & 0xffff) >> 8; buf[2] = integer & 0xff; return ((char *) buf); } int dh_insert (struct data *dp) { struct chain *chp; unsigned h; int off; int len; if (dh_isreadonly) { errno = EPERM; return -1; } /* Search first; if found, cannot insert. */ if (dhlock() == -1) return -1; chp = dhlocatechain(dp->messageid); while (chp != NULL) { unsigned index; if (strcmp(dp->messageid, chp->messageid) == 0) { dhunlock(); errno = EEXIST; return -1; } index = (unsigned) chp->next; if (index == 0) break; chp = allo_deref(index); if (chp == NULL) { LOG("dh_insert:bad deref in chain for \"<%s>\" during search", dp->messageid); break; } } len = sizeof (struct chain) + strlen(dp->messageid) + 1 - sizeof (chp->messageid); off = allo_make(len); chp = allo_deref(off); if (chp == NULL) { LOG("dh_insert:allo_make gave bad allo_deref"); dhunlock(); return -1; } chp->serial = dp->serial; strcpy(chp->messageid, dp->messageid); { int ident; ident = ng_ident(dp->newsgroup); if (-1 == ident) if (-1 == (ident = ng_addgroup(dp->newsgroup))) return -1; memcpy(chp->newsgroup, inttochar2(ident), 2); } /* * If someone searches before * dh_table->next[h] is updated, they just will miss the new one. * Readers don't need to lock, not even LOCK_SH. The worst that * can come of this is, someone else has just inserted the same * object, and we have two of them instead of one. */ h = dhhash(dp->messageid); { unsigned char *x = dh_table->next + h * 3; chp->next = char3toint(x); memcpy(x, inttochar3(off), 3); } dhunlock(); return 0; } int dh_delete (struct data *dp) { struct chain *oldchp = NULL; struct chain *chp; if (dh_isreadonly) { errno = EPERM; return -1; } if (dhlock() == -1) return -1; chp = dhlocatechain(dp->messageid); if (chp == NULL) goto fail; while (strcmp(chp->messageid, dp->messageid) != 0) { unsigned index = chp->next; if (index == 0) goto fail; oldchp = chp; chp = allo_deref(index); if (chp == NULL) { LOG2("dh_delete:dereferencing bad offset"); goto fail; } } *chp->messageid = '\0'; if (oldchp != NULL) oldchp->next = chp->next; else { unsigned i = chp->next; unsigned h = dhhash(dp->messageid); memcpy(dh_table->next + h * 3, inttochar3(i), 3); } allo_free(allo_ref(chp), sizeof (struct chain) + strlen(dp->messageid) + 1 - sizeof (chp->messageid)); dhunlock(); return 0; fail: dhunlock(); return -1; /* Not found */ } sn-0.3.8/field.h0000600000175000001440000000044307564256733014061 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef FIELD_H #define FIELD_H extern int check_field (char *field, int len); #endif /* FIELD_H */ sn-0.3.8/unfold.c0000600000175000001440000000364310042571016014240 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Return values: * -2 on format error * -1 on system error * 0 on success * 1 if decider short-circuited. */ #include #include static const char ver_ctrl_id[] = "$Id: unfold.c 29 2004-04-24 23:02:38Z patrik $"; static struct b tmp = { 0, }; static char *line; static int len; static int bytes_consumed; static int eof; /* * Not supposed to get EOF here. I think it is permitted to have an * empty body, but without the blank line separator? I shan't permit * it, because there's no way to know if indeed it is a blank * article, or an error upstream. */ static int getline (struct readln *in) { len = readln(in, &line, '\n'); if (0 == len) return -2; if (len < 0) return -1; bytes_consumed += len; if (--len > 0) if ('\r' == line[len - 1]) len--; return 0; } #define PUTLINE if (putline(tmp.buf, tmp.used)) return -3; int unfold (struct readln *in, int (*putline) (char *, int)) { eof = tmp.used = bytes_consumed = 0; switch (getline(in)) { case -2: return 0; case -1: return -1; } if (-1 == b_appendl(&tmp, line, len)) return -1; for (;;) { int ret; if ((ret = getline(in))) return ret; if (0 == len) { if (tmp.used) PUTLINE break; } if (' ' == *line || '\t' == *line) { do { len--; line++; } while (' ' == *line || '\t' == *line); if (-1 == b_appendl(&tmp, " ", 1)) return -1; } else { PUTLINE tmp.used = 0; } if (-1 == b_appendl(&tmp, line, len)) return -1; } return bytes_consumed; } sn-0.3.8/SNHELLO.in0000600000175000001440000001073310105423703014236 0ustar patrikusers00000000000000#!!!BASH!! # This default script is kept in !!BINDIR!!, and gets invoked # by snget/sngetd for ALL servers. If you want a special version for # just one particular server (for example if a username/password is # required), install an edited copy in that servers directory. # # We are invoked when snget/sngetd opens a connection to a # server. Our purpose is to read the server greeting and log in if # required, and exit successfully if all is well. We must leave the # NNTP conversation is a consistent state. # # When that is done, we take the opportunity (since we now have the # NNTP conversation all to ourselves) to upload any articles meant for # this server and do some other housekeeping. # # Invocation environment: # Descriptors 6 and 7 open to read and write NNTP, respectively. # Current directory is the appropriate server directory (one of # !!SNROOT!!/.outgoing/*/). # If the server times out, we will be sent SIGALRM from # snget/sngetd. # These variables will be set: # SERVER: name of the server we are talking to # PORT: port number of same # TIMEOUT: number of seconds of server inactivity before we are # sent SIGALRM ME=`basename $0` log () { echo "$ME:$SERVER:$@" >&2; } fail () { log "$@"; exit 1; } CODE= SAID= nntp () { [ "x$@" = x ] || echo "$@ " >&7; read -r CODE SAID <&6 || exit 1; } # # Logging in and authentication. # authenticate () { nntp "AUTHINFO USER $1" if [ x$CODE != x281 ]; then [ x$CODE = x381 ] && nntp "AUTHINFO PASS `cat password`" [ x$CODE != x281 ] && fail "$SERVER refused us: $CODE $SAID" fi } username= [ -f username ] && username=`cat username` read -r CODE SAID <&6 || fail "Can't read server greeting" case "$CODE" in 200|201) [ "x$username" != x ] && authenticate "$username" ;; 480) [ "x$username" = x ] && fail "Don't have a user name to log in with" authenticate "$username" ;; *) fail "$SERVER bad greeting:$CODE $SAID" ;; esac # # Some INN's want this. Jim Hague was here. # nntp "MODE READER" # # Our job is done, we have successfully logged in, so regardless of # what happens next we must exit 0. Note: Caller does not care if # we can successfully upload our articles, that is our responsibility. # end () { log "$@" >&2; exit 0; } on_exit () { [ -e $tmp ] && rm -f $tmp; [ "x$lockf_proc" != x ] && kill $lockf_proc; } trap on_exit 0 # Lock the server directory to serialise processing. exec 3>.lock || end "Unable to open/create .lock" read lockf_proc < <(exec snlockf 3>&3) [ "x$lockf_proc" != x ] || exit 0 # # Upload outgoing articles. # tmp=+HELLO.$$ rmcr () { sed -e 's/ //' -e '/^\.$/d' >$tmp || end "Can't run sed"; } posts="`echo '$'*`"; [ "x$posts" = 'x$*' ] && posts= for art in $posts; do nntp "POST" if [ "x$CODE" = x340 ]; then # Remove unnecessary headers. # Also remove the terminating dot if any (so we can replace it). sed '1,/^ $/{ /^[sS][eE][nN][dD][eE][rR] *:/d /^[nN][nN][tT][pP]-[pP][oO][sS][tT][iI][nN][gG]-[hH][oO][sS][tT] *:/d /^[bB][yY][tT][eE][sS] *:/d /^[lL][iI][nN][eE][sS] *:/d } /^\. $/d' $art >&7 nntp "." fi if [ "x$CODE" != x240 ]; then rmcr <$art; # XXX Problem with LOGNAME if snget started by pppd mail -s "Posting failed ($CODE $SAID)" ${NEWSMASTER:-${LOGNAME:-!!DEFAULT_ADMIN_EMAIL!!}} <$tmp || end "Can't run mail" fi rm -f $art done # # Service any requests. Add your own stuff here, but remember: # 1. Don't touch descriptor 3, because that is your lock. Otherwise # you can do pretty much anything. # 2. You should exit with success, regardless of what went wrong. # 3. Use todot() or nntp() or similar to read from the network. # I know it's slow but if you use an external program to read # it could drop bytes, and the alarm trap won't kick in. # todot () { while read -r b; do [ "$b" = ". " ] && break; echo "$b"; done <&6; } inform () { todot | rmcr; [ -s $tmp ] && sh -c "$*" <$tmp; } update () { date '+%y%m%d %H%M%S' >.t && mv .t .last-list && rm -f $1; } cmd= ; getcmd () { [ -f $1 ] && cmd="`head -1 $1`" && [ "x$cmd" != x ]; } if getcmd request-list; then nntp "LIST NEWSGROUPS" [ x$CODE != x215 ] && nntp "LIST" [ x$CODE != x215 ] && end "Can't request list:$CODE $SAID" inform "$cmd" || end "Unable to forward results" update request-list elif getcmd request-newgroups; then date=`cat .last-list 2>/dev/null` nntp "NEWGROUPS ${date:-990101 000000}" [ x$CODE != x231 ] && end "Can't request newgroups:$CODE $SAID" inform "$cmd" || { [ -s $tmp ] && end "Unable to forward results"; } update request-newgroups fi : script done sn-0.3.8/artfile.h0000600000175000001440000000243410101245331014373 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef ARTFILE_H #define ARTFILE_H /* * This is the structure of an article file. The ?len fields refer to * the total length of the entity (head or body), including leading * \0 and trailing \0. These \0's are just a simple check to see if * the entity is valid. * When an article is returned, the ?len fields in struct article are * decremented by 2, so it refers to the real length. */ /* * struct info is sometimes used to refer to a mmap()ed file, then * VOLATILE really is volatile. Other times its used to refer to just * some memory buffer. Then VOLATILE is not volatile. */ #ifndef VOLATILE #define VOLATILE volatile #endif struct info { VOLATILE int hoffset; /* Offset from the START of the FILE */ VOLATILE int hlen; /* Total length of the buffer, including leading and trailing '\0' */ VOLATILE int boffset; VOLATILE int blen; }; /* Each article file begins with this header. */ #define FILE_MAGIC 0xface0 struct file { int magic; struct info info[ARTSPERFILE]; }; struct fileobj { char *path; char *map; int size; }; #endif sn-0.3.8/dhash.h0000600000175000001440000000330610101153454014037 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef DHASH_H #define DHASH_H /* Relative to snroot, current directory */ #ifndef DH_TABLEFILE #define DH_TABLEFILE ".table" #endif #ifndef DH_CHAINFILE #define DH_CHAINFILE ".chain" #endif #ifndef DH_GROUPFILE #define DH_GROUPFILE ".newsgroup" #endif /* * Size (static) of the hash table. This is the number of possible * hash chains. */ #ifndef DH_SIZE #error "DH_SIZE not defined" #endif struct data { char *messageid; char *newsgroup; int serial; }; /* * Functions return 0 on success, -1 on error. For dh_find() * dp is in/out param, on entry must contain valid messageid. * On return, newsgroup and serial are filled in. For dh_delete(), * only dp->messageid is used. All returned values are readonly. */ extern int dh_open (char *pre, bool readonly); extern int dh_close (void); extern int dh_insert (struct data *dp); extern int dh_find (struct data *dp, bool readonly); extern int dh_delete (struct data *dp); /* Stuff to link in with sndumpdb */ struct chain { int serial; /* Serial number of the messsage within the ng */ int next; /* offset into database arena */ char newsgroup[2]; /* Newsgroup identifier, from ng_*() */ char messageid[sizeof (int) - 2]; /* Fake size */ }; #define DH_MAGIC 0xd0bed00 #define char2toint(p) ((p[0] << 8) | p[1]) #define char3toint(p) (((p)[0] << 18) | ((p)[1] << 10) | ((p)[2] << 2)) struct table { int magic; unsigned char next[DH_SIZE * 3]; }; extern struct table *dh_table; #endif sn-0.3.8/unfold.h0000600000175000001440000000051407564256733014264 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #ifndef UNFOLD_H #define UNFOLD_H struct readln; extern int unfold (struct readln *in, int (*putline) (char *, int)); #endif /* UNFOLD_H */ sn-0.3.8/args.c0000600000175000001440000000402010042571016013673 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ #include #include #include #include #include #include #include "config.h" extern int debug; static const char ver_ctrl_id[] = "$Id: args.c 29 2004-04-24 23:02:38Z patrik $"; char *args[20]; char args_outbuf[1024]; char args_inbuf[512]; int args_write (int fd, char *fmt, ...) { va_list ap; int len; va_start(ap, fmt); len = formatv(args_outbuf, sizeof (args_outbuf) - 2, fmt, ap); va_end(ap); if (-1 == write(fd, args_outbuf, len)) { *args_outbuf = '\0'; return -1; } args_outbuf[len - 2] = '\0'; if (debug >= 2) LOG("-> %s", args_outbuf); return len; } int args_read (struct readln *rp) { char *p; int len; int i; len = readln(rp, &p, '\n'); if (len <= 0) { *args_inbuf = '\0'; return -1; } for (len--; len > 0; len--) { switch (p[len]) { case '\n': case '\r': continue; } break; } p[len + 1] = '\0'; for (i = 0; i < sizeof (args_inbuf) - 1 && i <= len; i++) args_inbuf[i] = p[i]; args_inbuf[i] = '\0'; if (debug >= 2) LOG("<- %s", args_inbuf); if (len >= 512) return 0; for (i = 0; i < 20; i++) if (!(args[i] = tokensep(&p, " \t\r\n"))) break; return i; } void args_report (char *tag) { LOG("%sSent \"%s\" got \"%s\"", tag ? tag : "", args_outbuf, args_inbuf); } void args_flushtodot (struct readln *rp) { int tmo; tmo = rp->tmo; rp->tmo = 20; while (1) { int len; char *p; len = readln(rp, &p, '\n'); if (len <= 0) break; if (len > 1 && '.' == *p) { if ('\r' == p[len - 2]) p[len - 2] = '\0'; else p[len - 1] = '\0'; if (!p[1]) break; } } rp->tmo = tmo; } sn-0.3.8/path.c0000600000175000001440000000151710042571016013703 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Make sure our BINDIR exists in $PATH */ #include #include static const char ver_ctrl_id[] = "$Id: path.c 29 2004-04-24 23:02:38Z patrik $"; int set_path_var (void) { char *p; char *path; if (!(p = getenv("PATH"))) return putenv("PATH=" BINDIR); if ((path = strstr(p, BINDIR))) if (path == p || ':' == path[-1]) if (!path[sizeof (BINDIR) - 1] || ':' == path[sizeof (BINDIR) - 1]) return 0; path = malloc(strlen(p) + sizeof ("PATH=:" BINDIR) + 2); if (!path) return -1; strcpy(path, "PATH="); strcat(path, p); strcat(path, ":" BINDIR); return putenv(path); } sn-0.3.8/snexpire.c0000600000175000001440000001470110042571016014603 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * Expire old news articles in a newsgroup. Expiration is per file, * so ARTSPERFILE articles are deleted at a time. * * snexpire [-exp] newsgroup [[-exp] newsgroup] ... * If exp is not provided, uses the expiration time in that groups * .expire file. * The format is: #[hdwmy] where h is hours, d is days, w is weeks, * m is months, and y is years. * so 1w is 1 week. 1 week is the default if nothing is mentioned. */ #include #include #include #include #include #include #include #include #include #include "config.h" #include "art.h" #include "group.h" #include "dhash.h" #include "times.h" #include "parameters.h" #include "valid.h" #include #include #include #include static const char ver_ctrl_id[] = "$Id: snexpire.c 29 2004-04-24 23:02:38Z patrik $"; int debug = 0; bool report = FALSE; time_t parseexp (char *buf) { double exp; char *unit; if (buf == NULL) return -1; exp = strtod(buf, &unit); if (errno == ERANGE) return -1; switch (*unit) { case 'y': exp *= 12.0; case 'm': exp *= 4.345; case 'w': exp *= 7.0; case 'd': exp *= 24.0; case 'h': exp *= 60.0; exp *= 60.0; if (unit[1]) case '\0': default: return -1; } return ((time_t) exp); } static time_t readexp (char *newsgroup) { char buf[32]; int fd; int count; time_t t; if ((fd = openf(0, O_RDONLY, "%s/.expire", newsgroup)) > -1) { if ((count = read(fd, buf, sizeof (buf) - 1)) > -1) { for (buf[count--] = '\0'; count > 0; count--) if (buf[count] <= ' ') buf[count] = '\0'; else break; if ((t = parseexp(buf)) > -1) { close(fd); return t; } LOG("bad format in %s/.expire", newsgroup); } close(fd); } return DEFAULT_EXP; } /* * Always leave at least one file behind, so we know where the * article numbers start/end. */ static int expire (char *newsgroup, time_t age) { char buf[GROUPNAMELEN + sizeof ("/12345678901234")]; int maxfile, minfile; int file, serial, i; DIR *dir; struct dirent *dp; int tmp; char *end; LOG3("expire:group: %s, max age: %d hours", newsgroup, (age / 60 / 60)); if ((dir = opendir(newsgroup)) == NULL) { LOG("expire:opendir(%s):%m", newsgroup); return 0; } for (maxfile = -1, minfile = INT_MAX; (dp = readdir(dir)); ) if (*dp->d_name > '0' && *dp->d_name <= '9') if ((tmp = strtoul(dp->d_name, &end, 10)) > 0 && !*end) { if (tmp > maxfile) maxfile = tmp; if (tmp < minfile) minfile = tmp; } closedir(dir); if (maxfile == -1) { LOG1("expire:group \"%s\" is empty", newsgroup); return 0; } if (age) switch (serial = times_since(newsgroup, time(NULL) - age)) { case -1: LOG("times_since(%s):%m", newsgroup); return -1; case 1: /* everything is too young */ return 0; case 0: /* everything needs to be expired */ maxfile--; /* but leave one file behind */ break; default: maxfile = serial / ARTSPERFILE; if (serial % ARTSPERFILE != ARTSPERFILE - 1) maxfile--; break; } for (file = maxfile; file >= minfile; file--) { struct article a; for (i = ARTSPERFILE; i > -1; i--) { char *id; serial = (file * ARTSPERFILE) + i; if (-1 == art_gimmenoderef(newsgroup, serial, &a)) continue; id = art_findfield(a.head, "message-id"); if (id && *id) { struct data d; char *cp; if ('<' == *id) id++; if ((cp = strchr(id, '>'))) *cp = '\0'; d.messageid = id; if (0 == dh_find(&d, FALSE)) if (0 == strcmp(d.newsgroup, newsgroup)) if (d.serial == serial) if (-1 == dh_delete(&d)) LOG2("expire:can't delete id %s for %s:%d", id, newsgroup, serial); if (report) writef(1, "%s %d %s\n", newsgroup, serial, id); } else LOG2("expire:Can't find id for %s:%d", newsgroup, serial); } /* Delete the file */ formats(buf, sizeof (buf) - 1, "%s/%d", newsgroup, file); if (-1 == unlink(buf)) LOG("expire:unlink(%s):%m", buf); } times_expire(newsgroup, (maxfile + 1) * ARTSPERFILE); return 0; } static void usage (void) { fail(1, "Usage:%s [[-exp] newsgroup]...", progname); } int main (int argc, char **argv) { char *cp; int i, errors; progname = ((cp = strrchr(argv[0], '/')) ? cp + 1 : argv[0]); while ((i = opt_get(argc, argv, "")) > -1) switch (i) { case 'P': log_with_pid(); break; case 'd': debug++; break; case 'V': version(); _exit(0); case 'v': report = TRUE; break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': goto out; default: usage(); } out: parameters(TRUE); if (-1 == chdir(snroot)) FAIL(2, "Can't chdir(%s):%m", snroot); do { if (0 == times_init()) { if (0 == dh_open(NULL, FALSE)) { if (0 == group_init()) break; dh_close(); } times_fin(); } _exit(2); } while (0); errors = 0; for (i = opt_ind; i < argc; i++) { time_t exp; if ('-' == *argv[i]) { exp = parseexp(argv[i] + 1); if (-1 == exp) usage(); i++; } else if (-1 == (exp = readexp(argv[i]))) exp = DEFAULT_EXP; if (!is_valid_group(argv[i]) && strcmp(argv[i], JUNK_GROUP)) { LOG("%s is not a valid newsgroup", argv[i]); errors++; } else expire(argv[i], exp); } group_fin(); times_fin(); dh_close(); _exit(errors ? 1 : 0); } sn-0.3.8/body.c0000600000175000001440000000407410042571016013705 0ustar patrikusers00000000000000/* * This file is part of the sn package. * Distribution of sn is covered by the GNU GPL. See file COPYING. * Copyright © 1998-2000 Harold Tay. * Copyright © 2000- Patrik Rådman. */ /* * How to deal with article bodies, which may be compressed. * On input, compression takes place in store.c without our help. * Caller must not free the article body on return. */ #ifdef USE_ZLIB #include #include #include #include #include "art.h" static const char ver_ctrl_id[] = "$Id: body.c 29 2004-04-24 23:02:38Z patrik $"; int body (char **artbod, int *len) { static char *bodbuf = NULL; z_stream zs; int e, msize, inc; if (bodbuf) { free(bodbuf); bodbuf = NULL; } if (*len < 0) return 1; if (0 == *len || *len < COMPRESS_MAGIC_LEN) return 0; for (e = 0; e < COMPRESS_MAGIC_LEN; e++) if ((*artbod)[e] != COMPRESS_MAGIC[e]) return 0; inc = (*len / 2) + 1; zs.next_out = malloc(msize = zs.avail_out = 5 * inc); if (!(bodbuf = (char *) zs.next_out)) return -1; zs.next_in = (unsigned char *) *artbod + COMPRESS_MAGIC_LEN; zs.avail_in = (unsigned) (*len - COMPRESS_MAGIC_LEN); zs.zalloc = 0; zs.zfree = 0; if ((e = inflateInit(&zs))) return 0; while (0 == (e = inflate(&zs, Z_NO_FLUSH))) if (zs.avail_out < 10) { char *tmp; int used; if (!(tmp = malloc(msize + inc))) goto bomb; used = msize - zs.avail_out; memcpy(tmp, bodbuf, used); zs.next_out = (unsigned char *) tmp + used; msize += inc; zs.avail_out += inc; free(bodbuf); bodbuf = tmp; } inflateEnd(&zs); if (Z_STREAM_END == e #if 0 /* I have this problem; apparently I'm the only one who does */ || Z_DATA_ERROR == e #endif ) { *len = zs.next_out - (unsigned char *) bodbuf; *artbod = bodbuf; bodbuf[*len] = '\0'; return 0; } bomb: free(bodbuf); bodbuf = NULL; return -1; } #endif /* USE_ZLIB */