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