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