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