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