xref: /plan9/sys/src/cmd/nntpfs.c (revision 4d44ba9b9ee4246ddbd96c7fcaf0918ab92ab35a)
1 /*
2  * Network news transport protocol (NNTP) file server.
3  *
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
7  * a lot easier.
8  */
9 
10 #include <u.h>
11 #include <libc.h>
12 #include <bio.h>
13 #include <auth.h>
14 #include <fcall.h>
15 #include <thread.h>
16 #include <9p.h>
17 
18 typedef struct Netbuf Netbuf;
19 typedef struct Group Group;
20 
21 struct Netbuf {
22 	Biobuf br;
23 	Biobuf bw;
24 	int lineno;
25 	int fd;
26 	int code;			/* last response code */
27 	int auth;			/* Authorization required? */
28 	char response[128];	/* last response */
29 	Group *currentgroup;
30 	char *addr;
31 	char *user;
32 	char *pass;
33 	ulong extended;	/* supported extensions */
34 };
35 
36 struct Group {
37 	char *name;
38 	Group *parent;
39 	Group **kid;
40 	int num;
41 	int nkid;
42 	int lo, hi;
43 	int canpost;
44 	int isgroup;	/* might just be piece of hierarchy */
45 	ulong mtime;
46 	ulong atime;
47 };
48 
49 /*
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.
56  */
57 
58 /* Extensions */
59 enum {
60 	Nxover   = (1<<0),
61 	Nxhdr    = (1<<1),
62 	Nxpat    = (1<<2),
63 	Nxlistgp = (1<<3),
64 };
65 
66 Group *root;
67 Netbuf *net;
68 ulong now;
69 int netdebug;
70 int readonly;
71 
72 void*
73 erealloc(void *v, ulong n)
74 {
75 	v = realloc(v, n);
76 	if(v == nil)
77 		sysfatal("out of memory reallocating %lud", n);
78 	setmalloctag(v, getcallerpc(&v));
79 	return v;
80 }
81 
82 void*
83 emalloc(ulong n)
84 {
85 	void *v;
86 
87 	v = malloc(n);
88 	if(v == nil)
89 		sysfatal("out of memory allocating %lud", n);
90 	memset(v, 0, n);
91 	setmalloctag(v, getcallerpc(&n));
92 	return v;
93 }
94 
95 char*
96 estrdup(char *s)
97 {
98 	int l;
99 	char *t;
100 
101 	if (s == nil)
102 		return nil;
103 	l = strlen(s)+1;
104 	t = emalloc(l);
105 	memcpy(t, s, l);
106 	setmalloctag(t, getcallerpc(&s));
107 	return t;
108 }
109 
110 char*
111 estrdupn(char *s, int n)
112 {
113 	int l;
114 	char *t;
115 
116 	l = strlen(s);
117 	if(l > n)
118 		l = n;
119 	t = emalloc(l+1);
120 	memmove(t, s, l);
121 	t[l] = '\0';
122 	setmalloctag(t, getcallerpc(&s));
123 	return t;
124 }
125 
126 char*
127 Nrdline(Netbuf *n)
128 {
129 	char *p;
130 	int l;
131 
132 	n->lineno++;
133 	Bflush(&n->bw);
134 	if((p = Brdline(&n->br, '\n')) == nil){
135 		werrstr("nntp eof");
136 		return nil;
137 	}
138 	p[l=Blinelen(&n->br)-1] = '\0';
139 	if(l > 0 && p[l-1] == '\r')
140 		p[l-1] = '\0';
141 if(netdebug)
142 	fprint(2, "-> %s\n", p);
143 	return p;
144 }
145 
146 int
147 nntpresponse(Netbuf *n, int e, char *cmd)
148 {
149 	int r;
150 	char *p;
151 
152 	for(;;){
153 		p = Nrdline(n);
154 		if(p==nil){
155 			strcpy(n->response, "early nntp eof");
156 			return -1;
157 		}
158 		r = atoi(p);
159 		if(r/100 == 1){	/* BUG? */
160 			fprint(2, "%s\n", p);
161 			continue;
162 		}
163 		break;
164 	}
165 
166 	strecpy(n->response, n->response+sizeof(n->response), p);
167 
168 	if((r=atoi(p)) == 0){
169 		close(n->fd);
170 		n->fd = -1;
171 		fprint(2, "bad nntp response: %s\n", p);
172 		werrstr("bad nntp response");
173 		return -1;
174 	}
175 
176 	n->code = r;
177 	if(0 < e && e<10 && r/100 != e){
178 		fprint(2, "%s: expected %dxx: got %s\n", cmd, e, n->response);
179 		return -1;
180 	}
181 	if(10 <= e && e<100 && r/10 != e){
182 		fprint(2, "%s: expected %dx: got %s\n", cmd, e, n->response);
183 		return -1;
184 	}
185 	if(100 <= e && r != e){
186 		fprint(2, "%s: expected %d: got %s\n", cmd, e, n->response);
187 		return -1;
188 	}
189 	return r;
190 }
191 
192 int nntpauth(Netbuf*);
193 int nntpxcmdprobe(Netbuf*);
194 int nntpcurrentgroup(Netbuf*, Group*);
195 
196 /* XXX: bug OVER/XOVER et al. */
197 static struct {
198 	ulong n;
199 	char *s;
200 } extensions [] = {
201 	{ Nxover, "OVER" },
202 	{ Nxhdr, "HDR" },
203 	{ Nxpat, "PAT" },
204 	{ Nxlistgp, "LISTGROUP" },
205 	{ 0, nil }
206 };
207 
208 static int indial;
209 
210 int
211 nntpconnect(Netbuf *n)
212 {
213 	n->currentgroup = nil;
214 	close(n->fd);
215 	if((n->fd = dial(n->addr, nil, nil, nil)) < 0){
216 		snprint(n->response, sizeof n->response, "dial: %r");
217 		return -1;
218 	}
219 	Binit(&n->br, n->fd, OREAD);
220 	Binit(&n->bw, n->fd, OWRITE);
221 	if(nntpresponse(n, 20, "greeting") < 0)
222 		return -1;
223 	readonly = (n->code == 201);
224 
225 	indial = 1;
226 	if(n->auth != 0)
227 		nntpauth(n);
228 //	nntpxcmdprobe(n);
229 	indial = 0;
230 	return 0;
231 }
232 
233 int
234 nntpcmd(Netbuf *n, char *cmd, int e)
235 {
236 	int tried;
237 
238 	tried = 0;
239 	for(;;){
240 		if(netdebug)
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))
244 			return 0;
245 
246 		/* redial */
247 		if(indial || tried++ || nntpconnect(n) < 0)
248 			return -1;
249 	}
250 
251 	return -1;	/* shut up 8c */
252 }
253 
254 int
255 nntpauth(Netbuf *n)
256 {
257 	char cmd[256];
258 
259 	snprint(cmd, sizeof cmd, "AUTHINFO USER %s", n->user);
260 	if (nntpcmd(n, cmd, -1) < 0 || n->code != 381) {
261 		fprint(2, "Authentication failed: %s\n", n->response);
262 		return -1;
263 	}
264 
265 	snprint(cmd, sizeof cmd, "AUTHINFO PASS %s", n->pass);
266 	if (nntpcmd(n, cmd, -1) < 0 || n->code != 281) {
267 		fprint(2, "Authentication failed: %s\n", n->response);
268 		return -1;
269 	}
270 
271 	return 0;
272 }
273 
274 int
275 nntpxcmdprobe(Netbuf *n)
276 {
277 	int i;
278 	char *p;
279 
280 	n->extended = 0;
281 	if (nntpcmd(n, "LIST EXTENSIONS", 0) < 0 || n->code != 202)
282 		return 0;
283 
284 	while((p = Nrdline(n)) != nil) {
285 		if (strcmp(p, ".") == 0)
286 			break;
287 
288 		for(i=0; extensions[i].s != nil; i++)
289 			if (cistrcmp(extensions[i].s, p) == 0) {
290 				n->extended |= extensions[i].n;
291 				break;
292 			}
293 	}
294 	return 0;
295 }
296 
297 /* XXX: searching, lazy evaluation */
298 static int
299 overcmp(void *v1, void *v2)
300 {
301 	int a, b;
302 
303 	a = atoi(*(char**)v1);
304 	b = atoi(*(char**)v2);
305 
306 	if(a < b)
307 		return -1;
308 	else if(a > b)
309 		return 1;
310 	return 0;
311 }
312 
313 enum {
314 	XoverChunk = 100,
315 };
316 
317 char *xover[XoverChunk];
318 int xoverlo;
319 int xoverhi;
320 int xovercount;
321 Group *xovergroup;
322 
323 char*
324 nntpover(Netbuf *n, Group *g, int m)
325 {
326 	int i, lo, hi, mid, msg;
327 	char *p;
328 	char cmd[64];
329 
330 	if (g->isgroup == 0)	/* BUG: should check extension capabilities */
331 		return nil;
332 
333 	if(g != xovergroup || m < xoverlo || m >= xoverhi){
334 		lo = (m/XoverChunk)*XoverChunk;
335 		hi = lo+XoverChunk;
336 
337 		if(lo < g->lo)
338 			lo = g->lo;
339 		else if (lo > g->hi)
340 			lo = g->hi;
341 		if(hi < lo || hi > g->hi)
342 			hi = g->hi;
343 
344 		if(nntpcurrentgroup(n, g) < 0)
345 			return nil;
346 
347 		if(lo == hi)
348 			snprint(cmd, sizeof cmd, "XOVER %d", hi);
349 		else
350 			snprint(cmd, sizeof cmd, "XOVER %d-%d", lo, hi-1);
351 		if(nntpcmd(n, cmd, 224) < 0)
352 			return nil;
353 
354 		for(i=0; (p = Nrdline(n)) != nil; i++) {
355 			if(strcmp(p, ".") == 0)
356 				break;
357 			if(i >= XoverChunk)
358 				sysfatal("news server doesn't play by the rules");
359 			free(xover[i]);
360 			xover[i] = emalloc(strlen(p)+2);
361 			strcpy(xover[i], p);
362 			strcat(xover[i], "\n");
363 		}
364 		qsort(xover, i, sizeof(xover[0]), overcmp);
365 
366 		xovercount = i;
367 
368 		xovergroup = g;
369 		xoverlo = lo;
370 		xoverhi = hi;
371 	}
372 
373 	lo = 0;
374 	hi = xovercount;
375 	/* search for message */
376 	while(lo < hi){
377 		mid = (lo+hi)/2;
378 		msg = atoi(xover[mid]);
379 		if(m == msg)
380 			return xover[mid];
381 		else if(m < msg)
382 			hi = mid;
383 		else
384 			lo = mid+1;
385 	}
386 	return nil;
387 }
388 
389 /*
390  * Return the new Group structure for the group name.
391  * Destroys name.
392  */
393 static int printgroup(char*,Group*);
394 Group*
395 findgroup(Group *g, char *name, int mk)
396 {
397 	int lo, hi, m;
398 	char *p, *q;
399 	static int ngroup;
400 
401 	for(p=name; *p; p=q){
402 		if(q = strchr(p, '.'))
403 			*q++ = '\0';
404 		else
405 			q = p+strlen(p);
406 
407 		lo = 0;
408 		hi = g->nkid;
409 		while(hi-lo > 1){
410 			m = (lo+hi)/2;
411 			if(strcmp(p, g->kid[m]->name) < 0)
412 				hi = m;
413 			else
414 				lo = m;
415 		}
416 		assert(lo==hi || lo==hi-1);
417 		if(lo==hi || strcmp(p, g->kid[lo]->name) != 0){
418 			if(mk==0)
419 				return nil;
420 			if(g->nkid%16 == 0)
421 				g->kid = erealloc(g->kid, (g->nkid+16)*sizeof(g->kid[0]));
422 
423 			/*
424 			 * if we're down to a single place 'twixt lo and hi, the insertion might need
425 			 * to go at lo or at hi.  strcmp to find out.  the list needs to stay sorted.
426 		 	 */
427 			if(lo==hi-1 && strcmp(p, g->kid[lo]->name) < 0)
428 				hi = lo;
429 
430 			if(hi < g->nkid)
431 				memmove(g->kid+hi+1, g->kid+hi, sizeof(g->kid[0])*(g->nkid-hi));
432 			g->nkid++;
433 			g->kid[hi] = emalloc(sizeof(*g));
434 			g->kid[hi]->parent = g;
435 			g = g->kid[hi];
436 			g->name = estrdup(p);
437 			g->num = ++ngroup;
438 			g->mtime = time(0);
439 		}else
440 			g = g->kid[lo];
441 	}
442 	if(mk)
443 		g->isgroup = 1;
444 	return g;
445 }
446 
447 static int
448 printgroup(char *s, Group *g)
449 {
450 	if(g->parent == g)
451 		return 0;
452 
453 	if(printgroup(s, g->parent))
454 		strcat(s, ".");
455 	strcat(s, g->name);
456 	return 1;
457 }
458 
459 static char*
460 Nreaddata(Netbuf *n)
461 {
462 	char *p, *q;
463 	int l;
464 
465 	p = nil;
466 	l = 0;
467 	for(;;){
468 		q = Nrdline(n);
469 		if(q==nil){
470 			free(p);
471 			return nil;
472 		}
473 		if(strcmp(q, ".")==0)
474 			return p;
475 		if(q[0]=='.')
476 			q++;
477 		p = erealloc(p, l+strlen(q)+1+1);
478 		strcpy(p+l, q);
479 		strcat(p+l, "\n");
480 		l += strlen(p+l);
481 	}
482 	return nil;	/* shut up 8c */
483 }
484 
485 /*
486  * Return the output of a HEAD, BODY, or ARTICLE command.
487  */
488 char*
489 nntpget(Netbuf *n, Group *g, int msg, char *retr)
490 {
491 	char *s;
492 	char cmd[1024];
493 
494 	if(g->isgroup == 0){
495 		werrstr("not a group");
496 		return nil;
497 	}
498 
499 	if(strcmp(retr, "XOVER") == 0){
500 		s = nntpover(n, g, msg);
501 		if(s == nil)
502 			s = "";
503 		return estrdup(s);
504 	}
505 
506 	if(nntpcurrentgroup(n, g) < 0)
507 		return nil;
508 	sprint(cmd, "%s %d", retr, msg);
509 	nntpcmd(n, cmd, 0);
510 	if(n->code/10 != 22)
511 		return nil;
512 
513 	return Nreaddata(n);
514 }
515 
516 int
517 nntpcurrentgroup(Netbuf *n, Group *g)
518 {
519 	char cmd[1024];
520 
521 	if(n->currentgroup != g){
522 		strcpy(cmd, "GROUP ");
523 		printgroup(cmd, g);
524 		if(nntpcmd(n, cmd, 21) < 0)
525 			return -1;
526 		n->currentgroup = g;
527 	}
528 	return 0;
529 }
530 
531 void
532 nntprefreshall(Netbuf *n)
533 {
534 	char *f[10], *p;
535 	int hi, lo, nf;
536 	Group *g;
537 
538 	if(nntpcmd(n, "LIST", 21) < 0)
539 		return;
540 
541 	while(p = Nrdline(n)){
542 		if(strcmp(p, ".")==0)
543 			break;
544 
545 		nf = getfields(p, f, nelem(f), 1, "\t\r\n ");
546 		if(nf != 4){
547 			int i;
548 			for(i=0; i<nf; i++)
549 				fprint(2, "%s%s", i?" ":"", f[i]);
550 			fprint(2, "\n");
551 			fprint(2, "syntax error in group list, line %d", n->lineno);
552 			return;
553 		}
554 		g = findgroup(root, f[0], 1);
555 		hi = strtol(f[1], 0, 10)+1;
556 		lo = strtol(f[2], 0, 10);
557 		if(g->hi != hi){
558 			g->hi = hi;
559 			if(g->lo==0)
560 				g->lo = lo;
561 			g->canpost = f[3][0] == 'y';
562 			g->mtime = time(0);
563 		}
564 	}
565 }
566 
567 void
568 nntprefresh(Netbuf *n, Group *g)
569 {
570 	char cmd[1024];
571 	char *f[5];
572 	int lo, hi;
573 
574 	if(g->isgroup==0)
575 		return;
576 
577 	if(time(0) - g->atime < 30)
578 		return;
579 
580 	strcpy(cmd, "GROUP ");
581 	printgroup(cmd, g);
582 	if(nntpcmd(n, cmd, 21) < 0){
583 		n->currentgroup = nil;
584 		return;
585 	}
586 	n->currentgroup = g;
587 
588 	if(tokenize(n->response, f, nelem(f)) < 4){
589 		fprint(2, "error reading GROUP response");
590 		return;
591 	}
592 
593 	/* backwards from LIST! */
594 	hi = strtol(f[3], 0, 10)+1;
595 	lo = strtol(f[2], 0, 10);
596 	if(g->hi != hi){
597 		g->mtime = time(0);
598 		if(g->lo==0)
599 			g->lo = lo;
600 		g->hi = hi;
601 	}
602 	g->atime = time(0);
603 }
604 
605 char*
606 nntppost(Netbuf *n, char *msg)
607 {
608 	char *p, *q;
609 
610 	if(nntpcmd(n, "POST", 34) < 0)
611 		return n->response;
612 
613 	for(p=msg; *p; p=q){
614 		if(q = strchr(p, '\n'))
615 			*q++ = '\0';
616 		else
617 			q = p+strlen(p);
618 
619 		if(p[0]=='.')
620 			Bputc(&n->bw, '.');
621 		Bwrite(&n->bw, p, strlen(p));
622 		Bputc(&n->bw, '\r');
623 		Bputc(&n->bw, '\n');
624 	}
625 	Bprint(&n->bw, ".\r\n");
626 
627 	if(nntpresponse(n, 0, nil) < 0)
628 		return n->response;
629 
630 	if(n->code/100 != 2)
631 		return n->response;
632 	return nil;
633 }
634 
635 /*
636  * Because an expanded QID space makes thngs much easier,
637  * we sleazily use the version part of the QID as more path bits.
638  * Since we make sure not to mount ourselves cached, this
639  * doesn't break anything (unless you want to bind on top of
640  * things in this file system).  In the next version of 9P, we'll
641  * have more QID bits to play with.
642  *
643  * The newsgroup is encoded in the top 15 bits
644  * of the path.  The message number is the bottom 17 bits.
645  * The file within the message directory is in the version [sic].
646  */
647 
648 enum {	/* file qids */
649 	Qhead,
650 	Qbody,
651 	Qarticle,
652 	Qxover,
653 	Nfile,
654 };
655 char *filename[] = {
656 	"header",
657 	"body",
658 	"article",
659 	"xover",
660 };
661 char *nntpname[] = {
662 	"HEAD",
663 	"BODY",
664 	"ARTICLE",
665 	"XOVER",
666 };
667 
668 #define GROUP(p)	(((p)>>17)&0x3FFF)
669 #define MESSAGE(p)	((p)&0x1FFFF)
670 #define FILE(v)		((v)&0x3)
671 
672 #define PATH(g,m)	((((g)&0x3FFF)<<17)|((m)&0x1FFFF))
673 #define POST(g)	PATH(0,g,0)
674 #define VERS(f)		((f)&0x3)
675 
676 typedef struct Aux Aux;
677 struct Aux {
678 	Group *g;
679 	int n;
680 	int ispost;
681 	int file;
682 	char *s;
683 	int ns;
684 	int offset;
685 };
686 
687 static void
688 fsattach(Req *r)
689 {
690 	Aux *a;
691 	char *spec;
692 
693 	spec = r->ifcall.aname;
694 	if(spec && spec[0]){
695 		respond(r, "invalid attach specifier");
696 		return;
697 	}
698 
699 	a = emalloc(sizeof *a);
700 	a->g = root;
701 	a->n = -1;
702 	r->fid->aux = a;
703 
704 	r->ofcall.qid = (Qid){0, 0, QTDIR};
705 	r->fid->qid = r->ofcall.qid;
706 	respond(r, nil);
707 }
708 
709 static char*
710 fsclone(Fid *ofid, Fid *fid)
711 {
712 	Aux *a;
713 
714 	a = emalloc(sizeof(*a));
715 	*a = *(Aux*)ofid->aux;
716 	fid->aux = a;
717 	return nil;
718 }
719 
720 static char*
721 fswalk1(Fid *fid, char *name, Qid *qid)
722 {
723 	char *p;
724 	int i, isdotdot, n;
725 	Aux *a;
726 	Group *ng;
727 
728 	isdotdot = strcmp(name, "..")==0;
729 
730 	a = fid->aux;
731 	if(a->s)	/* file */
732 		return "protocol botch";
733 	if(a->n != -1){
734 		if(isdotdot){
735 			*qid = (Qid){PATH(a->g->num, 0), 0, QTDIR};
736 			fid->qid = *qid;
737 			a->n = -1;
738 			return nil;
739 		}
740 		for(i=0; i<Nfile; i++){
741 			if(strcmp(name, filename[i])==0){
742 				if(a->s = nntpget(net, a->g, a->n, nntpname[i])){
743 					*qid = (Qid){PATH(a->g->num, a->n), Qbody, 0};
744 					fid->qid = *qid;
745 					a->file = i;
746 					return nil;
747 				}else
748 					return "file does not exist";
749 			}
750 		}
751 		return "file does not exist";
752 	}
753 
754 	if(isdotdot){
755 		a->g = a->g->parent;
756 		*qid = (Qid){PATH(a->g->num, 0), 0, QTDIR};
757 		fid->qid = *qid;
758 		return nil;
759 	}
760 
761 	if(a->g->isgroup && !readonly && a->g->canpost
762 	&& strcmp(name, "post")==0){
763 		a->ispost = 1;
764 		*qid = (Qid){PATH(a->g->num, 0), 0, 0};
765 		fid->qid = *qid;
766 		return nil;
767 	}
768 
769 	if(ng = findgroup(a->g, name, 0)){
770 		a->g = ng;
771 		*qid = (Qid){PATH(a->g->num, 0), 0, QTDIR};
772 		fid->qid = *qid;
773 		return nil;
774 	}
775 
776 	n = strtoul(name, &p, 0);
777 	if('0'<=name[0] && name[0]<='9' && *p=='\0' && a->g->lo<=n && n<a->g->hi){
778 		a->n = n;
779 		*qid = (Qid){PATH(a->g->num, n+1-a->g->lo), 0, QTDIR};
780 		fid->qid = *qid;
781 		return nil;
782 	}
783 
784 	return "file does not exist";
785 }
786 
787 static void
788 fsopen(Req *r)
789 {
790 	Aux *a;
791 
792 	a = r->fid->aux;
793 	if((a->ispost && (r->ifcall.mode&~OTRUNC) != OWRITE)
794 	|| (!a->ispost && r->ifcall.mode != OREAD))
795 		respond(r, "permission denied");
796 	else
797 		respond(r, nil);
798 }
799 
800 static void
801 fillstat(Dir *d, Aux *a)
802 {
803 	char buf[32];
804 	Group *g;
805 
806 	memset(d, 0, sizeof *d);
807 	d->uid = estrdup("nntp");
808 	d->gid = estrdup("nntp");
809 	g = a->g;
810 	d->atime = d->mtime = g->mtime;
811 
812 	if(a->ispost){
813 		d->name = estrdup("post");
814 		d->mode = 0222;
815 		d->qid = (Qid){PATH(g->num, 0), 0, 0};
816 		d->length = a->ns;
817 		return;
818 	}
819 
820 	if(a->s){	/* article file */
821 		d->name = estrdup(filename[a->file]);
822 		d->mode = 0444;
823 		d->qid = (Qid){PATH(g->num, a->n+1-g->lo), a->file, 0};
824 		return;
825 	}
826 
827 	if(a->n != -1){	/* article directory */
828 		sprint(buf, "%d", a->n);
829 		d->name = estrdup(buf);
830 		d->mode = DMDIR|0555;
831 		d->qid = (Qid){PATH(g->num, a->n+1-g->lo), 0, QTDIR};
832 		return;
833 	}
834 
835 	/* group directory */
836 	if(g->name[0])
837 		d->name = estrdup(g->name);
838 	else
839 		d->name = estrdup("/");
840 	d->mode = DMDIR|0555;
841 	d->qid = (Qid){PATH(g->num, 0), g->hi-1, QTDIR};
842 }
843 
844 static int
845 dirfillstat(Dir *d, Aux *a, int i)
846 {
847 	int ndir;
848 	Group *g;
849 	char buf[32];
850 
851 	memset(d, 0, sizeof *d);
852 	d->uid = estrdup("nntp");
853 	d->gid = estrdup("nntp");
854 
855 	g = a->g;
856 	d->atime = d->mtime = g->mtime;
857 
858 	if(a->n != -1){	/* article directory */
859 		if(i >= Nfile)
860 			return -1;
861 
862 		d->name = estrdup(filename[i]);
863 		d->mode = 0444;
864 		d->qid = (Qid){PATH(g->num, a->n), i, 0};
865 		return 0;
866 	}
867 
868 	/* hierarchy directory: child groups */
869 	if(i < g->nkid){
870 		d->name = estrdup(g->kid[i]->name);
871 		d->mode = DMDIR|0555;
872 		d->qid = (Qid){PATH(g->kid[i]->num, 0), g->kid[i]->hi-1, QTDIR};
873 		return 0;
874 	}
875 	i -= g->nkid;
876 
877 	/* group directory: post file */
878 	if(g->isgroup && !readonly && g->canpost){
879 		if(i < 1){
880 			d->name = estrdup("post");
881 			d->mode = 0222;
882 			d->qid = (Qid){PATH(g->num, 0), 0, 0};
883 			return 0;
884 		}
885 		i--;
886 	}
887 
888 	/* group directory: child articles */
889 	ndir = g->hi - g->lo;
890 	if(i < ndir){
891 		sprint(buf, "%d", g->lo+i);
892 		d->name = estrdup(buf);
893 		d->mode = DMDIR|0555;
894 		d->qid = (Qid){PATH(g->num, i+1), 0, QTDIR};
895 		return 0;
896 	}
897 
898 	return -1;
899 }
900 
901 static void
902 fsstat(Req *r)
903 {
904 	Aux *a;
905 
906 	a = r->fid->aux;
907 	if(r->fid->qid.path == 0 && (r->fid->qid.type & QTDIR))
908 		nntprefreshall(net);
909 	else if(a->g->isgroup)
910 		nntprefresh(net, a->g);
911 	fillstat(&r->d, a);
912 	respond(r, nil);
913 }
914 
915 static void
916 fsread(Req *r)
917 {
918 	int offset, n;
919 	Aux *a;
920 	char *p, *ep;
921 	Dir d;
922 
923 	a = r->fid->aux;
924 	if(a->s){
925 		readstr(r, a->s);
926 		respond(r, nil);
927 		return;
928 	}
929 
930 	if(r->ifcall.offset == 0)
931 		offset = 0;
932 	else
933 		offset = a->offset;
934 
935 	p = r->ofcall.data;
936 	ep = r->ofcall.data+r->ifcall.count;
937 	for(; p+2 < ep; p += n){
938 		if(dirfillstat(&d, a, offset) < 0)
939 			break;
940 		n=convD2M(&d, (uchar*)p, ep-p);
941 		free(d.name);
942 		free(d.uid);
943 		free(d.gid);
944 		free(d.muid);
945 		if(n <= BIT16SZ)
946 			break;
947 		offset++;
948 	}
949 	a->offset = offset;
950 	r->ofcall.count = p - r->ofcall.data;
951 	respond(r, nil);
952 }
953 
954 static void
955 fswrite(Req *r)
956 {
957 	Aux *a;
958 	long count;
959 	vlong offset;
960 
961 	a = r->fid->aux;
962 
963 	if(r->ifcall.count == 0){	/* commit */
964 		respond(r, nntppost(net, a->s));
965 		free(a->s);
966 		a->ns = 0;
967 		a->s = nil;
968 		return;
969 	}
970 
971 	count = r->ifcall.count;
972 	offset = r->ifcall.offset;
973 	if(a->ns < count+offset+1){
974 		a->s = erealloc(a->s, count+offset+1);
975 		a->ns = count+offset;
976 		a->s[a->ns] = '\0';
977 	}
978 	memmove(a->s+offset, r->ifcall.data, count);
979 	r->ofcall.count = count;
980 	respond(r, nil);
981 }
982 
983 static void
984 fsdestroyfid(Fid *fid)
985 {
986 	Aux *a;
987 
988 	a = fid->aux;
989 	if(a==nil)
990 		return;
991 
992 	if(a->ispost && a->s)
993 		nntppost(net, a->s);
994 
995 	free(a->s);
996 	free(a);
997 }
998 
999 Srv nntpsrv = {
1000 .destroyfid=	fsdestroyfid,
1001 .attach=	fsattach,
1002 .clone=	fsclone,
1003 .walk1=	fswalk1,
1004 .open=	fsopen,
1005 .read=	fsread,
1006 .write=	fswrite,
1007 .stat=	fsstat,
1008 };
1009 
1010 void
1011 usage(void)
1012 {
1013 	fprint(2, "usage: nntpsrv [-a] [-s service] [-m mtpt] [nntp.server]\n");
1014 	exits("usage");
1015 }
1016 
1017 void
1018 dumpgroups(Group *g, int ind)
1019 {
1020 	int i;
1021 
1022 	print("%*s%s\n", ind*4, "", g->name);
1023 	for(i=0; i<g->nkid; i++)
1024 		dumpgroups(g->kid[i], ind+1);
1025 }
1026 
1027 void
1028 main(int argc, char **argv)
1029 {
1030 	int auth, x;
1031 	char *mtpt, *service, *where, *user;
1032 	Netbuf n;
1033 	UserPasswd *up;
1034 
1035 	mtpt = "/mnt/news";
1036 	service = nil;
1037 	memset(&n, 0, sizeof n);
1038 	user = nil;
1039 	auth = 0;
1040 	ARGBEGIN{
1041 	case 'D':
1042 		chatty9p++;
1043 		break;
1044 	case 'N':
1045 		netdebug = 1;
1046 		break;
1047 	case 'a':
1048 		auth = 1;
1049 		break;
1050 	case 'u':
1051 		user = EARGF(usage());
1052 		break;
1053 	case 's':
1054 		service = EARGF(usage());
1055 		break;
1056 	case 'm':
1057 		mtpt = EARGF(usage());
1058 		break;
1059 	default:
1060 		usage();
1061 	}ARGEND
1062 
1063 	if(argc > 1)
1064 		usage();
1065 	if(argc==0)
1066 		where = "$nntp";
1067 	else
1068 		where = argv[0];
1069 
1070 	now = time(0);
1071 
1072 	net = &n;
1073 	if(auth) {
1074 		n.auth = 1;
1075 		if(user)
1076 			up = auth_getuserpasswd(auth_getkey, "proto=pass service=nntp server=%q user=%q", where, user);
1077 		else
1078 			up = auth_getuserpasswd(auth_getkey, "proto=pass service=nntp server=%q", where);
1079 		if(up == nil)
1080 			sysfatal("no password: %r");
1081 
1082 		n.user = up->user;
1083 		n.pass = up->passwd;
1084 	}
1085 
1086 	n.addr = netmkaddr(where, "tcp", "nntp");
1087 
1088 	root = emalloc(sizeof *root);
1089 	root->name = estrdup("");
1090 	root->parent = root;
1091 
1092 	n.fd = -1;
1093 	if(nntpconnect(&n) < 0)
1094 		sysfatal("nntpconnect: %s", n.response);
1095 
1096 	x=netdebug;
1097 	netdebug=0;
1098 	nntprefreshall(&n);
1099 	netdebug=x;
1100 //	dumpgroups(root, 0);
1101 
1102 	postmountsrv(&nntpsrv, service, mtpt, MREPL);
1103 	exits(nil);
1104 }
1105 
1106