/*
* 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 <u.h>
#include <libc.h>
#include <bio.h>
#include <auth.h>
#include <fcall.h>
#include <thread.h>
#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 %s: %r", n->addr);
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; i<nf; i++)
fprint(2, "%s%s", i?" ":"", f[i]);
fprint(2, "\n");
fprint(2, "syntax error in group list, line %d", n->lineno);
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; i<Nfile; i++){
if(strcmp(name, filename[i])==0){
if(a->s = 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 && n<a->g->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; i<g->nkid; 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);
}
|