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