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