2 * Network news transport protocol (NNTP) file server.
4 * Unfortunately, the file system differs from that expected
5 * by Charles Forsyth's rin news reader. This is partially out
6 * of my own laziness, but it makes the bookkeeping here
18 typedef struct Netbuf Netbuf;
19 typedef struct Group Group;
26 int code; /* last response code */
27 int auth; /* Authorization required? */
28 char response[128]; /* last response */
33 ulong extended; /* supported extensions */
44 int isgroup; /* might just be piece of hierarchy */
50 * First eight fields are, in order:
51 * article number, subject, author, date, message-ID,
52 * references, byte count, line count
53 * We don't support OVERVIEW.FMT; when I see a server with more
54 * interesting fields, I'll implement support then. In the meantime,
55 * the standard defines the first eight fields.
73 erealloc(void *v, ulong n)
77 sysfatal("out of memory reallocating %lud", n);
78 setmalloctag(v, getcallerpc(&v));
89 sysfatal("out of memory allocating %lud", n);
91 setmalloctag(v, getcallerpc(&n));
106 setmalloctag(t, getcallerpc(&s));
111 estrdupn(char *s, int n)
122 setmalloctag(t, getcallerpc(&s));
134 if((p = Brdline(&n->br, '\n')) == nil){
138 p[l=Blinelen(&n->br)-1] = '\0';
139 if(l > 0 && p[l-1] == '\r')
142 fprint(2, "-> %s\n", p);
147 nntpresponse(Netbuf *n, int e, char *cmd)
155 strcpy(n->response, "early nntp eof");
159 if(r/100 == 1){ /* BUG? */
160 fprint(2, "%s\n", p);
166 strecpy(n->response, n->response+sizeof(n->response), p);
168 if((r=atoi(p)) == 0){
171 fprint(2, "bad nntp response: %s\n", p);
172 werrstr("bad nntp response");
177 if(0 < e && e<10 && r/100 != e){
178 fprint(2, "%s: expected %dxx: got %s\n", cmd, e, n->response);
181 if(10 <= e && e<100 && r/10 != e){
182 fprint(2, "%s: expected %dx: got %s\n", cmd, e, n->response);
185 if(100 <= e && r != e){
186 fprint(2, "%s: expected %d: got %s\n", cmd, e, n->response);
192 int nntpauth(Netbuf*);
193 int nntpxcmdprobe(Netbuf*);
194 int nntpcurrentgroup(Netbuf*, Group*);
196 /* XXX: bug OVER/XOVER et al. */
204 { Nxlistgp, "LISTGROUP" },
211 nntpconnect(Netbuf *n)
213 n->currentgroup = nil;
215 if((n->fd = dial(n->addr, nil, nil, nil)) < 0){
216 snprint(n->response, sizeof n->response, "dial: %r");
219 Binit(&n->br, n->fd, OREAD);
220 Binit(&n->bw, n->fd, OWRITE);
221 if(nntpresponse(n, 20, "greeting") < 0)
223 readonly = (n->code == 201);
234 nntpcmd(Netbuf *n, char *cmd, int e)
241 fprint(2, "<- %s\n", cmd);
242 Bprint(&n->bw, "%s\r\n", cmd);
243 if(nntpresponse(n, e, cmd)>=0 && (e < 0 || n->code/100 != 5))
247 if(indial || tried++ || nntpconnect(n) < 0)
257 snprint(cmd, sizeof cmd, "AUTHINFO USER %s", n->user);
258 if (nntpcmd(n, cmd, -1) < 0 || n->code != 381) {
259 fprint(2, "Authentication failed: %s\n", n->response);
263 snprint(cmd, sizeof cmd, "AUTHINFO PASS %s", n->pass);
264 if (nntpcmd(n, cmd, -1) < 0 || n->code != 281) {
265 fprint(2, "Authentication failed: %s\n", n->response);
273 nntpxcmdprobe(Netbuf *n)
279 if (nntpcmd(n, "LIST EXTENSIONS", 0) < 0 || n->code != 202)
282 while((p = Nrdline(n)) != nil) {
283 if (strcmp(p, ".") == 0)
286 for(i=0; extensions[i].s != nil; i++)
287 if (cistrcmp(extensions[i].s, p) == 0) {
288 n->extended |= extensions[i].n;
295 /* XXX: searching, lazy evaluation */
297 overcmp(void *v1, void *v2)
301 a = atoi(*(char**)v1);
302 b = atoi(*(char**)v2);
315 char *xover[XoverChunk];
322 nntpover(Netbuf *n, Group *g, int m)
324 int i, lo, hi, mid, msg;
328 if (g->isgroup == 0) /* BUG: should check extension capabilities */
331 if(g != xovergroup || m < xoverlo || m >= xoverhi){
332 lo = (m/XoverChunk)*XoverChunk;
339 if(hi < lo || hi > g->hi)
342 if(nntpcurrentgroup(n, g) < 0)
346 snprint(cmd, sizeof cmd, "XOVER %d", hi);
348 snprint(cmd, sizeof cmd, "XOVER %d-%d", lo, hi-1);
349 if(nntpcmd(n, cmd, 224) < 0)
352 for(i=0; (p = Nrdline(n)) != nil; i++) {
353 if(strcmp(p, ".") == 0)
356 sysfatal("news server doesn't play by the rules");
358 xover[i] = emalloc(strlen(p)+2);
360 strcat(xover[i], "\n");
362 qsort(xover, i, sizeof(xover[0]), overcmp);
373 /* search for message */
376 msg = atoi(xover[mid]);
388 * Return the new Group structure for the group name.
391 static int printgroup(char*,Group*);
393 findgroup(Group *g, char *name, int mk)
399 for(p=name; *p; p=q){
400 if(q = strchr(p, '.'))
409 if(strcmp(p, g->kid[m]->name) < 0)
414 assert(lo==hi || lo==hi-1);
415 if(lo==hi || strcmp(p, g->kid[lo]->name) != 0){
419 g->kid = erealloc(g->kid, (g->nkid+16)*sizeof(g->kid[0]));
422 * if we're down to a single place 'twixt lo and hi, the insertion might need
423 * to go at lo or at hi. strcmp to find out. the list needs to stay sorted.
425 if(lo==hi-1 && strcmp(p, g->kid[lo]->name) < 0)
429 memmove(g->kid+hi+1, g->kid+hi, sizeof(g->kid[0])*(g->nkid-hi));
431 g->kid[hi] = emalloc(sizeof(*g));
432 g->kid[hi]->parent = g;
434 g->name = estrdup(p);
446 printgroup(char *s, Group *g)
451 if(printgroup(s, g->parent))
471 if(strcmp(q, ".")==0)
475 p = erealloc(p, l+strlen(q)+1+1);
483 * Return the output of a HEAD, BODY, or ARTICLE command.
486 nntpget(Netbuf *n, Group *g, int msg, char *retr)
492 werrstr("not a group");
496 if(strcmp(retr, "XOVER") == 0){
497 s = nntpover(n, g, msg);
503 if(nntpcurrentgroup(n, g) < 0)
505 sprint(cmd, "%s %d", retr, msg);
514 nntpcurrentgroup(Netbuf *n, Group *g)
518 if(n->currentgroup != g){
519 strcpy(cmd, "GROUP ");
521 if(nntpcmd(n, cmd, 21) < 0)
529 nntprefreshall(Netbuf *n)
535 if(nntpcmd(n, "LIST", 21) < 0)
538 while(p = Nrdline(n)){
539 if(strcmp(p, ".")==0)
542 nf = getfields(p, f, nelem(f), 1, "\t\r\n ");
546 fprint(2, "%s%s", i?" ":"", f[i]);
548 fprint(2, "syntax error in group list, line %d", n->lineno);
551 g = findgroup(root, f[0], 1);
552 hi = strtol(f[1], 0, 10)+1;
553 lo = strtol(f[2], 0, 10);
558 g->canpost = f[3][0] == 'y';
565 nntprefresh(Netbuf *n, Group *g)
574 if(time(0) - g->atime < 30)
577 strcpy(cmd, "GROUP ");
579 if(nntpcmd(n, cmd, 21) < 0){
580 n->currentgroup = nil;
585 if(tokenize(n->response, f, nelem(f)) < 4){
586 fprint(2, "error reading GROUP response");
590 /* backwards from LIST! */
591 hi = strtol(f[3], 0, 10)+1;
592 lo = strtol(f[2], 0, 10);
603 nntppost(Netbuf *n, char *msg)
607 if(nntpcmd(n, "POST", 34) < 0)
611 if(q = strchr(p, '\n'))
618 Bwrite(&n->bw, p, strlen(p));
622 Bprint(&n->bw, ".\r\n");
624 if(nntpresponse(n, 0, nil) < 0)
633 * Because an expanded QID space makes thngs much easier,
634 * we sleazily use the version part of the QID as more path bits.
635 * Since we make sure not to mount ourselves cached, this
636 * doesn't break anything (unless you want to bind on top of
637 * things in this file system). In the next version of 9P, we'll
638 * have more QID bits to play with.
640 * The newsgroup is encoded in the top 15 bits
641 * of the path. The message number is the bottom 17 bits.
642 * The file within the message directory is in the version [sic].
645 enum { /* file qids */
665 #define GROUP(p) (((p)>>17)&0x3FFF)
666 #define MESSAGE(p) ((p)&0x1FFFF)
667 #define FILE(v) ((v)&0x3)
669 #define PATH(g,m) ((((g)&0x3FFF)<<17)|((m)&0x1FFFF))
670 #define POST(g) PATH(0,g,0)
671 #define VERS(f) ((f)&0x3)
673 typedef struct Aux Aux;
690 spec = r->ifcall.aname;
692 respond(r, "invalid attach specifier");
696 a = emalloc(sizeof *a);
701 r->ofcall.qid = (Qid){0, 0, QTDIR};
702 r->fid->qid = r->ofcall.qid;
707 fsclone(Fid *ofid, Fid *fid)
711 a = emalloc(sizeof(*a));
712 *a = *(Aux*)ofid->aux;
718 fswalk1(Fid *fid, char *name, Qid *qid)
725 isdotdot = strcmp(name, "..")==0;
729 return "protocol botch";
732 *qid = (Qid){PATH(a->g->num, 0), 0, QTDIR};
737 for(i=0; i<Nfile; i++){
738 if(strcmp(name, filename[i])==0){
739 if(a->s = nntpget(net, a->g, a->n, nntpname[i])){
740 *qid = (Qid){PATH(a->g->num, a->n), Qbody, 0};
745 return "file does not exist";
748 return "file does not exist";
753 *qid = (Qid){PATH(a->g->num, 0), 0, QTDIR};
758 if(a->g->isgroup && !readonly && a->g->canpost
759 && strcmp(name, "post")==0){
761 *qid = (Qid){PATH(a->g->num, 0), 0, 0};
766 if(ng = findgroup(a->g, name, 0)){
768 *qid = (Qid){PATH(a->g->num, 0), 0, QTDIR};
773 n = strtoul(name, &p, 0);
774 if('0'<=name[0] && name[0]<='9' && *p=='\0' && a->g->lo<=n && n<a->g->hi){
776 *qid = (Qid){PATH(a->g->num, n+1-a->g->lo), 0, QTDIR};
781 return "file does not exist";
790 if((a->ispost && (r->ifcall.mode&~OTRUNC) != OWRITE)
791 || (!a->ispost && r->ifcall.mode != OREAD))
792 respond(r, "permission denied");
798 fillstat(Dir *d, Aux *a)
803 memset(d, 0, sizeof *d);
804 d->uid = estrdup("nntp");
805 d->gid = estrdup("nntp");
807 d->atime = d->mtime = g->mtime;
810 d->name = estrdup("post");
812 d->qid = (Qid){PATH(g->num, 0), 0, 0};
817 if(a->s){ /* article file */
818 d->name = estrdup(filename[a->file]);
820 d->qid = (Qid){PATH(g->num, a->n+1-g->lo), a->file, 0};
824 if(a->n != -1){ /* article directory */
825 sprint(buf, "%d", a->n);
826 d->name = estrdup(buf);
827 d->mode = DMDIR|0555;
828 d->qid = (Qid){PATH(g->num, a->n+1-g->lo), 0, QTDIR};
832 /* group directory */
834 d->name = estrdup(g->name);
836 d->name = estrdup("/");
837 d->mode = DMDIR|0555;
838 d->qid = (Qid){PATH(g->num, 0), g->hi-1, QTDIR};
842 dirfillstat(Dir *d, Aux *a, int i)
848 memset(d, 0, sizeof *d);
849 d->uid = estrdup("nntp");
850 d->gid = estrdup("nntp");
853 d->atime = d->mtime = g->mtime;
855 if(a->n != -1){ /* article directory */
859 d->name = estrdup(filename[i]);
861 d->qid = (Qid){PATH(g->num, a->n), i, 0};
865 /* hierarchy directory: child groups */
867 d->name = estrdup(g->kid[i]->name);
868 d->mode = DMDIR|0555;
869 d->qid = (Qid){PATH(g->kid[i]->num, 0), g->kid[i]->hi-1, QTDIR};
874 /* group directory: post file */
875 if(g->isgroup && !readonly && g->canpost){
877 d->name = estrdup("post");
879 d->qid = (Qid){PATH(g->num, 0), 0, 0};
885 /* group directory: child articles */
886 ndir = g->hi - g->lo;
888 sprint(buf, "%d", g->lo+i);
889 d->name = estrdup(buf);
890 d->mode = DMDIR|0555;
891 d->qid = (Qid){PATH(g->num, i+1), 0, QTDIR};
904 if(r->fid->qid.path == 0 && (r->fid->qid.type & QTDIR))
906 else if(a->g->isgroup)
907 nntprefresh(net, a->g);
927 if(r->ifcall.offset == 0)
933 ep = r->ofcall.data+r->ifcall.count;
934 for(; p+2 < ep; p += n){
935 if(dirfillstat(&d, a, offset) < 0)
937 n=convD2M(&d, (uchar*)p, ep-p);
947 r->ofcall.count = p - r->ofcall.data;
960 if(r->ifcall.count == 0){ /* commit */
961 respond(r, nntppost(net, a->s));
968 count = r->ifcall.count;
969 offset = r->ifcall.offset;
970 if(a->ns < count+offset+1){
971 a->s = erealloc(a->s, count+offset+1);
972 a->ns = count+offset;
975 memmove(a->s+offset, r->ifcall.data, count);
976 r->ofcall.count = count;
981 fsdestroyfid(Fid *fid)
989 if(a->ispost && a->s)
997 .destroyfid= fsdestroyfid,
1010 fprint(2, "usage: nntpsrv [-a] [-s service] [-m mtpt] [nntp.server]\n");
1015 dumpgroups(Group *g, int ind)
1019 print("%*s%s\n", ind*4, "", g->name);
1020 for(i=0; i<g->nkid; i++)
1021 dumpgroups(g->kid[i], ind+1);
1025 main(int argc, char **argv)
1028 char *mtpt, *service, *where, *user;
1034 memset(&n, 0, sizeof n);
1048 user = EARGF(usage());
1051 service = EARGF(usage());
1054 mtpt = EARGF(usage());
1073 up = auth_getuserpasswd(auth_getkey, "proto=pass service=nntp server=%q user=%q", where, user);
1075 up = auth_getuserpasswd(auth_getkey, "proto=pass service=nntp server=%q", where);
1077 sysfatal("no password: %r");
1080 n.pass = up->passwd;
1083 n.addr = netmkaddr(where, "tcp", "nntp");
1085 root = emalloc(sizeof *root);
1086 root->name = estrdup("");
1087 root->parent = root;
1090 if(nntpconnect(&n) < 0)
1091 sysfatal("nntpconnect: %s", n.response);
1097 // dumpgroups(root, 0);
1099 postmountsrv(&nntpsrv, service, mtpt, MREPL);