/* * Network news transport protocol (NNTP) file server. * * Unfortunately, the file system differs from that expected * by Charles Forsyth's rin news reader. This is partially out * of my own laziness, but it makes the bookkeeping here * a lot easier. */ #include #include #include #include #include #include #include <9p.h> typedef struct Netbuf Netbuf; typedef struct Group Group; struct Netbuf { Biobuf br; Biobuf bw; int lineno; int fd; int code; /* last response code */ int auth; /* Authorization required? */ char response[128]; /* last response */ Group *currentgroup; char *addr; char *user; char *pass; ulong extended; /* supported extensions */ }; struct Group { char *name; Group *parent; Group **kid; int num; int nkid; int lo, hi; int canpost; int isgroup; /* might just be piece of hierarchy */ ulong mtime; ulong atime; }; /* * First eight fields are, in order: * article number, subject, author, date, message-ID, * references, byte count, line count * We don't support OVERVIEW.FMT; when I see a server with more * interesting fields, I'll implement support then. In the meantime, * the standard defines the first eight fields. */ /* Extensions */ enum { Nxover = (1<<0), Nxhdr = (1<<1), Nxpat = (1<<2), Nxlistgp = (1<<3), }; Group *root; Netbuf *net; ulong now; int netdebug; int readonly; void* erealloc(void *v, ulong n) { v = realloc(v, n); if(v == nil) sysfatal("out of memory reallocating %lud", n); setmalloctag(v, getcallerpc(&v)); return v; } void* emalloc(ulong n) { void *v; v = malloc(n); if(v == nil) sysfatal("out of memory allocating %lud", n); memset(v, 0, n); setmalloctag(v, getcallerpc(&n)); return v; } char* estrdup(char *s) { int l; char *t; if (s == nil) return nil; l = strlen(s)+1; t = emalloc(l); memcpy(t, s, l); setmalloctag(t, getcallerpc(&s)); return t; } char* estrdupn(char *s, int n) { int l; char *t; l = strlen(s); if(l > n) l = n; t = emalloc(l+1); memmove(t, s, l); t[l] = '\0'; setmalloctag(t, getcallerpc(&s)); return t; } char* Nrdline(Netbuf *n) { char *p; int l; n->lineno++; Bflush(&n->bw); if((p = Brdline(&n->br, '\n')) == nil){ werrstr("nntp eof"); return nil; } p[l=Blinelen(&n->br)-1] = '\0'; if(l > 0 && p[l-1] == '\r') p[l-1] = '\0'; if(netdebug) fprint(2, "-> %s\n", p); return p; } int nntpresponse(Netbuf *n, int e, char *cmd) { int r; char *p; for(;;){ p = Nrdline(n); if(p==nil){ strcpy(n->response, "early nntp eof"); return -1; } r = atoi(p); if(r/100 == 1){ /* BUG? */ fprint(2, "%s\n", p); continue; } break; } strecpy(n->response, n->response+sizeof(n->response), p); if((r=atoi(p)) == 0){ close(n->fd); n->fd = -1; fprint(2, "bad nntp response: %s\n", p); werrstr("bad nntp response"); return -1; } n->code = r; if(0 < e && e<10 && r/100 != e){ fprint(2, "%s: expected %dxx: got %s\n", cmd, e, n->response); return -1; } if(10 <= e && e<100 && r/10 != e){ fprint(2, "%s: expected %dx: got %s\n", cmd, e, n->response); return -1; } if(100 <= e && r != e){ fprint(2, "%s: expected %d: got %s\n", cmd, e, n->response); return -1; } return r; } int nntpauth(Netbuf*); int nntpxcmdprobe(Netbuf*); int nntpcurrentgroup(Netbuf*, Group*); /* XXX: bug OVER/XOVER et al. */ static struct { ulong n; char *s; } extensions [] = { { Nxover, "OVER" }, { Nxhdr, "HDR" }, { Nxpat, "PAT" }, { Nxlistgp, "LISTGROUP" }, { 0, nil } }; static int indial; int nntpconnect(Netbuf *n) { n->currentgroup = nil; close(n->fd); if((n->fd = dial(n->addr, nil, nil, nil)) < 0){ snprint(n->response, sizeof n->response, "dial: %r"); return -1; } Binit(&n->br, n->fd, OREAD); Binit(&n->bw, n->fd, OWRITE); if(nntpresponse(n, 20, "greeting") < 0) return -1; readonly = (n->code == 201); indial = 1; if(n->auth != 0) nntpauth(n); // nntpxcmdprobe(n); indial = 0; return 0; } int nntpcmd(Netbuf *n, char *cmd, int e) { int tried; tried = 0; for(;;){ if(netdebug) fprint(2, "<- %s\n", cmd); Bprint(&n->bw, "%s\r\n", cmd); if(nntpresponse(n, e, cmd)>=0 && (e < 0 || n->code/100 != 5)) return 0; /* redial */ if(indial || tried++ || nntpconnect(n) < 0) return -1; } } int nntpauth(Netbuf *n) { char cmd[256]; snprint(cmd, sizeof cmd, "AUTHINFO USER %s", n->user); if (nntpcmd(n, cmd, -1) < 0 || n->code != 381) { fprint(2, "Authentication failed: %s\n", n->response); return -1; } snprint(cmd, sizeof cmd, "AUTHINFO PASS %s", n->pass); if (nntpcmd(n, cmd, -1) < 0 || n->code != 281) { fprint(2, "Authentication failed: %s\n", n->response); return -1; } return 0; } int nntpxcmdprobe(Netbuf *n) { int i; char *p; n->extended = 0; if (nntpcmd(n, "LIST EXTENSIONS", 0) < 0 || n->code != 202) return 0; while((p = Nrdline(n)) != nil) { if (strcmp(p, ".") == 0) break; for(i=0; extensions[i].s != nil; i++) if (cistrcmp(extensions[i].s, p) == 0) { n->extended |= extensions[i].n; break; } } return 0; } /* XXX: searching, lazy evaluation */ static int overcmp(void *v1, void *v2) { int a, b; a = atoi(*(char**)v1); b = atoi(*(char**)v2); if(a < b) return -1; else if(a > b) return 1; return 0; } enum { XoverChunk = 100, }; char *xover[XoverChunk]; int xoverlo; int xoverhi; int xovercount; Group *xovergroup; char* nntpover(Netbuf *n, Group *g, int m) { int i, lo, hi, mid, msg; char *p; char cmd[64]; if (g->isgroup == 0) /* BUG: should check extension capabilities */ return nil; if(g != xovergroup || m < xoverlo || m >= xoverhi){ lo = (m/XoverChunk)*XoverChunk; hi = lo+XoverChunk; if(lo < g->lo) lo = g->lo; else if (lo > g->hi) lo = g->hi; if(hi < lo || hi > g->hi) hi = g->hi; if(nntpcurrentgroup(n, g) < 0) return nil; if(lo == hi) snprint(cmd, sizeof cmd, "XOVER %d", hi); else snprint(cmd, sizeof cmd, "XOVER %d-%d", lo, hi-1); if(nntpcmd(n, cmd, 224) < 0) return nil; for(i=0; (p = Nrdline(n)) != nil; i++) { if(strcmp(p, ".") == 0) break; if(i >= XoverChunk) sysfatal("news server doesn't play by the rules"); free(xover[i]); xover[i] = emalloc(strlen(p)+2); strcpy(xover[i], p); strcat(xover[i], "\n"); } qsort(xover, i, sizeof(xover[0]), overcmp); xovercount = i; xovergroup = g; xoverlo = lo; xoverhi = hi; } lo = 0; hi = xovercount; /* search for message */ while(lo < hi){ mid = (lo+hi)/2; msg = atoi(xover[mid]); if(m == msg) return xover[mid]; else if(m < msg) hi = mid; else lo = mid+1; } return nil; } /* * Return the new Group structure for the group name. * Destroys name. */ static int printgroup(char*,Group*); Group* findgroup(Group *g, char *name, int mk) { int lo, hi, m; char *p, *q; static int ngroup; for(p=name; *p; p=q){ if(q = strchr(p, '.')) *q++ = '\0'; else q = p+strlen(p); lo = 0; hi = g->nkid; while(hi-lo > 1){ m = (lo+hi)/2; if(strcmp(p, g->kid[m]->name) < 0) hi = m; else lo = m; } assert(lo==hi || lo==hi-1); if(lo==hi || strcmp(p, g->kid[lo]->name) != 0){ if(mk==0) return nil; if(g->nkid%16 == 0) g->kid = erealloc(g->kid, (g->nkid+16)*sizeof(g->kid[0])); /* * if we're down to a single place 'twixt lo and hi, the insertion might need * to go at lo or at hi. strcmp to find out. the list needs to stay sorted. */ if(lo==hi-1 && strcmp(p, g->kid[lo]->name) < 0) hi = lo; if(hi < g->nkid) memmove(g->kid+hi+1, g->kid+hi, sizeof(g->kid[0])*(g->nkid-hi)); g->nkid++; g->kid[hi] = emalloc(sizeof(*g)); g->kid[hi]->parent = g; g = g->kid[hi]; g->name = estrdup(p); g->num = ++ngroup; g->mtime = time(0); }else g = g->kid[lo]; } if(mk) g->isgroup = 1; return g; } static int printgroup(char *s, Group *g) { if(g->parent == g) return 0; if(printgroup(s, g->parent)) strcat(s, "."); strcat(s, g->name); return 1; } static char* Nreaddata(Netbuf *n) { char *p, *q; int l; p = nil; l = 0; for(;;){ q = Nrdline(n); if(q==nil){ free(p); return nil; } if(strcmp(q, ".")==0) return p; if(q[0]=='.') q++; p = erealloc(p, l+strlen(q)+1+1); strcpy(p+l, q); strcat(p+l, "\n"); l += strlen(p+l); } } /* * Return the output of a HEAD, BODY, or ARTICLE command. */ char* nntpget(Netbuf *n, Group *g, int msg, char *retr) { char *s; char cmd[1024]; if(g->isgroup == 0){ werrstr("not a group"); return nil; } if(strcmp(retr, "XOVER") == 0){ s = nntpover(n, g, msg); if(s == nil) s = ""; return estrdup(s); } if(nntpcurrentgroup(n, g) < 0) return nil; sprint(cmd, "%s %d", retr, msg); nntpcmd(n, cmd, 0); if(n->code/10 != 22) return nil; return Nreaddata(n); } int nntpcurrentgroup(Netbuf *n, Group *g) { char cmd[1024]; if(n->currentgroup != g){ strcpy(cmd, "GROUP "); printgroup(cmd, g); if(nntpcmd(n, cmd, 21) < 0) return -1; n->currentgroup = g; } return 0; } void nntprefreshall(Netbuf *n) { char *f[10], *p; int hi, lo, nf; Group *g; if(nntpcmd(n, "LIST", 21) < 0) return; while(p = Nrdline(n)){ if(strcmp(p, ".")==0) break; nf = getfields(p, f, nelem(f), 1, "\t\r\n "); if(nf != 4){ int i; for(i=0; ilineno); return; } g = findgroup(root, f[0], 1); hi = strtol(f[1], 0, 10)+1; lo = strtol(f[2], 0, 10); if(g->hi != hi){ g->hi = hi; if(g->lo==0) g->lo = lo; g->canpost = f[3][0] == 'y'; g->mtime = time(0); } } } void nntprefresh(Netbuf *n, Group *g) { char cmd[1024]; char *f[5]; int lo, hi; if(g->isgroup==0) return; if(time(0) - g->atime < 30) return; strcpy(cmd, "GROUP "); printgroup(cmd, g); if(nntpcmd(n, cmd, 21) < 0){ n->currentgroup = nil; return; } n->currentgroup = g; if(tokenize(n->response, f, nelem(f)) < 4){ fprint(2, "error reading GROUP response"); return; } /* backwards from LIST! */ hi = strtol(f[3], 0, 10)+1; lo = strtol(f[2], 0, 10); if(g->hi != hi){ g->mtime = time(0); if(g->lo==0) g->lo = lo; g->hi = hi; } g->atime = time(0); } char* nntppost(Netbuf *n, char *msg) { char *p, *q; if(nntpcmd(n, "POST", 34) < 0) return n->response; for(p=msg; *p; p=q){ if(q = strchr(p, '\n')) *q++ = '\0'; else q = p+strlen(p); if(p[0]=='.') Bputc(&n->bw, '.'); Bwrite(&n->bw, p, strlen(p)); Bputc(&n->bw, '\r'); Bputc(&n->bw, '\n'); } Bprint(&n->bw, ".\r\n"); if(nntpresponse(n, 0, nil) < 0) return n->response; if(n->code/100 != 2) return n->response; return nil; } /* * Because an expanded QID space makes thngs much easier, * we sleazily use the version part of the QID as more path bits. * Since we make sure not to mount ourselves cached, this * doesn't break anything (unless you want to bind on top of * things in this file system). In the next version of 9P, we'll * have more QID bits to play with. * * The newsgroup is encoded in the top 15 bits * of the path. The message number is the bottom 17 bits. * The file within the message directory is in the version [sic]. */ enum { /* file qids */ Qhead, Qbody, Qarticle, Qxover, Nfile, }; char *filename[] = { "header", "body", "article", "xover", }; char *nntpname[] = { "HEAD", "BODY", "ARTICLE", "XOVER", }; #define GROUP(p) (((p)>>17)&0x3FFF) #define MESSAGE(p) ((p)&0x1FFFF) #define FILE(v) ((v)&0x3) #define PATH(g,m) ((((g)&0x3FFF)<<17)|((m)&0x1FFFF)) #define POST(g) PATH(0,g,0) #define VERS(f) ((f)&0x3) typedef struct Aux Aux; struct Aux { Group *g; int n; int ispost; int file; char *s; int ns; int offset; }; static void fsattach(Req *r) { Aux *a; char *spec; spec = r->ifcall.aname; if(spec && spec[0]){ respond(r, "invalid attach specifier"); return; } a = emalloc(sizeof *a); a->g = root; a->n = -1; r->fid->aux = a; r->ofcall.qid = (Qid){0, 0, QTDIR}; r->fid->qid = r->ofcall.qid; respond(r, nil); } static char* fsclone(Fid *ofid, Fid *fid) { Aux *a; a = emalloc(sizeof(*a)); *a = *(Aux*)ofid->aux; fid->aux = a; return nil; } static char* fswalk1(Fid *fid, char *name, Qid *qid) { char *p; int i, isdotdot, n; Aux *a; Group *ng; isdotdot = strcmp(name, "..")==0; a = fid->aux; if(a->s) /* file */ return "protocol botch"; if(a->n != -1){ if(isdotdot){ *qid = (Qid){PATH(a->g->num, 0), 0, QTDIR}; fid->qid = *qid; a->n = -1; return nil; } for(i=0; is = nntpget(net, a->g, a->n, nntpname[i])){ *qid = (Qid){PATH(a->g->num, a->n), Qbody, 0}; fid->qid = *qid; a->file = i; return nil; }else return "file does not exist"; } } return "file does not exist"; } if(isdotdot){ a->g = a->g->parent; *qid = (Qid){PATH(a->g->num, 0), 0, QTDIR}; fid->qid = *qid; return nil; } if(a->g->isgroup && !readonly && a->g->canpost && strcmp(name, "post")==0){ a->ispost = 1; *qid = (Qid){PATH(a->g->num, 0), 0, 0}; fid->qid = *qid; return nil; } if(ng = findgroup(a->g, name, 0)){ a->g = ng; *qid = (Qid){PATH(a->g->num, 0), 0, QTDIR}; fid->qid = *qid; return nil; } n = strtoul(name, &p, 0); if('0'<=name[0] && name[0]<='9' && *p=='\0' && a->g->lo<=n && ng->hi){ a->n = n; *qid = (Qid){PATH(a->g->num, n+1-a->g->lo), 0, QTDIR}; fid->qid = *qid; return nil; } return "file does not exist"; } static void fsopen(Req *r) { Aux *a; a = r->fid->aux; if((a->ispost && (r->ifcall.mode&~OTRUNC) != OWRITE) || (!a->ispost && r->ifcall.mode != OREAD)) respond(r, "permission denied"); else respond(r, nil); } static void fillstat(Dir *d, Aux *a) { char buf[32]; Group *g; memset(d, 0, sizeof *d); d->uid = estrdup("nntp"); d->gid = estrdup("nntp"); g = a->g; d->atime = d->mtime = g->mtime; if(a->ispost){ d->name = estrdup("post"); d->mode = 0222; d->qid = (Qid){PATH(g->num, 0), 0, 0}; d->length = a->ns; return; } if(a->s){ /* article file */ d->name = estrdup(filename[a->file]); d->mode = 0444; d->qid = (Qid){PATH(g->num, a->n+1-g->lo), a->file, 0}; return; } if(a->n != -1){ /* article directory */ sprint(buf, "%d", a->n); d->name = estrdup(buf); d->mode = DMDIR|0555; d->qid = (Qid){PATH(g->num, a->n+1-g->lo), 0, QTDIR}; return; } /* group directory */ if(g->name[0]) d->name = estrdup(g->name); else d->name = estrdup("/"); d->mode = DMDIR|0555; d->qid = (Qid){PATH(g->num, 0), g->hi-1, QTDIR}; } static int dirfillstat(Dir *d, Aux *a, int i) { int ndir; Group *g; char buf[32]; memset(d, 0, sizeof *d); d->uid = estrdup("nntp"); d->gid = estrdup("nntp"); g = a->g; d->atime = d->mtime = g->mtime; if(a->n != -1){ /* article directory */ if(i >= Nfile) return -1; d->name = estrdup(filename[i]); d->mode = 0444; d->qid = (Qid){PATH(g->num, a->n), i, 0}; return 0; } /* hierarchy directory: child groups */ if(i < g->nkid){ d->name = estrdup(g->kid[i]->name); d->mode = DMDIR|0555; d->qid = (Qid){PATH(g->kid[i]->num, 0), g->kid[i]->hi-1, QTDIR}; return 0; } i -= g->nkid; /* group directory: post file */ if(g->isgroup && !readonly && g->canpost){ if(i < 1){ d->name = estrdup("post"); d->mode = 0222; d->qid = (Qid){PATH(g->num, 0), 0, 0}; return 0; } i--; } /* group directory: child articles */ ndir = g->hi - g->lo; if(i < ndir){ sprint(buf, "%d", g->lo+i); d->name = estrdup(buf); d->mode = DMDIR|0555; d->qid = (Qid){PATH(g->num, i+1), 0, QTDIR}; return 0; } return -1; } static void fsstat(Req *r) { Aux *a; a = r->fid->aux; if(r->fid->qid.path == 0 && (r->fid->qid.type & QTDIR)) nntprefreshall(net); else if(a->g->isgroup) nntprefresh(net, a->g); fillstat(&r->d, a); respond(r, nil); } static void fsread(Req *r) { int offset, n; Aux *a; char *p, *ep; Dir d; a = r->fid->aux; if(a->s){ readstr(r, a->s); respond(r, nil); return; } if(r->ifcall.offset == 0) offset = 0; else offset = a->offset; p = r->ofcall.data; ep = r->ofcall.data+r->ifcall.count; for(; p+2 < ep; p += n){ if(dirfillstat(&d, a, offset) < 0) break; n=convD2M(&d, (uchar*)p, ep-p); free(d.name); free(d.uid); free(d.gid); free(d.muid); if(n <= BIT16SZ) break; offset++; } a->offset = offset; r->ofcall.count = p - r->ofcall.data; respond(r, nil); } static void fswrite(Req *r) { Aux *a; long count; vlong offset; a = r->fid->aux; if(r->ifcall.count == 0){ /* commit */ respond(r, nntppost(net, a->s)); free(a->s); a->ns = 0; a->s = nil; return; } count = r->ifcall.count; offset = r->ifcall.offset; if(a->ns < count+offset+1){ a->s = erealloc(a->s, count+offset+1); a->ns = count+offset; a->s[a->ns] = '\0'; } memmove(a->s+offset, r->ifcall.data, count); r->ofcall.count = count; respond(r, nil); } static void fsdestroyfid(Fid *fid) { Aux *a; a = fid->aux; if(a==nil) return; if(a->ispost && a->s) nntppost(net, a->s); free(a->s); free(a); } Srv nntpsrv = { .destroyfid= fsdestroyfid, .attach= fsattach, .clone= fsclone, .walk1= fswalk1, .open= fsopen, .read= fsread, .write= fswrite, .stat= fsstat, }; void usage(void) { fprint(2, "usage: nntpsrv [-a] [-s service] [-m mtpt] [nntp.server]\n"); exits("usage"); } void dumpgroups(Group *g, int ind) { int i; print("%*s%s\n", ind*4, "", g->name); for(i=0; inkid; i++) dumpgroups(g->kid[i], ind+1); } void main(int argc, char **argv) { int auth, x; char *mtpt, *service, *where, *user; Netbuf n; UserPasswd *up; mtpt = "/mnt/news"; service = nil; memset(&n, 0, sizeof n); user = nil; auth = 0; ARGBEGIN{ case 'D': chatty9p++; break; case 'N': netdebug = 1; break; case 'a': auth = 1; break; case 'u': user = EARGF(usage()); break; case 's': service = EARGF(usage()); break; case 'm': mtpt = EARGF(usage()); break; default: usage(); }ARGEND if(argc > 1) usage(); if(argc==0) where = "$nntp"; else where = argv[0]; now = time(0); net = &n; if(auth) { n.auth = 1; if(user) up = auth_getuserpasswd(auth_getkey, "proto=pass service=nntp server=%q user=%q", where, user); else up = auth_getuserpasswd(auth_getkey, "proto=pass service=nntp server=%q", where); if(up == nil) sysfatal("no password: %r"); n.user = up->user; n.pass = up->passwd; } n.addr = netmkaddr(where, "tcp", "nntp"); root = emalloc(sizeof *root); root->name = estrdup(""); root->parent = root; n.fd = -1; if(nntpconnect(&n) < 0) sysfatal("nntpconnect: %s", n.response); x=netdebug; netdebug=0; nntprefreshall(&n); netdebug=x; // dumpgroups(root, 0); postmountsrv(&nntpsrv, service, mtpt, MREPL); exits(nil); }