xref: /plan9/sys/src/cmd/webfs/cookies.c (revision ff8c3af2f44d95267f67219afa20ba82ff6cf7e4)
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 	jar->qid = q;
456 	return 0;
457 }
458 
459 static Jar*
460 readjar(char *file)
461 {
462 	char *lock, *p;
463 	Jar *jar;
464 
465 	jar = newjar();
466 	lock = emalloc9p(strlen(file)+10);
467 	strcpy(lock, file);
468 	if((p = strrchr(lock, '/')) != nil)
469 		p++;
470 	else
471 		p = lock;
472 	memmove(p+2, p, strlen(p)+1);
473 	p[0] = 'L';
474 	p[1] = '.';
475 	jar->lockfile = lock;
476 	jar->file = file;
477 	jar->dirty = 1;
478 
479 	if(syncjar(jar) < 0){
480 		free(jar->file);
481 		free(jar->lockfile);
482 		free(jar);
483 		return nil;
484 	}
485 	return jar;
486 }
487 
488 static void
489 closejar(Jar *jar)
490 {
491 	int i;
492 
493 	if(jar == nil)
494 		return;
495 	expirejar(jar, 0);
496 	if(syncjar(jar) < 0)
497 		fprint(2, "warning: cannot rewrite cookie jar: %r\n");
498 
499 	for(i=0; i<jar->nc; i++)
500 		freecookie(&jar->c[i]);
501 
502 	free(jar->file);
503 	free(jar);
504 }
505 
506 /*
507  * Domain name matching is per RFC2109, section 2:
508  *
509  * Hosts names can be specified either as an IP address or a FQHN
510  * string.  Sometimes we compare one host name with another.  Host A's
511  * name domain-matches host B's if
512  *
513  * * both host names are IP addresses and their host name strings match
514  *   exactly; or
515  *
516  * * both host names are FQDN strings and their host name strings match
517  *   exactly; or
518  *
519  * * A is a FQDN string and has the form NB, where N is a non-empty name
520  *   string, B has the form .B', and B' is a FQDN string.  (So, x.y.com
521  *   domain-matches .y.com but not y.com.)
522  *
523  * Note that domain-match is not a commutative operation: a.b.c.com
524  * domain-matches .c.com, but not the reverse.
525  *
526  * (This does not verify that IP addresses and FQDN's are well-formed.)
527  */
528 static int
529 isdomainmatch(char *name, char *pattern)
530 {
531 	int lname, lpattern;
532 
533 	if(cistrcmp(name, pattern)==0)
534 		return 1;
535 
536 	if(strcmp(ipattr(name), "dom")==0 && pattern[0]=='.'){
537 		lname = strlen(name);
538 		lpattern = strlen(pattern);
539 		if(lname >= lpattern && cistrcmp(name+lname-lpattern, pattern)==0)
540 			return 1;
541 	}
542 
543 	return 0;
544 }
545 
546 /*
547  * RFC2109 4.3.4:
548  *	- domain must match
549  *	- path in cookie must be a prefix of request path
550  *	- cookie must not have expired
551  */
552 static int
553 iscookiematch(Cookie *c, char *dom, char *path, uint now)
554 {
555 	return isdomainmatch(dom, c->dom)
556 		&& strncmp(c->path, path, strlen(c->path))==0
557 		&& c->expire >= now;
558 }
559 
560 /*
561  * Produce a subjar of matching cookies.
562  * Secure cookies are only included if secure is set.
563  */
564 static Jar*
565 cookiesearch(Jar *jar, char *dom, char *path, int issecure)
566 {
567 	int i;
568 	Jar *j;
569 	uint now;
570 
571 	if(cookiedebug)
572 		fprint(2, "cookiesearch %s %s %d\n", dom, path, issecure);
573 	now = time(0);
574 	j = newjar();
575 	for(i=0; i<jar->nc; i++){
576 		if(cookiedebug)
577 			fprint(2, "\ttry %s %s %d %s\n", jar->c[i].dom, jar->c[i].path, jar->c[i].secure, jar->c[i].name);
578 		if((issecure || !jar->c[i].secure) && iscookiematch(&jar->c[i], dom, path, now)){
579 			if(cookiedebug)
580 				fprint(2, "\tmatched\n");
581 			addcookie(j, &jar->c[i]);
582 		}
583 	}
584 	if(j->nc == 0){
585 		closejar(j);
586 		werrstr("no cookies found");
587 		return nil;
588 	}
589 	qsort(j->c, j->nc, sizeof(j->c[0]), (int(*)(const void*, const void*))cookiecmp);
590 	return j;
591 }
592 
593 /*
594  * RFC2109 4.3.2 security checks
595  */
596 static char*
597 isbadcookie(Cookie *c, char *dom, char *path)
598 {
599 	if(strncmp(c->path, path, strlen(c->path)) != 0)
600 		return "cookie path is not a prefix of the request path";
601 
602 	if(c->dom[0] != '.')
603 		return "cookie domain doesn't start with dot";
604 
605 	if(memchr(c->dom+1, '.', strlen(c->dom)-1-1) == nil)
606 		return "cookie domain doesn't have embedded dots";
607 
608 	if(!isdomainmatch(dom, c->dom))
609 		return "request host does not match cookie domain";
610 
611 	if(strcmp(ipattr(dom), "dom")==0
612 	&& memchr(dom, '.', strlen(dom)-strlen(c->dom)) != nil)
613 		return "request host contains dots before cookie domain";
614 
615 	return 0;
616 }
617 
618 /*
619  * Sunday, 25-Jan-2002 12:24:36 GMT
620  * Sunday, 25 Jan 2002 12:24:36 GMT
621  * Sun, 25 Jan 02 12:24:36 GMT
622  */
623 static int
624 isleap(int year)
625 {
626 	return year%4==0 && (year%100!=0 || year%400==0);
627 }
628 
629 static uint
630 strtotime(char *s)
631 {
632 	char *os;
633 	int i;
634 	Tm tm;
635 
636 	static int mday[2][12] = {
637 		31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
638 		31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
639 	};
640 	static char *wday[] = {
641 		"Sunday", "Monday", "Tuesday", "Wednesday",
642 		"Thursday", "Friday", "Saturday",
643 	};
644 	static char *mon[] = {
645 		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
646 		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
647 	};
648 
649 	os = s;
650 	/* Sunday, */
651 	for(i=0; i<nelem(wday); i++){
652 		if(cistrncmp(s, wday[i], strlen(wday[i])) == 0){
653 			s += strlen(wday[i]);
654 			break;
655 		}
656 		if(cistrncmp(s, wday[i], 3) == 0){
657 			s += 3;
658 			break;
659 		}
660 	}
661 	if(i==nelem(wday)){
662 		if(cookiedebug)
663 			fprint(2, "bad wday (%s)\n", os);
664 		return -1;
665 	}
666 	if(*s++ != ',' || *s++ != ' '){
667 		if(cookiedebug)
668 			fprint(2, "bad wday separator (%s)\n", os);
669 		return -1;
670 	}
671 
672 	/* 25- */
673 	if(!isdigit(s[0]) || !isdigit(s[1]) || (s[2]!='-' && s[2]!=' ')){
674 		if(cookiedebug)
675 			fprint(2, "bad day of month (%s)\n", os);
676 		return -1;
677 	}
678 	tm.mday = strtol(s, 0, 10);
679 	s += 3;
680 
681 	/* Jan- */
682 	for(i=0; i<nelem(mon); i++)
683 		if(cistrncmp(s, mon[i], 3) == 0){
684 			tm.mon = i;
685 			s += 3;
686 			break;
687 		}
688 	if(i==nelem(mon)){
689 		if(cookiedebug)
690 			fprint(2, "bad month (%s)\n", os);
691 		return -1;
692 	}
693 	if(s[0] != '-' && s[0] != ' '){
694 		if(cookiedebug)
695 			fprint(2, "bad month separator (%s)\n", os);
696 		return -1;
697 	}
698 	s++;
699 
700 	/* 2002 */
701 	if(!isdigit(s[0]) || !isdigit(s[1])){
702 		if(cookiedebug)
703 			fprint(2, "bad year (%s)\n", os);
704 		return -1;
705 	}
706 	tm.year = strtol(s, 0, 10);
707 	s += 2;
708 	if(isdigit(s[0]) && isdigit(s[1]))
709 		s += 2;
710 	else{
711 		if(tm.year <= 68)
712 			tm.year += 2000;
713 		else
714 			tm.year += 1900;
715 	}
716 	if(tm.mday==0 || tm.mday > mday[isleap(tm.year)][tm.mon]){
717 		if(cookiedebug)
718 			fprint(2, "invalid day of month (%s)\n", os);
719 		return -1;
720 	}
721 	tm.year -= 1900;
722 	if(*s++ != ' '){
723 		if(cookiedebug)
724 			fprint(2, "bad year separator (%s)\n", os);
725 		return -1;
726 	}
727 
728 	if(!isdigit(s[0]) || !isdigit(s[1]) || s[2]!=':'
729 	|| !isdigit(s[3]) || !isdigit(s[4]) || s[5]!=':'
730 	|| !isdigit(s[6]) || !isdigit(s[7]) || s[8]!=' '){
731 		if(cookiedebug)
732 			fprint(2, "bad time (%s)\n", os);
733 		return -1;
734 	}
735 
736 	tm.hour = atoi(s);
737 	tm.min = atoi(s+3);
738 	tm.sec = atoi(s+6);
739 	if(tm.hour >= 24 || tm.min >= 60 || tm.sec >= 60){
740 		if(cookiedebug)
741 			fprint(2, "invalid time (%s)\n", os);
742 		return -1;
743 	}
744 	s += 9;
745 
746 	if(cistrcmp(s, "GMT") != 0){
747 		if(cookiedebug)
748 			fprint(2, "time zone not GMT (%s)\n", os);
749 		return -1;
750 	}
751 	strcpy(tm.zone, "GMT");
752 	tm.yday = 0;
753 	return tm2sec(&tm);
754 }
755 
756 /*
757  * skip linear whitespace.  we're a bit more lenient than RFC2616 2.2.
758  */
759 static char*
760 skipspace(char *s)
761 {
762 	while(*s=='\r' || *s=='\n' || *s==' ' || *s=='\t')
763 		s++;
764 	return s;
765 }
766 
767 /*
768  * Try to identify old netscape headers.
769  * The old headers:
770  *	- didn't allow spaces around the '='
771  *	- used an 'Expires' attribute
772  *	- had no 'Version' attribute
773  *	- had no quotes
774  *	- allowed whitespace in values
775  *	- apparently separated attr/value pairs with ';' exclusively
776  */
777 static int
778 isnetscape(char *hdr)
779 {
780 	char *s;
781 
782 	for(s=hdr; (s=strchr(s, '=')) != nil; s++){
783 		if(isspace(s[1]) || (s > hdr && isspace(s[-1])))
784 			return 0;
785 		if(s[1]=='"')
786 			return 0;
787 	}
788 	if(cistrstr(hdr, "version="))
789 		return 0;
790 	return 1;
791 }
792 
793 /*
794  * Parse HTTP response headers, adding cookies to jar.
795  * Overwrites the headers.
796  */
797 static char* parsecookie(Cookie*, char*, char**, int, char*, char*);
798 static int
799 parsehttp(Jar *jar, char *hdr, char *dom, char *path)
800 {
801 	static char setcookie[] = "Set-Cookie:";
802 	char *e, *p, *nextp;
803 	Cookie c;
804 	int isns, n;
805 
806 	isns = isnetscape(hdr);
807 	n = 0;
808 	for(p=hdr; p; p=nextp){
809 		p = skipspace(p);
810 		if(*p == '\0')
811 			break;
812 		nextp = strchr(p, '\n');
813 		if(nextp != nil)
814 			*nextp++ = '\0';
815 		if(cistrncmp(p, setcookie, strlen(setcookie)) != 0)
816 			continue;
817 		if(cookiedebug)
818 			fprint(2, "%s\n", p);
819 		p = skipspace(p+strlen(setcookie));
820 		for(; *p; p=skipspace(p)){
821 			if((e = parsecookie(&c, p, &p, isns, dom, path)) != nil){
822 				if(cookiedebug)
823 					fprint(2, "parse cookie: %s\n", e);
824 				break;
825 			}
826 			if((e = isbadcookie(&c, dom, path)) != nil){
827 				if(cookiedebug)
828 					fprint(2, "reject cookie; %s\n", e);
829 				continue;
830 			}
831 			addcookie(jar, &c);
832 			n++;
833 		}
834 	}
835 	return n;
836 }
837 
838 static char*
839 skipquoted(char *s)
840 {
841 	/*
842 	 * Sec 2.2 of RFC2616 defines a "quoted-string" as:
843 	 *
844 	 *  quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
845 	 *  qdtext         = <any TEXT except <">>
846 	 *  quoted-pair    = "\" CHAR
847 	 *
848 	 * TEXT is any octet except CTLs, but including LWS;
849 	 * LWS is [CR LF] 1*(SP | HT);
850 	 * CHARs are ASCII octets 0-127;  (NOTE: we reject 0's)
851 	 * CTLs are octets 0-31 and 127;
852 	 */
853 	if(*s != '"')
854 		return s;
855 
856 	for(s++; 32 <= *s && *s < 127 && *s != '"'; s++)
857 		if(*s == '\\' && *(s+1) != '\0')
858 			s++;
859 	return s;
860 }
861 
862 static char*
863 skiptoken(char *s)
864 {
865 	/*
866 	 * Sec 2.2 of RFC2616 defines a "token" as
867  	 *  1*<any CHAR except CTLs or separators>;
868 	 * CHARs are ASCII octets 0-127;
869 	 * CTLs are octets 0-31 and 127;
870 	 * separators are "()<>@,;:\/[]?={}", double-quote, SP (32), and HT (9)
871 	 */
872 	while(32 <= *s && *s < 127 && strchr("()<>@,;:[]?={}\" \t\\", *s)==nil)
873 		s++;
874 
875 	return s;
876 }
877 
878 static char*
879 skipvalue(char *s, int isns)
880 {
881 	char *t;
882 
883 	/*
884 	 * An RFC2109 value is an HTTP token or an HTTP quoted string.
885 	 * Netscape servers ignore the spec and rely on semicolons, apparently.
886 	 */
887 	if(isns){
888 		if((t = strchr(s, ';')) == nil)
889 			t = s+strlen(s);
890 		return t;
891 	}
892 	if(*s == '"')
893 		return skipquoted(s);
894 	return skiptoken(s);
895 }
896 
897 /*
898  * RMID=80b186bb64c03c65fab767f8; expires=Monday, 10-Feb-2003 04:44:39 GMT;
899  *	path=/; domain=.nytimes.com
900  */
901 static char*
902 parsecookie(Cookie *c, char *p, char **e, int isns, char *dom, char *path)
903 {
904 	int i, done;
905 	char *t, *u, *attr, *val;
906 
907 	memset(c, 0, sizeof *c);
908 
909 	/* NAME=VALUE */
910 	t = skiptoken(p);
911 	c->name = p;
912 	p = skipspace(t);
913 	if(*p != '='){
914 	Badname:
915 		return "malformed cookie: no NAME=VALUE";
916 	}
917 	*t = '\0';
918 	p = skipspace(p+1);
919 	t = skipvalue(p, isns);
920 	if(*t)
921 		*t++ = '\0';
922 	c->value = p;
923 	p = skipspace(t);
924 	if(c->name[0]=='\0' || c->value[0]=='\0')
925 		goto Badname;
926 
927 	done = 0;
928 	for(; *p && !done; p=skipspace(p)){
929 		attr = p;
930 		t = skiptoken(p);
931 		u = skipspace(t);
932 		switch(*u){
933 		case ';':
934 			*t = '\0';
935 			val = "";
936 			p = u+1;
937 			break;
938 		case '=':
939 			*t = '\0';
940 			val = skipspace(u+1);
941 			p = skipvalue(val, isns);
942 			if(*p==',')
943 				done = 1;
944 			if(*p)
945 				*p++ = '\0';
946 			break;
947 		case ',':
948 			if(!isns){
949 				val = "";
950 				p = u;
951 				*p++ = '\0';
952 				done = 1;
953 				break;
954 			}
955 		default:
956 			if(cookiedebug)
957 				fprint(2, "syntax: %s\n", p);
958 			return "syntax error";
959 		}
960 		for(i=0; i<nelem(stab); i++)
961 			if(stab[i].ishttp && cistrcmp(stab[i].s, attr)==0)
962 				*(char**)((ulong)c+stab[i].offset) = val;
963 		if(cistrcmp(attr, "expires") == 0){
964 			if(!isns)
965 				return "non-netscape cookie has Expires tag";
966 			if(!val[0])
967 				return "bad expires tag";
968 			c->expire = strtotime(val);
969 			if(c->expire == ~0)
970 				return "cannot parse netscape expires tag";
971 		}
972 		if(cistrcmp(attr, "max-age") == 0)
973 			c->expire = time(0)+atoi(val);
974 		if(cistrcmp(attr, "secure") == 0)
975 			c->secure = 1;
976 	}
977 
978 	if(c->dom)
979 		c->explicitdom = 1;
980 	else
981 		c->dom = dom;
982 	if(c->path)
983 		c->explicitpath = 1;
984 	else
985 		c->path = path;
986 	c->netscapestyle = isns;
987 	*e = p;
988 
989 	return nil;
990 }
991 
992 Jar *jar;
993 
994 typedef struct Aux Aux;
995 struct Aux
996 {
997 	char *dom;
998 	char *path;
999 	char *inhttp;
1000 	char *outhttp;
1001 	char *ctext;
1002 	int rdoff;
1003 };
1004 enum
1005 {
1006 	AuxBuf = 4096,
1007 	MaxCtext = 16*1024*1024,
1008 };
1009 
1010 void
1011 cookieopen(Req *r)
1012 {
1013 	char *s, *es;
1014 	int i, sz;
1015 	Aux *a;
1016 
1017 	syncjar(jar);
1018 	a = emalloc9p(sizeof(Aux));
1019 	r->fid->aux = a;
1020 	if(r->ifcall.mode&OTRUNC){
1021 		a->ctext = emalloc9p(1);
1022 		a->ctext[0] = '\0';
1023 	}else{
1024 		sz = 256*jar->nc+1024;	/* BUG should do better */
1025 		a->ctext = emalloc9p(sz);
1026 		a->ctext[0] = '\0';
1027 		s = a->ctext;
1028 		es = s+sz;
1029 		for(i=0; i<jar->nc; i++)
1030 			s = seprint(s, es, "%K\n", &jar->c[i]);
1031 	}
1032 	respond(r, nil);
1033 }
1034 
1035 void
1036 cookieread(Req *r)
1037 {
1038 	Aux *a;
1039 
1040 	a = r->fid->aux;
1041 	readstr(r, a->ctext);
1042 	respond(r, nil);
1043 }
1044 
1045 void
1046 cookiewrite(Req *r)
1047 {
1048 	Aux *a;
1049 	int sz;
1050 
1051 	a = r->fid->aux;
1052 	sz = r->ifcall.count+r->ifcall.offset;
1053 	if(sz > strlen(a->ctext)){
1054 		if(sz >= MaxCtext){
1055 			respond(r, "cookie file too large");
1056 			return;
1057 		}
1058 		a->ctext = erealloc9p(a->ctext, sz+1);
1059 		a->ctext[sz] = '\0';
1060 	}
1061 	memmove(a->ctext+r->ifcall.offset, r->ifcall.data, r->ifcall.count);
1062 	r->ofcall.count = r->ifcall.count;
1063 	respond(r, nil);
1064 }
1065 
1066 void
1067 cookieclunk(Fid *fid)
1068 {
1069 	char *p, *nextp;
1070 	Aux *a;
1071 	int i;
1072 
1073 	a = fid->aux;
1074 	if(a == nil)
1075 		return;
1076 	for(i=0; i<jar->nc; i++)
1077 		jar->c[i].mark = 1;
1078 	for(p=a->ctext; *p; p=nextp){
1079 		if((nextp = strchr(p, '\n')) != nil)
1080 			*nextp++ = '\0';
1081 		else
1082 			nextp = "";
1083 		addtojar(jar, p, 0);
1084 	}
1085 	for(i=0; i<jar->nc; i++)
1086 		if(jar->c[i].mark)
1087 			delcookie(jar, &jar->c[i]);
1088 	syncjar(jar);
1089 	free(a->dom);
1090 	free(a->path);
1091 	free(a->inhttp);
1092 	free(a->outhttp);
1093 	free(a->ctext);
1094 	free(a);
1095 }
1096 
1097 void
1098 closecookies(void)
1099 {
1100 	closejar(jar);
1101 }
1102 
1103 void
1104 initcookies(char *file)
1105 {
1106 	char *home;
1107 
1108 	fmtinstall('J', jarfmt);
1109 	fmtinstall('K', cookiefmt);
1110 
1111 	if(file == nil){
1112 		home = getenv("home");
1113 		if(home == nil)
1114 			sysfatal("no cookie file specified and no $home");
1115 		file = emalloc9p(strlen(home)+30);
1116 		strcpy(file, home);
1117 		strcat(file, "/lib/webcookies");
1118 	}
1119 	jar = readjar(file);
1120 	if(jar == nil)
1121 		sysfatal("readjar: %r");
1122 }
1123 
1124 void
1125 httpsetcookie(char *hdr, char *dom, char *path)
1126 {
1127 	if(path == nil)
1128 		path = "/";
1129 
1130 	parsehttp(jar, hdr, dom, path);
1131 	syncjar(jar);
1132 }
1133 
1134 char*
1135 httpcookies(char *dom, char *path, int issecure)
1136 {
1137 	char buf[1024];
1138 	Jar *j;
1139 
1140 	syncjar(jar);
1141 	j = cookiesearch(jar, dom, path, issecure);
1142 	snprint(buf, sizeof buf, "%J", j);
1143 	closejar(j);
1144 	return estrdup(buf);
1145 }
1146