]> git.lizzy.rs Git - plan9front.git/blob - sys/src/cmd/webfs/cookies.c
cwfs: back to previous version
[plan9front.git] / sys / src / cmd / webfs / cookies.c
1 #include <u.h>
2 #include <libc.h>
3 #include <bio.h>
4 #include <ndb.h>
5 #include <fcall.h>
6 #include <thread.h>
7 #include <9p.h>
8 #include <ctype.h>
9 #include "dat.h"
10 #include "fns.h"
11
12 int cookiedebug;
13
14 typedef struct Cookie Cookie;
15 typedef struct Jar Jar;
16
17 struct Cookie
18 {
19         /* external info */
20         char*   name;
21         char*   value;
22         char*   dom;            /* starts with . */
23         char*   path;
24         char*   version;
25         char*   comment;                /* optional, may be nil */
26
27         uint            expire;         /* time of expiration: ~0 means when webcookies dies */
28         int             secure;
29         int             explicitdom;    /* dom was explicitly set */
30         int             explicitpath;   /* path was explicitly set */
31         int             netscapestyle;
32
33         /* internal info */
34         int             deleted;
35         int             mark;
36         int             ondisk;
37 };
38
39 struct Jar
40 {
41         Cookie  *c;
42         int             nc;
43         int             mc;
44
45         Qid             qid;
46         int             dirty;
47         char            *file;
48         char            *lockfile;
49 };
50
51 struct {
52         char *s;
53         int     offset;
54         int     ishttp;
55 } stab[] = {
56         "domain",               offsetof(Cookie, dom),          1,
57         "path",         offsetof(Cookie, path),         1,
58         "name",         offsetof(Cookie, name),         0,
59         "value",                offsetof(Cookie, value),                0,
60         "comment",      offsetof(Cookie, comment),      1,
61         "version",              offsetof(Cookie, version),              1,
62 };
63
64 struct {
65         char *s;
66         int     offset;
67 } itab[] = {
68         "expire",                       offsetof(Cookie, expire),
69         "secure",                       offsetof(Cookie, secure),
70         "explicitdomain",       offsetof(Cookie, explicitdom),
71         "explicitpath",         offsetof(Cookie, explicitpath),
72         "netscapestyle",        offsetof(Cookie, netscapestyle),
73 };
74
75 #pragma varargck type "J"       Jar*
76 #pragma varargck type "K"       Cookie*
77
78 /* HTTP format */
79 static int
80 jarfmt(Fmt *fp)
81 {
82         int i;
83         Jar *jar;
84
85         jar = va_arg(fp->args, Jar*);
86
87         if(jar == nil || jar->nc == 0)
88                 return 0;
89
90         fmtstrcpy(fp, "Cookie: ");
91         if(jar->c[0].version)
92                 fmtprint(fp, "$Version=%s; ", jar->c[0].version);
93         for(i=0; i<jar->nc; i++)
94                 fmtprint(fp, "%s%s=%s", i ? "; ": "", jar->c[i].name, jar->c[i].value);
95         fmtstrcpy(fp, "\r\n");
96         return 0;
97 }
98
99 /* individual cookie */
100 static int
101 cookiefmt(Fmt *fp)
102 {
103         int j, k, first;
104         char *t;
105         Cookie *c;
106
107         c = va_arg(fp->args, Cookie*);
108
109         first = 1;
110         for(j=0; j<nelem(stab); j++){
111                 t = *(char**)((uintptr)c+stab[j].offset);
112                 if(t == nil)
113                         continue;
114                 if(first)
115                         first = 0;
116                 else
117                         fmtstrcpy(fp, " ");
118                 fmtprint(fp, "%s=%q", stab[j].s, t);
119         }
120         for(j=0; j<nelem(itab); j++){
121                 k = *(int*)((uintptr)c+itab[j].offset);
122                 if(k == 0)
123                         continue;
124                 if(first)
125                         first = 0;
126                 else
127                         fmtstrcpy(fp, " ");
128                 fmtprint(fp, "%s=%ud", itab[j].s, k);
129         }
130         return 0;
131 }
132
133 /*
134  * sort cookies:
135  *      - alpha by name
136  *      - alpha by domain
137  *      - longer paths first, then alpha by path (RFC2109 4.3.4)
138  */
139 static int
140 cookiecmp(Cookie *a, Cookie *b)
141 {
142         int i;
143
144         if((i = strcmp(a->name, b->name)) != 0)
145                 return i;
146         if((i = cistrcmp(a->dom, b->dom)) != 0)
147                 return i;
148         if((i = strlen(b->path) - strlen(a->path)) != 0)
149                 return i;
150         if((i = strcmp(a->path, b->path)) != 0)
151                 return i;
152         return 0;
153 }
154
155 static int
156 exactcookiecmp(Cookie *a, Cookie *b)
157 {
158         int i;
159
160         if((i = cookiecmp(a, b)) != 0)
161                 return i;
162         if((i = strcmp(a->value, b->value)) != 0)
163                 return i;
164         if(a->version || b->version){
165                 if(!a->version)
166                         return -1;
167                 if(!b->version)
168                         return 1;
169                 if((i = strcmp(a->version, b->version)) != 0)
170                         return i;
171         }
172         if(a->comment || b->comment){
173                 if(!a->comment)
174                         return -1;
175                 if(!b->comment)
176                         return 1;
177                 if((i = strcmp(a->comment, b->comment)) != 0)
178                         return i;
179         }
180         if((i = b->expire - a->expire) != 0)
181                 return i;
182         if((i = b->secure - a->secure) != 0)
183                 return i;
184         if((i = b->explicitdom - a->explicitdom) != 0)
185                 return i;
186         if((i = b->explicitpath - a->explicitpath) != 0)
187                 return i;
188         if((i = b->netscapestyle - a->netscapestyle) != 0)
189                 return i;
190
191         return 0;
192 }
193
194 static void
195 freecookie(Cookie *c)
196 {
197         int i;
198
199         for(i=0; i<nelem(stab); i++)
200                 free(*(char**)((uintptr)c+stab[i].offset));
201 }
202
203 static void
204 copycookie(Cookie *c)
205 {
206         int i;
207         char **ps;
208
209         for(i=0; i<nelem(stab); i++){
210                 ps = (char**)((uintptr)c+stab[i].offset);
211                 if(*ps)
212                         *ps = estrdup9p(*ps);
213         }
214 }
215
216 static void
217 delcookie(Jar *j, Cookie *c)
218 {
219         int i;
220
221         j->dirty = 1;
222         i = c - j->c;
223         if(i < 0 || i >= j->nc)
224                 abort();
225         c->deleted = 1;
226 }
227
228 static void
229 addcookie(Jar *j, Cookie *c)
230 {
231         int i;
232
233         if(!c->name || !c->value || !c->path || !c->dom){
234                 fprint(2, "not adding incomplete cookie\n");
235                 return;
236         }
237
238         if(cookiedebug)
239                 fprint(2, "add %K\n", c);
240
241         for(i=0; i<j->nc; i++)
242                 if(cookiecmp(&j->c[i], c) == 0){
243                         if(cookiedebug)
244                                 fprint(2, "cookie %K matches %K\n", &j->c[i], c);
245                         if(exactcookiecmp(&j->c[i], c) == 0){
246                                 if(cookiedebug)
247                                         fprint(2, "\texactly\n");
248                                 j->c[i].mark = 0;
249                                 return;
250                         }
251                         delcookie(j, &j->c[i]);
252                 }
253
254         j->dirty = 1;
255         if(j->nc == j->mc){
256                 j->mc += 16;
257                 j->c = erealloc9p(j->c, j->mc*sizeof(Cookie));
258         }
259         j->c[j->nc] = *c;
260         copycookie(&j->c[j->nc]);
261         j->nc++;
262 }
263
264 static void
265 purgejar(Jar *j)
266 {
267         int i;
268
269         for(i=j->nc-1; i>=0; i--){
270                 if(!j->c[i].deleted)
271                         continue;
272                 freecookie(&j->c[i]);
273                 --j->nc;
274                 j->c[i] = j->c[j->nc];
275         }
276 }
277
278 static void
279 addtojar(Jar *jar, char *line, int ondisk)
280 {
281         Cookie c;
282         int i, j, nf, *pint;
283         char *f[20], *attr, *val, **pstr;
284         
285         memset(&c, 0, sizeof c);
286         c.expire = ~0;
287         c.ondisk = ondisk;
288         nf = tokenize(line, f, nelem(f));
289         for(i=0; i<nf; i++){
290                 attr = f[i];
291                 if((val = strchr(attr, '=')) != nil)
292                         *val++ = '\0';
293                 else
294                         val = "";
295                 /* string attributes */
296                 for(j=0; j<nelem(stab); j++){
297                         if(strcmp(stab[j].s, attr) == 0){
298                                 pstr = (char**)((uintptr)&c+stab[j].offset);
299                                 *pstr = val;
300                         }
301                 }
302                 /* integer attributes */
303                 for(j=0; j<nelem(itab); j++){
304                         if(strcmp(itab[j].s, attr) == 0){
305                                 pint = (int*)((uintptr)&c+itab[j].offset);
306                                 if(val[0]=='\0')
307                                         *pint = 1;
308                                 else
309                                         *pint = strtoul(val, 0, 0);
310                         }
311                 }
312         }
313         if(c.name==nil || c.value==nil || c.dom==nil || c.path==nil){
314                 if(cookiedebug)
315                         fprint(2, "ignoring fractional cookie %K\n", &c);
316                 return;
317         }
318         addcookie(jar, &c);
319 }
320
321 static Jar*
322 newjar(void)
323 {
324         Jar *jar;
325
326         jar = emalloc9p(sizeof(Jar));
327         return jar;
328 }
329
330 static int
331 expirejar(Jar *jar, int exiting)
332 {
333         int i, n;
334         uint now;
335
336         now = time(0);
337         n = 0;
338         for(i=0; i<jar->nc; i++){
339                 if(jar->c[i].expire < now || (exiting && jar->c[i].expire==~0)){
340                         delcookie(jar, &jar->c[i]);
341                         n++;
342                 }
343         }
344         return n;
345 }
346
347 static void
348 dumpjar(Jar *jar, char *desc)
349 {
350         int i;
351         Biobuf *b;
352         char *s;
353
354         print("%s\n", desc);
355         print("\tin memory:\n");
356         
357         for(i=0; i<jar->nc; i++)
358                 print("\t%K%s%s%s\n", &jar->c[i],
359                         jar->c[i].ondisk ? " ondisk" : "",
360                         jar->c[i].deleted ? " deleted" : "",
361                         jar->c[i].mark ? " mark" : "");
362         print("\n\ton disk:\n");
363         if((b = Bopen(jar->file, OREAD)) == nil){
364                 print("\tno file\n");
365         }else{
366                 while((s = Brdstr(b, '\n', 1)) != nil){
367                         print("\t%s\n", s);
368                         free(s);
369                 }
370                 Bterm(b);
371         }
372         print("\n");
373 }
374
375 static int
376 syncjar(Jar *jar)
377 {
378         int i, fd;
379         char *line;
380         Dir *d;
381         Biobuf *b;
382         Qid q;
383
384         if(jar->file==nil)
385                 return 0;
386
387         memset(&q, 0, sizeof q);
388         if((d = dirstat(jar->file)) != nil){
389                 q = d->qid;
390                 if(d->qid.path != jar->qid.path || d->qid.vers != jar->qid.vers)
391                         jar->dirty = 1;
392                 free(d);
393         }
394
395         if(jar->dirty == 0)
396                 return 0;
397
398         fd = -1;
399         for(i=0; i<50; i++){
400                 if((fd = create(jar->lockfile, OWRITE, DMEXCL|0666)) < 0){
401                         sleep(100);
402                         continue;
403                 }
404                 break;
405         }
406         if(fd < 0){
407                 if(cookiedebug)
408                         fprint(2, "open %s: %r", jar->lockfile);
409                 werrstr("cannot acquire jar lock: %r");
410                 return -1;
411         }
412
413         for(i=0; i<jar->nc; i++)        /* mark is cleared by addcookie */
414                 jar->c[i].mark = jar->c[i].ondisk;
415
416         if((b = Bopen(jar->file, OREAD)) == nil){
417                 if(cookiedebug)
418                         fprint(2, "Bopen %s: %r", jar->file);
419                 werrstr("cannot read cookie file %s: %r", jar->file);
420                 close(fd);
421                 return -1;
422         }
423         for(; (line = Brdstr(b, '\n', 1)) != nil; free(line)){
424                 if(*line == '#')
425                         continue;
426                 addtojar(jar, line, 1);
427         }
428         Bterm(b);
429
430         for(i=0; i<jar->nc; i++)
431                 if(jar->c[i].mark && jar->c[i].expire != ~0)
432                         delcookie(jar, &jar->c[i]);
433
434         purgejar(jar);
435
436         b = Bopen(jar->file, OWRITE);
437         if(b == nil){
438                 if(cookiedebug)
439                         fprint(2, "Bopen write %s: %r", jar->file);
440                 close(fd);
441                 return -1;
442         }
443         Bprint(b, "# webcookies cookie jar\n");
444         Bprint(b, "# comments and non-standard fields will be lost\n");
445         for(i=0; i<jar->nc; i++){
446                 if(jar->c[i].expire == ~0)
447                         continue;
448                 Bprint(b, "%K\n", &jar->c[i]);
449                 jar->c[i].ondisk = 1;
450         }
451         Bterm(b);
452
453         jar->dirty = 0;
454         close(fd);
455         if((d = dirstat(jar->file)) != nil){
456                 jar->qid = d->qid;
457                 free(d);
458         }
459         return 0;
460 }
461
462 static Jar*
463 readjar(char *file)
464 {
465         char *lock, *p;
466         Jar *jar;
467
468         jar = newjar();
469         lock = emalloc9p(strlen(file)+10);
470         strcpy(lock, file);
471         if((p = strrchr(lock, '/')) != nil)
472                 p++;
473         else
474                 p = lock;
475         memmove(p+2, p, strlen(p)+1);
476         p[0] = 'L';
477         p[1] = '.';
478         jar->lockfile = lock;
479         jar->file = file;
480         jar->dirty = 1;
481
482         if(syncjar(jar) < 0){
483                 free(jar->file);
484                 free(jar->lockfile);
485                 free(jar);
486                 return nil;
487         }
488         return jar;
489 }
490
491 static void
492 closejar(Jar *jar)
493 {
494         int i;
495
496         if(jar == nil)
497                 return;
498         expirejar(jar, 0);
499         if(syncjar(jar) < 0)
500                 fprint(2, "warning: cannot rewrite cookie jar: %r\n");
501
502         for(i=0; i<jar->nc; i++)
503                 freecookie(&jar->c[i]);
504
505         free(jar->file);
506         free(jar);      
507 }
508
509 /*
510  * Domain name matching is per RFC2109, section 2:
511  *
512  * Hosts names can be specified either as an IP address or a FQHN
513  * string.  Sometimes we compare one host name with another.  Host A's
514  * name domain-matches host B's if
515  *
516  * * both host names are IP addresses and their host name strings match
517  *   exactly; or
518  *
519  * * both host names are FQDN strings and their host name strings match
520  *   exactly; or
521  *
522  * * A is a FQDN string and has the form NB, where N is a non-empty name
523  *   string, B has the form .B', and B' is a FQDN string.  (So, x.y.com
524  *   domain-matches .y.com but not y.com.)
525  *
526  * Note that domain-match is not a commutative operation: a.b.c.com
527  * domain-matches .c.com, but not the reverse.
528  *
529  * (This does not verify that IP addresses and FQDN's are well-formed.)
530  */
531 static int
532 isdomainmatch(char *name, char *pattern)
533 {
534         int lname, lpattern;
535
536         if(cistrcmp(name, pattern)==0)
537                 return 1;
538
539         if(strcmp(ipattr(name), "dom")==0 && pattern[0]=='.'){
540                 lname = strlen(name);
541                 lpattern = strlen(pattern);
542                 /* e.g., name: www.google.com && pattern: .google.com */
543                 if(lname >= lpattern && cistrcmp(name+lname-lpattern, pattern)==0)
544                         return 1;
545                 /* e.g., name: google.com && pattern: .google.com */
546                 if(lpattern > lname &&
547                     cistrcmp(pattern+lpattern-lname, name) == 0)
548                         return 1;
549         }
550         return 0;
551 }
552
553 /*
554  * RFC2109 4.3.4:
555  *      - domain must match
556  *      - path in cookie must be a prefix of request path
557  *      - cookie must not have expired
558  */
559 static int
560 iscookiematch(Cookie *c, char *dom, char *path, uint now)
561 {
562         return isdomainmatch(dom, c->dom)
563                 && strncmp(c->path, path, strlen(c->path))==0
564                 && (c->expire == 0 || c->expire >= now);
565 }
566
567 /* 
568  * Produce a subjar of matching cookies.
569  * Secure cookies are only included if secure is set.
570  */
571 static Jar*
572 cookiesearch(Jar *jar, char *dom, char *path, int issecure)
573 {
574         int i;
575         Jar *j;
576         uint now;
577
578         if(cookiedebug)
579                 fprint(2, "cookiesearch %s %s %d\n", dom, path, issecure);
580         now = time(0);
581         j = newjar();
582         for(i=0; i<jar->nc; i++){
583                 if(cookiedebug)
584                         fprint(2, "\ttry %s %s %d %s\n", jar->c[i].dom,
585                                 jar->c[i].path, jar->c[i].secure,
586                                 jar->c[i].name);
587                 if((issecure || !jar->c[i].secure) &&
588                     iscookiematch(&jar->c[i], dom, path, now)){
589                         if(cookiedebug)
590                                 fprint(2, "\tmatched\n");
591                         addcookie(j, &jar->c[i]);
592                 }
593         }
594         if(j->nc == 0){
595                 closejar(j);
596                 werrstr("no cookies found");
597                 return nil;
598         }
599         qsort(j->c, j->nc, sizeof(j->c[0]), (int(*)(void*, void*))cookiecmp);
600         return j;
601 }
602
603 /*
604  * RFC2109 4.3.2 security checks
605  */
606 static char*
607 isbadcookie(Cookie *c, char *dom, char *path)
608 {
609         int lcdom, ldom;
610
611         if(strncmp(c->path, path, strlen(c->path)) != 0)
612                 return "cookie path is not a prefix of the request path";
613
614         /*
615          * fgb says omitting this test is necessary to get some sites to work,
616          * but it seems dubious.
617          */
618         if(c->explicitdom && c->dom[0] != '.')
619                 return "cookie domain doesn't start with dot";
620
621         lcdom = strlen(c->dom);
622         if(memchr(c->dom+1, '.', lcdom-1-1) == nil)
623                 return "cookie domain doesn't have embedded dots";
624
625         if(!isdomainmatch(dom, c->dom))
626                 return "request host does not match cookie domain";
627
628         ldom = strlen(dom);
629         if(strcmp(ipattr(dom), "dom")==0 && lcdom > ldom &&
630             memchr(dom, '.', lcdom - ldom) != nil)
631                 return "request host contains dots before cookie domain";
632
633         return 0;
634 }
635
636 /*
637  * Sunday, 25-Jan-2002 12:24:36 GMT
638  * Sunday, 25 Jan 2002 12:24:36 GMT
639  * Sun, 25 Jan 02 12:24:36 GMT
640  */
641 static int
642 isleap(int year)
643 {
644         return year%4==0 && (year%100!=0 || year%400==0);
645 }
646
647 static uint
648 strtotime(char *s)
649 {
650         char *os;
651         int i;
652         Tm tm;
653
654         static int mday[2][12] = {
655                 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
656                 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
657         };
658         static char *wday[] = {
659                 "Sunday", "Monday", "Tuesday", "Wednesday",
660                 "Thursday", "Friday", "Saturday",
661         };
662         static char *mon[] = {
663                 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
664                 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
665         };
666
667         os = s;
668         /* Sunday, */
669         for(i=0; i<nelem(wday); i++){
670                 if(cistrncmp(s, wday[i], strlen(wday[i])) == 0){
671                         s += strlen(wday[i]);
672                         break;
673                 }
674                 if(cistrncmp(s, wday[i], 3) == 0){
675                         s += 3;
676                         break;
677                 }
678         }
679         if(i==nelem(wday)){
680                 if(cookiedebug)
681                         fprint(2, "bad wday (%s)\n", os);
682                 return -1;
683         }
684         if(*s++ != ',' || *s++ != ' '){
685                 if(cookiedebug)
686                         fprint(2, "bad wday separator (%s)\n", os);
687                 return -1;
688         }
689
690         /* 25- */
691         if(!isdigit(s[0]) || !isdigit(s[1]) || (s[2]!='-' && s[2]!=' ')){
692                 if(cookiedebug)
693                         fprint(2, "bad day of month (%s)\n", os);
694                 return -1;
695         }
696         tm.mday = strtol(s, 0, 10);
697         s += 3;
698
699         /* Jan- */
700         for(i=0; i<nelem(mon); i++)
701                 if(cistrncmp(s, mon[i], 3) == 0){
702                         tm.mon = i;
703                         s += 3;
704                         break;
705                 }
706         if(i==nelem(mon)){
707                 if(cookiedebug)
708                         fprint(2, "bad month (%s)\n", os);
709                 return -1;
710         }
711         if(s[0] != '-' && s[0] != ' '){
712                 if(cookiedebug)
713                         fprint(2, "bad month separator (%s)\n", os);
714                 return -1;
715         }
716         s++;
717
718         /* 2002 */
719         if(!isdigit(s[0]) || !isdigit(s[1])){
720                 if(cookiedebug)
721                         fprint(2, "bad year (%s)\n", os);
722                 return -1;
723         }
724         tm.year = strtol(s, 0, 10);
725         s += 2;
726         if(isdigit(s[0]) && isdigit(s[1]))
727                 s += 2;
728         else{
729                 if(tm.year <= 68)
730                         tm.year += 2000;
731                 else
732                         tm.year += 1900;
733         }
734         if(tm.mday==0 || tm.mday > mday[isleap(tm.year)][tm.mon]){
735                 if(cookiedebug)
736                         fprint(2, "invalid day of month (%s)\n", os);
737                 return -1;
738         }
739         tm.year -= 1900;
740         if(*s++ != ' '){
741                 if(cookiedebug)
742                         fprint(2, "bad year separator (%s)\n", os);
743                 return -1;
744         }
745
746         if(!isdigit(s[0]) || !isdigit(s[1]) || s[2]!=':'
747         || !isdigit(s[3]) || !isdigit(s[4]) || s[5]!=':'
748         || !isdigit(s[6]) || !isdigit(s[7]) || s[8]!=' '){
749                 if(cookiedebug)
750                         fprint(2, "bad time (%s)\n", os);
751                 return -1;
752         }
753
754         tm.hour = atoi(s);
755         tm.min = atoi(s+3);
756         tm.sec = atoi(s+6);
757         if(tm.hour >= 24 || tm.min >= 60 || tm.sec >= 60){
758                 if(cookiedebug)
759                         fprint(2, "invalid time (%s)\n", os);
760                 return -1;
761         }
762         s += 9;
763
764         if(cistrcmp(s, "GMT") != 0){
765                 if(cookiedebug)
766                         fprint(2, "time zone not GMT (%s)\n", os);
767                 return -1;
768         }
769         strcpy(tm.zone, "GMT");
770         tm.yday = 0;
771         return tm2sec(&tm);
772 }
773
774 /*
775  * skip linear whitespace.  we're a bit more lenient than RFC2616 2.2.
776  */
777 static char*
778 skipspace(char *s)
779 {
780         while(*s=='\r' || *s=='\n' || *s==' ' || *s=='\t')
781                 s++;
782         return s;
783 }
784
785 /*
786  * Try to identify old netscape headers.
787  * The old headers:
788  *      - didn't allow spaces around the '='
789  *      - used an 'Expires' attribute
790  *      - had no 'Version' attribute
791  *      - had no quotes
792  *      - allowed whitespace in values
793  *      - apparently separated attr/value pairs with ';' exclusively
794  */
795 static int
796 isnetscape(char *hdr)
797 {
798         char *s;
799
800         for(s=hdr; (s=strchr(s, '=')) != nil; s++){
801                 if(isspace(s[1]) || (s > hdr && isspace(s[-1])))
802                         return 0;
803                 if(s[1]=='"')
804                         return 0;
805         }
806         if(cistrstr(hdr, "version="))
807                 return 0;
808         return 1;
809 }
810
811 /*
812  * Parse HTTP response headers, adding cookies to jar.
813  * Overwrites the headers.  May overwrite path.
814  */
815 static char* parsecookie(Cookie*, char*, char**, int, char*, char*);
816 static int
817 parsehttp(Jar *jar, char *hdr, char *dom, char *path)
818 {
819         static char setcookie[] = "Set-Cookie:";
820         char *e, *p, *nextp;
821         Cookie c;
822         int isns, n;
823
824         isns = isnetscape(hdr);
825         n = 0;
826         for(p=hdr; p; p=nextp){
827                 p = skipspace(p);
828                 if(*p == '\0')
829                         break;
830                 nextp = strchr(p, '\n');
831                 if(nextp != nil)
832                         *nextp++ = '\0';
833                 if(cistrncmp(p, setcookie, strlen(setcookie)) != 0)
834                         continue;
835                 if(cookiedebug)
836                         fprint(2, "%s\n", p);
837                 p = skipspace(p+strlen(setcookie));
838                 for(; *p; p=skipspace(p)){
839                         if((e = parsecookie(&c, p, &p, isns, dom, path)) != nil){
840                                 if(cookiedebug)
841                                         fprint(2, "parse cookie: %s\n", e);
842                                 break;
843                         }
844                         if((e = isbadcookie(&c, dom, path)) != nil){
845                                 if(cookiedebug)
846                                         fprint(2, "reject cookie; %s\n", e);
847                                 continue;
848                         }
849                         addcookie(jar, &c);
850                         n++;
851                 }
852         }
853         return n;
854 }
855
856 static char*
857 skipquoted(char *s)
858 {
859         /*
860          * Sec 2.2 of RFC2616 defines a "quoted-string" as:
861          *
862          *  quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
863          *  qdtext         = <any TEXT except <">>
864          *  quoted-pair    = "\" CHAR
865          *
866          * TEXT is any octet except CTLs, but including LWS;
867          * LWS is [CR LF] 1*(SP | HT);
868          * CHARs are ASCII octets 0-127;  (NOTE: we reject 0's)
869          * CTLs are octets 0-31 and 127;
870          */
871         if(*s != '"')
872                 return s;
873
874         for(s++; 32 <= *s && *s < 127 && *s != '"'; s++)
875                 if(*s == '\\' && *(s+1) != '\0')
876                         s++;
877         return s;
878 }
879
880 static char*
881 skiptoken(char *s)
882 {
883         /*
884          * Sec 2.2 of RFC2616 defines a "token" as
885          *  1*<any CHAR except CTLs or separators>;
886          * CHARs are ASCII octets 0-127;
887          * CTLs are octets 0-31 and 127;
888          * separators are "()<>@,;:\/[]?={}", double-quote, SP (32), and HT (9)
889          */
890         while(32 <= *s && *s < 127 && strchr("()<>@,;:[]?={}\" \t\\", *s)==nil)
891                 s++;
892
893         return s;
894 }
895
896 static char*
897 skipvalue(char *s, int isns)
898 {
899         char *t;
900
901         /*
902          * An RFC2109 value is an HTTP token or an HTTP quoted string.
903          * Netscape servers ignore the spec and rely on semicolons, apparently.
904          */
905         if(isns){
906                 if((t = strchr(s, ';')) == nil)
907                         t = s+strlen(s);
908                 return t;
909         }
910         if(*s == '"')
911                 return skipquoted(s);
912         return skiptoken(s);
913 }
914
915 /*
916  * RMID=80b186bb64c03c65fab767f8; expires=Monday, 10-Feb-2003 04:44:39 GMT; 
917  *      path=/; domain=.nytimes.com
918  */
919 static char*
920 parsecookie(Cookie *c, char *p, char **e, int isns, char *dom, char *path)
921 {
922         int i, done;
923         char *t, *u, *attr, *val;
924
925         c->expire = ~0;
926         memset(c, 0, sizeof *c);
927
928         /* NAME=VALUE */
929         t = skiptoken(p);
930         c->name = p;
931         p = skipspace(t);
932         if(*p != '='){
933         Badname:
934                 return "malformed cookie: no NAME=VALUE";
935         }
936         *t = '\0';
937         p = skipspace(p+1);
938         t = skipvalue(p, isns);
939         if(*t)
940                 *t++ = '\0';
941         c->value = p;
942         p = skipspace(t);
943         if(c->name[0]=='\0' || c->value[0]=='\0')
944                 goto Badname;
945
946         done = 0;
947         for(; *p && !done; p=skipspace(p)){
948                 attr = p;
949                 t = skiptoken(p);
950                 u = skipspace(t);
951                 switch(*u){
952                 case '\0':
953                         *t = '\0';
954                         val = p = u;
955                         break;
956                 case ';':
957                         *t = '\0';
958                         val = "";
959                         p = u+1;
960                         break;
961                 case '=':
962                         *t = '\0';
963                         val = skipspace(u+1);
964                         p = skipvalue(val, isns);
965                         if(*p==',')
966                                 done = 1;
967                         if(*p)
968                                 *p++ = '\0';
969                         break;
970                 case ',':
971                         if(!isns){
972                                 val = "";
973                                 p = u;
974                                 *p++ = '\0';
975                                 done = 1;
976                                 break;
977                         }
978                 default:
979                         if(cookiedebug)
980                                 fprint(2, "syntax: %s\n", p);
981                         return "syntax error";
982                 }
983                 for(i=0; i<nelem(stab); i++)
984                         if(stab[i].ishttp && cistrcmp(stab[i].s, attr)==0)
985                                 *(char**)((uintptr)c+stab[i].offset) = val;
986                 if(cistrcmp(attr, "expires") == 0){
987                         if(!isns)
988                                 return "non-netscape cookie has Expires tag";
989                         if(!val[0])
990                                 return "bad expires tag";
991                         c->expire = strtotime(val);
992                         if(c->expire == ~0)
993                                 return "cannot parse netscape expires tag";
994                 }
995                 if(cistrcmp(attr, "max-age") == 0)
996                         c->expire = time(0)+atoi(val);
997                 if(cistrcmp(attr, "secure") == 0)
998                         c->secure = 1;
999         }
1000
1001         if(c->dom)
1002                 c->explicitdom = 1;
1003         else
1004                 c->dom = dom;
1005         if(c->path)
1006                 c->explicitpath = 1;
1007         else{
1008                 c->path = path;
1009                 if((t = strchr(c->path, '?')) != 0)
1010                         *t = '\0';
1011                 if((t = strrchr(c->path, '/')) != 0)
1012                         *t = '\0';
1013         }       
1014         c->netscapestyle = isns;
1015         *e = p;
1016
1017         return nil;
1018 }
1019
1020 Jar *jar;
1021
1022 typedef struct Aux Aux;
1023 struct Aux
1024 {
1025         char *dom;
1026         char *path;
1027         char *inhttp;
1028         char *outhttp;
1029         char *ctext;
1030         int rdoff;
1031 };
1032 enum
1033 {
1034         AuxBuf = 4096,
1035         MaxCtext = 16*1024*1024,
1036 };
1037
1038 void
1039 cookieopen(Req *r)
1040 {
1041         char *s, *es;
1042         int i, sz;
1043         Aux *a;
1044
1045         syncjar(jar);
1046         a = emalloc9p(sizeof(Aux));
1047         r->fid->aux = a;
1048         if(r->ifcall.mode&OTRUNC){
1049                 a->ctext = emalloc9p(1);
1050                 a->ctext[0] = '\0';
1051         }else{
1052                 sz = 256*jar->nc+1024;  /* BUG should do better */
1053                 a->ctext = emalloc9p(sz);
1054                 a->ctext[0] = '\0';
1055                 s = a->ctext;
1056                 es = s+sz;
1057                 for(i=0; i<jar->nc; i++)
1058                         s = seprint(s, es, "%K\n", &jar->c[i]);
1059         }
1060         respond(r, nil);
1061 }
1062
1063 void
1064 cookieread(Req *r)
1065 {
1066         Aux *a;
1067
1068         a = r->fid->aux;
1069         readstr(r, a->ctext);
1070         respond(r, nil);
1071 }
1072
1073 void
1074 cookiewrite(Req *r)
1075 {
1076         Aux *a;
1077         int sz;
1078
1079         a = r->fid->aux;
1080         sz = r->ifcall.count+r->ifcall.offset;
1081         if(sz > strlen(a->ctext)){
1082                 if(sz >= MaxCtext){
1083                         respond(r, "cookie file too large");
1084                         return;
1085                 }
1086                 a->ctext = erealloc9p(a->ctext, sz+1);
1087                 a->ctext[sz] = '\0';
1088         }
1089         memmove(a->ctext+r->ifcall.offset, r->ifcall.data, r->ifcall.count);
1090         r->ofcall.count = r->ifcall.count;
1091         respond(r, nil);
1092 }
1093
1094 void
1095 cookieclunk(Fid *fid)
1096 {
1097         char *p, *nextp;
1098         Aux *a;
1099         int i;
1100
1101         a = fid->aux;
1102         if(a == nil)
1103                 return;
1104         for(i=0; i<jar->nc; i++)
1105                 jar->c[i].mark = 1;
1106         for(p=a->ctext; *p; p=nextp){
1107                 if((nextp = strchr(p, '\n')) != nil)
1108                         *nextp++ = '\0';
1109                 else
1110                         nextp = "";
1111                 addtojar(jar, p, 0);
1112         }
1113         for(i=0; i<jar->nc; i++)
1114                 if(jar->c[i].mark)
1115                         delcookie(jar, &jar->c[i]);
1116         syncjar(jar);
1117         free(a->dom);
1118         free(a->path);
1119         free(a->inhttp);
1120         free(a->outhttp);
1121         free(a->ctext);
1122         free(a);
1123 }
1124
1125 void
1126 closecookies(void)
1127 {
1128         closejar(jar);
1129 }
1130
1131 void
1132 initcookies(char *file)
1133 {
1134         char *home;
1135
1136         fmtinstall('J', jarfmt);
1137         fmtinstall('K', cookiefmt);
1138
1139         if(file == nil){
1140                 home = getenv("home");
1141                 if(home == nil)
1142                         sysfatal("no cookie file specified and no $home");
1143                 file = emalloc9p(strlen(home)+30);
1144                 strcpy(file, home);
1145                 strcat(file, "/lib/webcookies");
1146         }
1147         jar = readjar(file);
1148         if(jar == nil)
1149                 sysfatal("readjar: %r");
1150 }
1151
1152 void
1153 httpsetcookie(char *hdr, char *dom, char *path)
1154 {
1155         if(path == nil)
1156                 path = "/";
1157
1158         parsehttp(jar, hdr, dom, path);
1159         syncjar(jar);
1160 }
1161
1162 char*
1163 httpcookies(char *dom, char *path, int issecure)
1164 {
1165         char buf[1024];
1166         Jar *j;
1167
1168         syncjar(jar);
1169         j = cookiesearch(jar, dom, path, issecure);
1170         snprint(buf, sizeof buf, "%J", j);
1171         closejar(j);
1172         return estrdup(buf);
1173 }