xref: /plan9-contrib/sys/src/cmd/webcookies.c (revision d46c239f8612929b7dbade67d0d071633df3a15d)
1 /*
2  * Cookie file system.  Allows hget and multiple webfs's to collaborate.
3  * Conventionally mounted on /mnt/webcookies.
4  */
5 
6 #include <u.h>
7 #include <libc.h>
8 #include <bio.h>
9 #include <ndb.h>
10 #include <fcall.h>
11 #include <thread.h>
12 #include <9p.h>
13 #include <ctype.h>
14 
15 int debug = 0;
16 
17 typedef struct Cookie Cookie;
18 typedef struct Jar Jar;
19 
20 struct Cookie
21 {
22 	/* external info */
23 	char*	name;
24 	char*	value;
25 	char*	dom;		/* starts with . */
26 	char*	path;
27 	char*	version;
28 	char*	comment;		/* optional, may be nil */
29 
30 	uint		expire;		/* time of expiration: ~0 means when webcookies dies */
31 	int		secure;
32 	int		explicitdom;	/* dom was explicitly set */
33 	int		explicitpath;	/* path was explicitly set */
34 	int		netscapestyle;
35 
36 	/* internal info */
37 	int		deleted;
38 	int		mark;
39 	int		ondisk;
40 };
41 
42 struct Jar
43 {
44 	Cookie	*c;
45 	int		nc;
46 	int		mc;
47 
48 	Qid		qid;
49 	int		dirty;
50 	char		*file;
51 	char		*lockfile;
52 };
53 
54 struct {
55 	char *s;
56 	int	offset;
57 	int	ishttp;
58 } stab[] = {
59 	"domain",		offsetof(Cookie, dom),		1,
60 	"path",		offsetof(Cookie, path),		1,
61 	"name",		offsetof(Cookie, name),		0,
62 	"value",		offsetof(Cookie, value),		0,
63 	"comment",	offsetof(Cookie, comment),	1,
64 	"version",		offsetof(Cookie, version),		1,
65 };
66 
67 struct {
68 	char *s;
69 	int	offset;
70 } itab[] = {
71 	"expire",			offsetof(Cookie, expire),
72 	"secure",			offsetof(Cookie, secure),
73 	"explicitdomain",	offsetof(Cookie, explicitdom),
74 	"explicitpath",		offsetof(Cookie, explicitpath),
75 	"netscapestyle",	offsetof(Cookie, netscapestyle),
76 };
77 
78 #pragma varargck type "J"	Jar*
79 #pragma varargck type "K"	Cookie*
80 
81 /* HTTP format */
82 int
83 jarfmt(Fmt *fmt)
84 {
85 	int i;
86 	Jar *jar;
87 
88 	jar = va_arg(fmt->args, Jar*);
89 	if(jar == nil || jar->nc == 0)
90 		return fmtstrcpy(fmt, "");
91 
92 	fmtprint(fmt, "Cookie: ");
93 	if(jar->c[0].version)
94 		fmtprint(fmt, "$Version=%s; ", jar->c[0].version);
95 	for(i=0; i<jar->nc; i++)
96 		fmtprint(fmt, "%s%s=%s", i ? "; ":"", jar->c[i].name, jar->c[i].value);
97 	fmtprint(fmt, "\r\n");
98 	return 0;
99 }
100 
101 /* individual cookie */
102 int
103 cookiefmt(Fmt *fmt)
104 {
105 	int j, k, first;
106 	char *t;
107 	Cookie *c;
108 
109 	c = va_arg(fmt->args, Cookie*);
110 
111 	first = 1;
112 	for(j=0; j<nelem(stab); j++){
113 		t = *(char**)((ulong)c+stab[j].offset);
114 		if(t == nil)
115 			continue;
116 		if(first)
117 			first = 0;
118 		else
119 			fmtprint(fmt, " ");
120 		fmtprint(fmt, "%s=%q", stab[j].s, t);
121 	}
122 	for(j=0; j<nelem(itab); j++){
123 		k = *(int*)((ulong)c+itab[j].offset);
124 		if(k == 0)
125 			continue;
126 		if(first)
127 			first = 0;
128 		else
129 			fmtprint(fmt, " ");
130 		fmtprint(fmt, "%s=%ud", itab[j].s, k);
131 	}
132 	return 0;
133 }
134 
135 /*
136  * sort cookies:
137  *	- alpha by name
138  *	- alpha by domain
139  *	- longer paths first, then alpha by path (RFC2109 4.3.4)
140  */
141 int
142 cookiecmp(Cookie *a, Cookie *b)
143 {
144 	int i;
145 
146 	if((i = strcmp(a->name, b->name)) != 0)
147 		return i;
148 	if((i = cistrcmp(a->dom, b->dom)) != 0)
149 		return i;
150 	if((i = strlen(b->path) - strlen(a->path)) != 0)
151 		return i;
152 	if((i = strcmp(a->path, b->path)) != 0)
153 		return i;
154 	return 0;
155 }
156 
157 int
158 exactcookiecmp(Cookie *a, Cookie *b)
159 {
160 	int i;
161 
162 	if((i = cookiecmp(a, b)) != 0)
163 		return i;
164 	if((i = strcmp(a->value, b->value)) != 0)
165 		return i;
166 	if(a->version || b->version){
167 		if(!a->version)
168 			return -1;
169 		if(!b->version)
170 			return 1;
171 		if((i = strcmp(a->version, b->version)) != 0)
172 			return i;
173 	}
174 	if(a->comment || b->comment){
175 		if(!a->comment)
176 			return -1;
177 		if(!b->comment)
178 			return 1;
179 		if((i = strcmp(a->comment, b->comment)) != 0)
180 			return i;
181 	}
182 	if((i = b->expire - a->expire) != 0)
183 		return i;
184 	if((i = b->secure - a->secure) != 0)
185 		return i;
186 	if((i = b->explicitdom - a->explicitdom) != 0)
187 		return i;
188 	if((i = b->explicitpath - a->explicitpath) != 0)
189 		return i;
190 	if((i = b->netscapestyle - a->netscapestyle) != 0)
191 		return i;
192 
193 	return 0;
194 }
195 
196 void
197 freecookie(Cookie *c)
198 {
199 	int i;
200 
201 	for(i=0; i<nelem(stab); i++)
202 		free(*(char**)((ulong)c+stab[i].offset));
203 }
204 
205 void
206 copycookie(Cookie *c)
207 {
208 	int i;
209 	char **ps;
210 
211 	for(i=0; i<nelem(stab); i++){
212 		ps = (char**)((ulong)c+stab[i].offset);
213 		if(*ps)
214 			*ps = estrdup9p(*ps);
215 	}
216 }
217 
218 void
219 delcookie(Jar *j, Cookie *c)
220 {
221 	int i;
222 
223 	j->dirty = 1;
224 	i = c - j->c;
225 	if(i < 0 || i >= j->nc)
226 		abort();
227 	c->deleted = 1;
228 }
229 
230 void
231 addcookie(Jar *j, Cookie *c)
232 {
233 	int i;
234 
235 	if(!c->name || !c->value || !c->path || !c->dom){
236 		fprint(2, "not adding incomplete cookie\n");
237 		return;
238 	}
239 
240 	if(debug)
241 		fprint(2, "add %K\n", c);
242 
243 	for(i=0; i<j->nc; i++)
244 		if(cookiecmp(&j->c[i], c) == 0){
245 			if(debug)
246 				fprint(2, "cookie %K matches %K\n", &j->c[i], c);
247 			if(exactcookiecmp(&j->c[i], c) == 0){
248 				if(debug)
249 					fprint(2, "\texactly\n");
250 				j->c[i].mark = 0;
251 				return;
252 			}
253 			delcookie(j, &j->c[i]);
254 		}
255 
256 	j->dirty = 1;
257 	if(j->nc == j->mc){
258 		j->mc += 16;
259 		j->c = erealloc9p(j->c, j->mc*sizeof(Cookie));
260 	}
261 	j->c[j->nc] = *c;
262 	copycookie(&j->c[j->nc]);
263 	j->nc++;
264 }
265 
266 void
267 purgejar(Jar *j)
268 {
269 	int i;
270 
271 	for(i=j->nc-1; i>=0; i--){
272 		if(!j->c[i].deleted)
273 			continue;
274 		freecookie(&j->c[i]);
275 		--j->nc;
276 		j->c[i] = j->c[j->nc];
277 	}
278 }
279 
280 void
281 addtojar(Jar *jar, char *line, int ondisk)
282 {
283 	Cookie c;
284 	int i, j, nf, *pint;
285 	char *f[20], *attr, *val, **pstr;
286 
287 	memset(&c, 0, sizeof c);
288 	c.expire = ~0;
289 	c.ondisk = ondisk;
290 	nf = tokenize(line, f, nelem(f));
291 	for(i=0; i<nf; i++){
292 		attr = f[i];
293 		if((val = strchr(attr, '=')) != nil)
294 			*val++ = '\0';
295 		else
296 			val = "";
297 		/* string attributes */
298 		for(j=0; j<nelem(stab); j++){
299 			if(strcmp(stab[j].s, attr) == 0){
300 				pstr = (char**)((ulong)&c+stab[j].offset);
301 				*pstr = val;
302 			}
303 		}
304 		/* integer attributes */
305 		for(j=0; j<nelem(itab); j++){
306 			if(strcmp(itab[j].s, attr) == 0){
307 				pint = (int*)((ulong)&c+itab[j].offset);
308 				if(val[0]=='\0')
309 					*pint = 1;
310 				else
311 					*pint = strtoul(val, 0, 0);
312 			}
313 		}
314 	}
315 	if(c.name==nil || c.value==nil || c.dom==nil || c.path==nil){
316 		if(debug)
317 			fprint(2, "ignoring fractional cookie %K\n", &c);
318 		return;
319 	}
320 	addcookie(jar, &c);
321 }
322 
323 Jar*
324 newjar(void)
325 {
326 	Jar *jar;
327 
328 	jar = emalloc9p(sizeof(Jar));
329 	return jar;
330 }
331 
332 int
333 expirejar(Jar *jar, int exiting)
334 {
335 	int i, n;
336 	uint now;
337 
338 	now = time(0);
339 	n = 0;
340 	for(i=0; i<jar->nc; i++){
341 		if(jar->c[i].expire < now || (exiting && jar->c[i].expire==~0)){
342 			delcookie(jar, &jar->c[i]);
343 			n++;
344 		}
345 	}
346 	return n;
347 }
348 
349 int
350 syncjar(Jar *jar)
351 {
352 	int i, fd;
353 	char *line;
354 	Dir *d;
355 	Biobuf *b;
356 	Qid q;
357 
358 	if(jar->file==nil)
359 		return 0;
360 
361 	memset(&q, 0, sizeof q);
362 	if((d = dirstat(jar->file)) != nil){
363 		q = d->qid;
364 		if(d->qid.path != jar->qid.path || d->qid.vers != jar->qid.vers)
365 			jar->dirty = 1;
366 		free(d);
367 	}
368 
369 	if(jar->dirty == 0)
370 		return 0;
371 
372 	fd = -1;
373 	for(i=0; i<50; i++){
374 		if((fd = create(jar->lockfile, OWRITE, DMEXCL|0666)) < 0){
375 			sleep(100);
376 			continue;
377 		}
378 		break;
379 	}
380 	if(fd < 0){
381 		if(debug)
382 			fprint(2, "open %s: %r", jar->lockfile);
383 		werrstr("cannot acquire jar lock: %r");
384 		return -1;
385 	}
386 
387 	for(i=0; i<jar->nc; i++)	/* mark is cleared by addcookie */
388 		jar->c[i].mark = jar->c[i].ondisk;
389 
390 	if((b = Bopen(jar->file, OREAD)) == nil){
391 		if(debug)
392 			fprint(2, "Bopen %s: %r", jar->file);
393 		werrstr("cannot read cookie file %s: %r", jar->file);
394 		close(fd);
395 		return -1;
396 	}
397 	for(; (line = Brdstr(b, '\n', 1)) != nil; free(line)){
398 		if(*line == '#')
399 			continue;
400 		addtojar(jar, line, 1);
401 	}
402 	Bterm(b);
403 
404 	for(i=0; i<jar->nc; i++)
405 		if(jar->c[i].mark)
406 			delcookie(jar, &jar->c[i]);
407 
408 	purgejar(jar);
409 
410 	b = Bopen(jar->file, OWRITE);
411 	if(b == nil){
412 		if(debug)
413 			fprint(2, "Bopen write %s: %r", jar->file);
414 		close(fd);
415 		return -1;
416 	}
417 	Bprint(b, "# webcookies cookie jar\n");
418 	Bprint(b, "# comments and non-standard fields will be lost\n");
419 	for(i=0; i<jar->nc; i++){
420 		if(jar->c[i].expire == ~0)
421 			continue;
422 		Bprint(b, "%K\n", &jar->c[i]);
423 		jar->c[i].ondisk = 1;
424 	}
425 	Bterm(b);
426 
427 	jar->dirty = 0;
428 	close(fd);
429 	jar->qid = q;
430 	return 0;
431 }
432 
433 Jar*
434 readjar(char *file)
435 {
436 	char *lock, *p;
437 	Jar *jar;
438 
439 	jar = newjar();
440 	lock = emalloc9p(strlen(file)+10);
441 	strcpy(lock, file);
442 	if((p = strrchr(lock, '/')) != nil)
443 		p++;
444 	else
445 		p = lock;
446 	memmove(p+2, p, strlen(p)+1);
447 	p[0] = 'L';
448 	p[1] = '.';
449 	jar->lockfile = lock;
450 	jar->file = file;
451 	jar->dirty = 1;
452 
453 	if(syncjar(jar) < 0){
454 		free(jar->file);
455 		free(jar->lockfile);
456 		free(jar);
457 		return nil;
458 	}
459 	return jar;
460 }
461 
462 void
463 closejar(Jar *jar)
464 {
465 	int i;
466 
467 	expirejar(jar, 0);
468 	if(syncjar(jar) < 0)
469 		fprint(2, "warning: cannot rewrite cookie jar: %r\n");
470 
471 	for(i=0; i<jar->nc; i++)
472 		freecookie(&jar->c[i]);
473 
474 	free(jar->file);
475 	free(jar);
476 }
477 
478 /*
479  * Domain name matching is per RFC2109, section 2:
480  *
481  * Hosts names can be specified either as an IP address or a FQHN
482  * string.  Sometimes we compare one host name with another.  Host A's
483  * name domain-matches host B's if
484  *
485  * * both host names are IP addresses and their host name strings match
486  *   exactly; or
487  *
488  * * both host names are FQDN strings and their host name strings match
489  *   exactly; or
490  *
491  * * A is a FQDN string and has the form NB, where N is a non-empty name
492  *   string, B has the form .B', and B' is a FQDN string.  (So, x.y.com
493  *   domain-matches .y.com but not y.com.)
494  *
495  * Note that domain-match is not a commutative operation: a.b.c.com
496  * domain-matches .c.com, but not the reverse.
497  *
498  * (This does not verify that IP addresses and FQDN's are well-formed.)
499  */
500 int
501 isdomainmatch(char *name, char *pattern)
502 {
503 	int lname, lpattern;
504 
505 	if(cistrcmp(name, pattern)==0)
506 		return 1;
507 
508 	if(strcmp(ipattr(name), "dom")==0 && pattern[0]=='.'){
509 		lname = strlen(name);
510 		lpattern = strlen(pattern);
511 		if(lname >= lpattern && cistrcmp(name+lname-lpattern, pattern)==0)
512 			return 1;
513 	}
514 
515 	return 0;
516 }
517 
518 /*
519  * RFC2109 4.3.4:
520  *	- domain must match
521  *	- path in cookie must be a prefix of request path
522  *	- cookie must not have expired
523  */
524 int
525 iscookiematch(Cookie *c, char *dom, char *path, uint now)
526 {
527 	return isdomainmatch(dom, c->dom)
528 		&& strncmp(c->path, path, strlen(c->path))==0
529 		&& c->expire >= now;
530 }
531 
532 /*
533  * Produce a subjar of matching cookies.
534  * Secure cookies are only included if secure is set.
535  */
536 Jar*
537 cookiesearch(Jar *jar, char *dom, char *path, int issecure)
538 {
539 	int i;
540 	Jar *j;
541 	uint now;
542 
543 	now = time(0);
544 	j = newjar();
545 	for(i=0; i<jar->nc; i++)
546 		if((issecure || !jar->c[i].secure) && iscookiematch(&jar->c[i], dom, path, now))
547 			addcookie(j, &jar->c[i]);
548 	if(j->nc == 0){
549 		closejar(j);
550 		werrstr("no cookies found");
551 		return nil;
552 	}
553 	qsort(j->c, j->nc, sizeof(j->c[0]), (int(*)(const void*, const void*))cookiecmp);
554 	return j;
555 }
556 
557 /*
558  * RFC2109 4.3.2 security checks
559  */
560 char*
561 isbadcookie(Cookie *c, char *dom, char *path)
562 {
563 	if(strncmp(c->path, path, strlen(c->path)) != 0)
564 		return "cookie path is not a prefix of the request path";
565 
566 	if(c->dom[0] != '.')
567 		return "cookie domain doesn't start with dot";
568 
569 	if(memchr(c->dom+1, '.', strlen(c->dom)-1-1) == nil)
570 		return "cookie domain doesn't have embedded dots";
571 
572 	if(!isdomainmatch(dom, c->dom))
573 		return "request host does not match cookie domain";
574 
575 	if(strcmp(ipattr(dom), "dom")==0
576 	&& memchr(dom, '.', strlen(dom)-strlen(c->dom)) != nil)
577 		return "request host contains dots before cookie domain";
578 
579 	return 0;
580 }
581 
582 /*
583  * Sunday, 25-Jan-2002 12:24:36 GMT
584  * Sunday, 25 Jan 2002 12:24:36 GMT
585  * Sun, 25 Jan 02 12:24:36 GMT
586  */
587 int
588 isleap(int year)
589 {
590 	return year%4==0 && (year%100!=0 || year%400==0);
591 }
592 
593 uint
594 strtotime(char *s)
595 {
596 	char *os;
597 	int i;
598 	Tm tm;
599 
600 	static int mday[2][12] = {
601 		31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
602 		31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
603 	};
604 	static char *wday[] = {
605 		"Sunday", "Monday", "Tuesday", "Wednesday",
606 		"Thursday", "Friday", "Saturday",
607 	};
608 	static char *mon[] = {
609 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
610 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
611 	};
612 
613 	os = s;
614 	/* Sunday, */
615 	for(i=0; i<nelem(wday); i++){
616 		if(cistrncmp(s, wday[i], strlen(wday[i])) == 0){
617 			s += strlen(wday[i]);
618 			break;
619 		}
620 		if(cistrncmp(s, wday[i], 3) == 0){
621 			s += 3;
622 			break;
623 		}
624 	}
625 	if(i==nelem(wday)){
626 		if(debug)
627 			fprint(2, "bad wday (%s)\n", os);
628 		return -1;
629 	}
630 	if(*s++ != ',' || *s++ != ' '){
631 		if(debug)
632 			fprint(2, "bad wday separator (%s)\n", os);
633 		return -1;
634 	}
635 
636 	/* 25- */
637 	if(!isdigit(s[0]) || !isdigit(s[1]) || (s[2]!='-' && s[2]!=' ')){
638 		if(debug)
639 			fprint(2, "bad day of month (%s)\n", os);
640 		return -1;
641 	}
642 	tm.mday = strtol(s, 0, 10);
643 	s += 3;
644 
645 	/* Jan- */
646 	for(i=0; i<nelem(mon); i++)
647 		if(cistrncmp(s, mon[i], 3) == 0){
648 			tm.mon = i;
649 			s += 3;
650 			break;
651 		}
652 	if(i==nelem(mon)){
653 		if(debug)
654 			fprint(2, "bad month (%s)\n", os);
655 		return -1;
656 	}
657 	if(s[0] != '-' && s[0] != ' '){
658 		if(debug)
659 			fprint(2, "bad month separator (%s)\n", os);
660 		return -1;
661 	}
662 	s++;
663 
664 	/* 2002 */
665 	if(!isdigit(s[0]) || !isdigit(s[1])){
666 		if(debug)
667 			fprint(2, "bad year (%s)\n", os);
668 		return -1;
669 	}
670 	tm.year = strtol(s, 0, 10);
671 	s += 2;
672 	if(isdigit(s[0]) && isdigit(s[1]))
673 		s += 2;
674 	else{
675 		if(tm.year <= 68)
676 			tm.year += 2000;
677 		else
678 			tm.year += 1900;
679 	}
680 	if(tm.mday==0 || tm.mday > mday[isleap(tm.year)][tm.mon]){
681 		if(debug)
682 			fprint(2, "invalid day of month (%s)\n", os);
683 		return -1;
684 	}
685 	tm.year -= 1900;
686 	if(*s++ != ' '){
687 		if(debug)
688 			fprint(2, "bad year separator (%s)\n", os);
689 		return -1;
690 	}
691 
692 	if(!isdigit(s[0]) || !isdigit(s[1]) || s[2]!=':'
693 	|| !isdigit(s[3]) || !isdigit(s[4]) || s[5]!=':'
694 	|| !isdigit(s[6]) || !isdigit(s[7]) || s[8]!=' '){
695 		if(debug)
696 			fprint(2, "bad time (%s)\n", os);
697 		return -1;
698 	}
699 
700 	tm.hour = atoi(s);
701 	tm.min = atoi(s+3);
702 	tm.sec = atoi(s+6);
703 	if(tm.hour >= 24 || tm.min >= 60 || tm.sec >= 60){
704 		if(debug)
705 			fprint(2, "invalid time (%s)\n", os);
706 		return -1;
707 	}
708 	s += 9;
709 
710 	if(cistrcmp(s, "GMT") != 0){
711 		if(debug)
712 			fprint(2, "time zone not GMT (%s)\n", os);
713 		return -1;
714 	}
715 	strcpy(tm.zone, "GMT");
716 	return tm2sec(&tm);
717 }
718 
719 /*
720  * skip linear whitespace.  we're a bit more lenient than RFC2616 2.2.
721  */
722 char*
723 skipspace(char *s)
724 {
725 	while(*s=='\r' || *s=='\n' || *s==' ' || *s=='\t')
726 		s++;
727 	return s;
728 }
729 
730 /*
731  * Try to identify old netscape headers.
732  * The old headers:
733  *	- didn't allow spaces around the '='
734  *	- used an 'Expires' attribute
735  *	- had no 'Version' attribute
736  *	- had no quotes
737  *	- allowed whitespace in values
738  *	- apparently separated attr/value pairs with ';' exclusively
739  */
740 int
741 isnetscape(char *hdr)
742 {
743 	char *s;
744 
745 	for(s=hdr; (s=strchr(s, '=')) != nil; s++){
746 		if(isspace(s[1]) || (s > hdr && isspace(s[-1])))
747 			return 0;
748 		if(s[1]=='"')
749 			return 0;
750 	}
751 	if(cistrstr(hdr, "version="))
752 		return 0;
753 	return 1;
754 }
755 
756 /*
757  * Parse HTTP response headers, adding cookies to jar.
758  * Overwrites the headers.
759  */
760 char* parsecookie(Cookie*, char*, char**, int, char*, char*);
761 int
762 parsehttp(Jar *jar, char *hdr, char *dom, char *path)
763 {
764 	static char setcookie[] = "Set-Cookie:";
765 	char *e, *p, *nextp;
766 	Cookie c;
767 	int isns, n;
768 
769 	isns = isnetscape(hdr);
770 	n = 0;
771 	for(p=hdr; p; p=nextp){
772 		p = skipspace(p);
773 		if(*p == '\0')
774 			break;
775 		nextp = strchr(p, '\n');
776 		if(nextp != nil)
777 			*nextp++ = '\0';
778 		if(debug)
779 			fprint(2, "?%s\n", p);
780 		if(cistrncmp(p, setcookie, strlen(setcookie)) != 0)
781 			continue;
782 		if(debug)
783 			fprint(2, "%s\n", p);
784 		p = skipspace(p+strlen(setcookie));
785 		for(; *p; p=skipspace(p)){
786 			if((e = parsecookie(&c, p, &p, isns, dom, path)) != nil){
787 				if(debug)
788 					fprint(2, "parse cookie: %s\n", e);
789 				break;
790 			}
791 			if((e = isbadcookie(&c, dom, path)) != nil){
792 				if(debug)
793 					fprint(2, "reject cookie; %s\n", e);
794 				continue;
795 			}
796 			addcookie(jar, &c);
797 			n++;
798 		}
799 	}
800 	return n;
801 }
802 
803 static char*
804 skipquoted(char *s)
805 {
806 	/*
807 	 * Sec 2.2 of RFC2616 defines a "quoted-string" as:
808 	 *
809 	 *  quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
810 	 *  qdtext         = <any TEXT except <">>
811 	 *  quoted-pair    = "\" CHAR
812 	 *
813 	 * TEXT is any octet except CTLs, but including LWS;
814 	 * LWS is [CR LF] 1*(SP | HT);
815 	 * CHARs are ASCII octets 0-127;  (NOTE: we reject 0's)
816 	 * CTLs are octets 0-31 and 127;
817 	 */
818 	if(*s != '"')
819 		return s;
820 
821 	for(s++; 32 <= *s && *s < 127 && *s != '"'; s++)
822 		if(*s == '\\' && *(s+1) != '\0')
823 			s++;
824 	return s;
825 }
826 
827 static char*
828 skiptoken(char *s)
829 {
830 	/*
831 	 * Sec 2.2 of RFC2616 defines a "token" as
832  	 *  1*<any CHAR except CTLs or separators>;
833 	 * CHARs are ASCII octets 0-127;
834 	 * CTLs are octets 0-31 and 127;
835 	 * separators are "()<>@,;:\/[]?={}", double-quote, SP (32), and HT (9)
836 	 */
837 	while(32 <= *s && *s < 127 && strchr("()<>@,;:[]?={}\" \t\\", *s)==nil)
838 		s++;
839 
840 	return s;
841 }
842 
843 static char*
844 skipvalue(char *s, int isns)
845 {
846 	char *t;
847 
848 	/*
849 	 * An RFC2109 value is an HTTP token or an HTTP quoted string.
850 	 * Netscape servers ignore the spec and rely on semicolons, apparently.
851 	 */
852 	if(isns){
853 		if((t = strchr(s, ';')) == nil)
854 			t = s+strlen(s);
855 		return t;
856 	}
857 	if(*s == '"')
858 		return skipquoted(s);
859 	return skiptoken(s);
860 }
861 
862 /*
863  * RMID=80b186bb64c03c65fab767f8; expires=Monday, 10-Feb-2003 04:44:39 GMT;
864  *	path=/; domain=.nytimes.com
865  */
866 char*
867 parsecookie(Cookie *c, char *p, char **e, int isns, char *dom, char *path)
868 {
869 	int i, done;
870 	char *t, *u, *attr, *val;
871 
872 	memset(c, 0, sizeof *c);
873 
874 	/* NAME=VALUE */
875 	t = skiptoken(p);
876 	c->name = p;
877 	p = skipspace(t);
878 	if(*p != '='){
879 	Badname:
880 		return "malformed cookie: no NAME=VALUE";
881 	}
882 	*t = '\0';
883 	p = skipspace(p+1);
884 	t = skipvalue(p, isns);
885 	if(*t)
886 		*t++ = '\0';
887 	c->value = p;
888 	p = skipspace(t);
889 	if(c->name[0]=='\0' || c->value[0]=='\0')
890 		goto Badname;
891 
892 	done = 0;
893 	for(; *p && !done; p=skipspace(p)){
894 		attr = p;
895 		t = skiptoken(p);
896 		u = skipspace(t);
897 		switch(*u){
898 		case ';':
899 			*t = '\0';
900 			val = "";
901 			p = u+1;
902 			break;
903 		case '=':
904 			*t = '\0';
905 			val = skipspace(u+1);
906 			p = skipvalue(val, isns);
907 			if(*p==',')
908 				done = 1;
909 			if(*p)
910 				*p++ = '\0';
911 			break;
912 		case ',':
913 			if(!isns){
914 				val = "";
915 				p = u;
916 				*p++ = '\0';
917 				done = 1;
918 				break;
919 			}
920 		default:
921 			if(debug)
922 				fprint(2, "syntax: %s\n", p);
923 			return "syntax error";
924 		}
925 		for(i=0; i<nelem(stab); i++)
926 			if(stab[i].ishttp && cistrcmp(stab[i].s, attr)==0)
927 				*(char**)((ulong)c+stab[i].offset) = val;
928 		if(cistrcmp(attr, "expires") == 0){
929 			if(!isns)
930 				return "non-netscape cookie has Expires tag";
931 			if(!val[0])
932 				return "bad expires tag";
933 			c->expire = strtotime(val);
934 			if(c->expire == ~0)
935 				return "cannot parse netscape expires tag";
936 		}
937 		if(cistrcmp(attr, "max-age") == 0)
938 			c->expire = time(0)+atoi(val);
939 		if(cistrcmp(attr, "secure") == 0)
940 			c->secure = 1;
941 	}
942 
943 	if(c->dom)
944 		c->explicitdom = 1;
945 	else
946 		c->dom = dom;
947 	if(c->path)
948 		c->explicitpath = 1;
949 	else
950 		c->path = path;
951 	c->netscapestyle = isns;
952 	*e = p;
953 
954 	return nil;
955 }
956 
957 Jar *jar;
958 
959 enum
960 {
961 	Xhttp = 1,
962 	Xcookies,
963 
964 	NeedUrl = 0,
965 	HaveUrl,
966 };
967 
968 typedef struct Aux Aux;
969 struct Aux
970 {
971 	int state;
972 	char *dom;
973 	char *path;
974 	char *inhttp;
975 	char *outhttp;
976 	char *ctext;
977 	int rdoff;
978 };
979 enum
980 {
981 	AuxBuf = 4096,
982 	MaxCtext = 16*1024*1024,
983 };
984 
985 void
986 fsopen(Req *r)
987 {
988 	char *s, *es;
989 	int i, sz;
990 	Aux *a;
991 
992 	switch((int)r->fid->file->aux){
993 	case Xhttp:
994 		syncjar(jar);
995 		a = emalloc9p(sizeof(Aux));
996 		r->fid->aux = a;
997 		a->inhttp = emalloc9p(AuxBuf);
998 		a->outhttp = emalloc9p(AuxBuf);
999 		break;
1000 
1001 	case Xcookies:
1002 		syncjar(jar);
1003 		a = emalloc9p(sizeof(Aux));
1004 		r->fid->aux = a;
1005 		if(r->ifcall.mode&OTRUNC){
1006 			a->ctext = emalloc9p(1);
1007 			a->ctext[0] = '\0';
1008 		}else{
1009 			sz = 256*jar->nc+1024;	/* BUG should do better */
1010 			a->ctext = emalloc9p(sz);
1011 			a->ctext[0] = '\0';
1012 			s = a->ctext;
1013 			es = s+sz;
1014 			for(i=0; i<jar->nc; i++)
1015 				s = seprint(s, es, "%K\n", &jar->c[i]);
1016 		}
1017 		break;
1018 	}
1019 	respond(r, nil);
1020 }
1021 
1022 void
1023 fsread(Req *r)
1024 {
1025 	Aux *a;
1026 
1027 	a = r->fid->aux;
1028 	switch((int)r->fid->file->aux){
1029 	case Xhttp:
1030 		if(a->state == NeedUrl){
1031 			respond(r, "must write url before read");
1032 			return;
1033 		}
1034 		r->ifcall.offset = a->rdoff;
1035 		readstr(r, a->outhttp);
1036 		a->rdoff += r->ofcall.count;
1037 		respond(r, nil);
1038 		return;
1039 
1040 	case Xcookies:
1041 		readstr(r, a->ctext);
1042 		respond(r, nil);
1043 		return;
1044 
1045 	default:
1046 		respond(r, "bug in webcookies");
1047 		return;
1048 	}
1049 }
1050 
1051 void
1052 fswrite(Req *r)
1053 {
1054 	Aux *a;
1055 	int i, sz, hlen, issecure;
1056 	char buf[1024], *p;
1057 	Jar *j;
1058 
1059 	a = r->fid->aux;
1060 	switch((int)r->fid->file->aux){
1061 	case Xhttp:
1062 		if(a->state == NeedUrl){
1063 			if(r->ifcall.count >= sizeof buf){
1064 				respond(r, "url too long");
1065 				return;
1066 			}
1067 			memmove(buf, r->ifcall.data, r->ifcall.count);
1068 			buf[r->ifcall.count] = '\0';
1069 			issecure = 0;
1070 			if(cistrncmp(buf, "http://", 7) == 0)
1071 				hlen = 7;
1072 			else if(cistrncmp(buf, "https://", 8) == 0){
1073 				hlen = 8;
1074 				issecure = 1;
1075 			}else{
1076 				respond(r, "url must begin http:// or https://");
1077 				return;
1078 			}
1079 			if(buf[hlen]=='/'){
1080 				respond(r, "url without host name");
1081 				return;
1082 			}
1083 			p = strchr(buf+hlen, '/');
1084 			if(p == nil)
1085 				a->path = estrdup9p("/");
1086 			else{
1087 				a->path = estrdup9p(p);
1088 				*p = '\0';
1089 			}
1090 			a->dom = estrdup9p(buf+hlen);
1091 			a->state = HaveUrl;
1092 			j = cookiesearch(jar, a->dom, a->path, issecure);
1093 			if(debug){
1094 				fprint(2, "search %s %s got %p\n", a->dom, a->path, j);
1095 				if(j){
1096 					fprint(2, "%d cookies\n", j->nc);
1097 					for(i=0; i<j->nc; i++)
1098 						fprint(2, "%K\n", &j->c[i]);
1099 				}
1100 			}
1101 			snprint(a->outhttp, AuxBuf, "%J", j);
1102 			if(j)
1103 				closejar(j);
1104 		}else{
1105 			if(strlen(a->inhttp)+r->ifcall.count >= AuxBuf){
1106 				respond(r, "http headers too large");
1107 				return;
1108 			}
1109 			memmove(a->inhttp+strlen(a->inhttp), r->ifcall.data, r->ifcall.count);
1110 		}
1111 		r->ofcall.count = r->ifcall.count;
1112 		respond(r, nil);
1113 		return;
1114 
1115 	case Xcookies:
1116 		sz = r->ifcall.count+r->ifcall.offset;
1117 		if(sz > strlen(a->ctext)){
1118 			if(sz >= MaxCtext){
1119 				respond(r, "cookie file too large");
1120 				return;
1121 			}
1122 			a->ctext = erealloc9p(a->ctext, sz+1);
1123 			a->ctext[sz] = '\0';
1124 		}
1125 		memmove(a->ctext+r->ifcall.offset, r->ifcall.data, r->ifcall.count);
1126 		r->ofcall.count = r->ifcall.count;
1127 		respond(r, nil);
1128 		return;
1129 
1130 	default:
1131 		respond(r, "bug in webcookies");
1132 		return;
1133 	}
1134 }
1135 
1136 void
1137 fsdestroyfid(Fid *fid)
1138 {
1139 	char *p, *nextp;
1140 	Aux *a;
1141 	int i;
1142 
1143 	a = fid->aux;
1144 	if(a == nil)
1145 		return;
1146 	switch((int)fid->file->aux){
1147 	case Xhttp:
1148 		parsehttp(jar, a->inhttp, a->dom, a->path);
1149 		break;
1150 	case Xcookies:
1151 		for(i=0; i<jar->nc; i++)
1152 			jar->c[i].mark = 1;
1153 		for(p=a->ctext; *p; p=nextp){
1154 			if((nextp = strchr(p, '\n')) != nil)
1155 				*nextp++ = '\0';
1156 			else
1157 				nextp = "";
1158 			addtojar(jar, p, 0);
1159 		}
1160 		for(i=0; i<jar->nc; i++)
1161 			if(jar->c[i].mark)
1162 				delcookie(jar, &jar->c[i]);
1163 		break;
1164 	}
1165 	syncjar(jar);
1166 	free(a->dom);
1167 	free(a->path);
1168 	free(a->inhttp);
1169 	free(a->outhttp);
1170 	free(a->ctext);
1171 	free(a);
1172 }
1173 
1174 void
1175 fsend(Srv*)
1176 {
1177 	closejar(jar);
1178 }
1179 
1180 Srv fs =
1181 {
1182 .open=		fsopen,
1183 .read=		fsread,
1184 .write=		fswrite,
1185 .destroyfid=	fsdestroyfid,
1186 .end=		fsend,
1187 };
1188 
1189 void
1190 usage(void)
1191 {
1192 	fprint(2, "usage: webcookies [-f file] [-m mtpt] [-s service]\n");
1193 	exits("usage");
1194 }
1195 
1196 void
1197 main(int argc, char **argv)
1198 {
1199 	char *file, *mtpt, *home, *srv;
1200 
1201 	file = nil;
1202 	srv = nil;
1203 	mtpt = "/mnt/webcookies";
1204 	ARGBEGIN{
1205 	case 'D':
1206 		chatty9p++;
1207 		break;
1208 	case 'd':
1209 		debug = 1;
1210 		break;
1211 	case 'f':
1212 		file = EARGF(usage());
1213 		break;
1214 	case 's':
1215 		srv = EARGF(usage());
1216 		break;
1217 	case 'm':
1218 		mtpt = EARGF(usage());
1219 		break;
1220 	default:
1221 		usage();
1222 	}ARGEND
1223 
1224 	if(argc != 0)
1225 		usage();
1226 
1227 	quotefmtinstall();
1228 	fmtinstall('J', jarfmt);
1229 	fmtinstall('K', cookiefmt);
1230 
1231 	if(file == nil){
1232 		home = getenv("home");
1233 		if(home == nil)
1234 			sysfatal("no cookie file specified and no $home");
1235 		file = emalloc9p(strlen(home)+30);
1236 		strcpy(file, home);
1237 		strcat(file, "/lib/webcookies");
1238 	}
1239 	if(access(file, AEXIST) < 0)
1240 		close(create(file, OWRITE, 0666));
1241 
1242 	jar = readjar(file);
1243 	if(jar == nil)
1244 		sysfatal("readjar: %r");
1245 
1246 	fs.tree = alloctree("cookie", "cookie", DMDIR|0555, nil);
1247 	closefile(createfile(fs.tree->root, "http", "cookie", 0666, (void*)Xhttp));
1248 	closefile(createfile(fs.tree->root, "cookies", "cookie", 0666, (void*)Xcookies));
1249 
1250 	postmountsrv(&fs, srv, mtpt, MREPL);
1251 	exits(nil);
1252 }
1253