/*
* Cookie file system.  Allows hget and multiple webfs's to collaborate.
* Conventionally mounted on /mnt/webcookies.
*/

#include <u.h>
#include <libc.h>
#include <bio.h>
#include <ndb.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <ctype.h>

int debug = 0;

typedef struct Cookie Cookie;
typedef struct Jar Jar;

struct Cookie
{
       /* external info */
       char*   name;
       char*   value;
       char*   dom;            /* starts with . */
       char*   path;
       char*   version;
       char*   comment;        /* optional, may be nil */

       uint    expire;         /* time of expiration: ~0 means when webcookies dies */
       int     secure;
       int     explicitdom;    /* dom was explicitly set */
       int     explicitpath;   /* path was explicitly set */
       int     netscapestyle;

       /* internal info */
       int     deleted;
       int     mark;
       int     ondisk;
};

struct Jar
{
       Cookie  *c;
       int     nc;
       int     mc;

       Qid     qid;
       int     dirty;
       char    *file;
       char    *lockfile;
};

struct {
       char    *s;
       int     offset;
       int     ishttp;
} stab[] = {
       "domain",               offsetof(Cookie, dom),          1,
       "path",                 offsetof(Cookie, path),         1,
       "name",                 offsetof(Cookie, name),         0,
       "value",                offsetof(Cookie, value),        0,
       "comment",              offsetof(Cookie, comment),      1,
       "version",              offsetof(Cookie, version),      1,
};

struct {
       char *s;
       int     offset;
} itab[] = {
       "expire",               offsetof(Cookie, expire),
       "secure",               offsetof(Cookie, secure),
       "explicitdomain",       offsetof(Cookie, explicitdom),
       "explicitpath",         offsetof(Cookie, explicitpath),
       "netscapestyle",        offsetof(Cookie, netscapestyle),
};

#pragma varargck type "J"       Jar*
#pragma varargck type "K"       Cookie*

/* HTTP format */
int
jarfmt(Fmt *fmt)
{
       int i;
       Jar *jar;

       jar = va_arg(fmt->args, Jar*);
       if(jar == nil || jar->nc == 0)
               return fmtstrcpy(fmt, "");

       fmtprint(fmt, "Cookie: ");
       if(jar->c[0].version)
               fmtprint(fmt, "$Version=%s; ", jar->c[0].version);
       for(i=0; i<jar->nc; i++)
               fmtprint(fmt, "%s%s=%s", i ? "; ":"", jar->c[i].name, jar->c[i].value);
       fmtprint(fmt, "\r\n");
       return 0;
}

/* individual cookie */
int
cookiefmt(Fmt *fmt)
{
       int j, k, first;
       char *t;
       Cookie *c;

       c = va_arg(fmt->args, Cookie*);

       first = 1;
       for(j=0; j<nelem(stab); j++){
               t = *(char**)((char*)c+stab[j].offset);
               if(t == nil)
                       continue;
               if(first)
                       first = 0;
               else
                       fmtprint(fmt, " ");
               fmtprint(fmt, "%s=%q", stab[j].s, t);
       }
       for(j=0; j<nelem(itab); j++){
               k = *(int*)((char*)c+itab[j].offset);
               if(k == 0)
                       continue;
               if(first)
                       first = 0;
               else
                       fmtprint(fmt, " ");
               fmtprint(fmt, "%s=%ud", itab[j].s, k);
       }
       return 0;
}

/*
* sort cookies:
*      - alpha by name
*      - alpha by domain
*      - longer paths first, then alpha by path (RFC2109 4.3.4)
*/
int
cookiecmp(Cookie *a, Cookie *b)
{
       int i;

       if((i = strcmp(a->name, b->name)) != 0)
               return i;
       if((i = cistrcmp(a->dom, b->dom)) != 0)
               return i;
       if((i = strlen(b->path) - strlen(a->path)) != 0)
               return i;
       if((i = strcmp(a->path, b->path)) != 0)
               return i;
       return 0;
}

int
exactcookiecmp(Cookie *a, Cookie *b)
{
       int i;

       if((i = cookiecmp(a, b)) != 0)
               return i;
       if((i = strcmp(a->value, b->value)) != 0)
               return i;
       if(a->version || b->version){
               if(!a->version)
                       return -1;
               if(!b->version)
                       return 1;
               if((i = strcmp(a->version, b->version)) != 0)
                       return i;
       }
       if(a->comment || b->comment){
               if(!a->comment)
                       return -1;
               if(!b->comment)
                       return 1;
               if((i = strcmp(a->comment, b->comment)) != 0)
                       return i;
       }
       if((i = b->expire - a->expire) != 0)
               return i;
       if((i = b->secure - a->secure) != 0)
               return i;
       if((i = b->explicitdom - a->explicitdom) != 0)
               return i;
       if((i = b->explicitpath - a->explicitpath) != 0)
               return i;
       if((i = b->netscapestyle - a->netscapestyle) != 0)
               return i;

       return 0;
}

void
freecookie(Cookie *c)
{
       int i;

       for(i=0; i<nelem(stab); i++)
               free(*(char**)((char*)c+stab[i].offset));
}

void
copycookie(Cookie *c)
{
       int i;
       char **ps;

       for(i=0; i<nelem(stab); i++){
               ps = (char**)((char*)c+stab[i].offset);
               if(*ps)
                       *ps = estrdup9p(*ps);
       }
}

void
delcookie(Jar *j, Cookie *c)
{
       int i;

       j->dirty = 1;
       i = c - j->c;
       if(i < 0 || i >= j->nc)
               abort();
       c->deleted = 1;
}

void
addcookie(Jar *j, Cookie *c)
{
       int i;

       if(!c->name || !c->value || !c->path || !c->dom){
               fprint(2, "not adding incomplete cookie\n");
               return;
       }

       if(debug)
               fprint(2, "add %K\n", c);

       for(i=0; i<j->nc; i++)
               if(cookiecmp(&j->c[i], c) == 0){
                       if(debug)
                               fprint(2, "cookie %K matches %K\n", &j->c[i], c);
                       if(exactcookiecmp(&j->c[i], c) == 0){
                               if(debug)
                                       fprint(2, "\texactly\n");
                               j->c[i].mark = 0;
                               return;
                       }
                       delcookie(j, &j->c[i]);
               }

       j->dirty = 1;
       if(j->nc == j->mc){
               j->mc += 16;
               j->c = erealloc9p(j->c, j->mc*sizeof(Cookie));
       }
       j->c[j->nc] = *c;
       copycookie(&j->c[j->nc]);
       j->nc++;
}

void
purgejar(Jar *j)
{
       int i;

       for(i=j->nc-1; i>=0; i--){
               if(!j->c[i].deleted)
                       continue;
               freecookie(&j->c[i]);
               --j->nc;
               j->c[i] = j->c[j->nc];
       }
}

void
addtojar(Jar *jar, char *line, int ondisk)
{
       Cookie c;
       int i, j, nf, *pint;
       char *f[20], *attr, *val, **pstr;

       memset(&c, 0, sizeof c);
       c.expire = ~0;
       c.ondisk = ondisk;
       nf = tokenize(line, f, nelem(f));
       for(i=0; i<nf; i++){
               attr = f[i];
               if((val = strchr(attr, '=')) != nil)
                       *val++ = '\0';
               else
                       val = "";
               /* string attributes */
               for(j=0; j<nelem(stab); j++){
                       if(strcmp(stab[j].s, attr) == 0){
                               pstr = (char**)((char*)&c+stab[j].offset);
                               *pstr = val;
                       }
               }
               /* integer attributes */
               for(j=0; j<nelem(itab); j++){
                       if(strcmp(itab[j].s, attr) == 0){
                               pint = (int*)((char*)&c+itab[j].offset);
                               if(val[0]=='\0')
                                       *pint = 1;
                               else
                                       *pint = strtoul(val, 0, 0);
                       }
               }
       }
       if(c.name==nil || c.value==nil || c.dom==nil || c.path==nil){
               if(debug)
                       fprint(2, "ignoring fractional cookie %K\n", &c);
               return;
       }
       addcookie(jar, &c);
}

Jar*
newjar(void)
{
       Jar *jar;

       jar = emalloc9p(sizeof(Jar));
       return jar;
}

int
expirejar(Jar *jar, int exiting)
{
       int i, n;
       uint now;

       now = time(0);
       n = 0;
       for(i=0; i<jar->nc; i++){
               if(jar->c[i].expire < now || (exiting && jar->c[i].expire==~0)){
                       delcookie(jar, &jar->c[i]);
                       n++;
               }
       }
       return n;
}

int
syncjar(Jar *jar)
{
       int i, fd, doread, dowrite;
       char *line;
       Biobuf *b;
       Dir *d;
       Qid q;

       if(jar->file==nil)
               return 0;

       doread = 0;
       dowrite = jar->dirty;

       q = jar->qid;
       if((d = dirstat(jar->file)) == nil)
               dowrite = 1;
       else {
               if(q.path != d->qid.path || q.vers != d->qid.vers){
                       q = d->qid;
                       doread = 1;
               }
               free(d);
       }

       if(!doread && !dowrite)
               return 0;

       fd = -1;
       for(i=0; i<50; i++){
               if((fd = create(jar->lockfile, OWRITE, DMEXCL|0600)) < 0){
                       sleep(100);
                       continue;
               }
               break;
       }
       if(fd < 0){
               if(debug)
                       fprint(2, "open %s: %r", jar->lockfile);
               werrstr("cannot acquire jar lock: %r");
               return -1;
       }

       if(doread){
               for(i=0; i<jar->nc; i++)        /* mark is cleared by addcookie */
                       jar->c[i].mark = jar->c[i].ondisk;

               if((b = Bopen(jar->file, OREAD)) == nil){
                       if(debug)
                               fprint(2, "Bopen %s: %r", jar->file);
                       werrstr("cannot read cookie file %s: %r", jar->file);
                       close(fd);
                       return -1;
               }
               for(; (line = Brdstr(b, '\n', 1)) != nil; free(line)){
                       if(*line == '#')
                               continue;
                       addtojar(jar, line, 1);
               }
               Bterm(b);

               for(i=0; i<jar->nc; i++)
                       if(jar->c[i].mark)
                               delcookie(jar, &jar->c[i]);
       }

       purgejar(jar);

       if(dowrite){
               i = create(jar->file, OWRITE, 0600);
               if(i < 0 || (b = Bfdopen(i, OWRITE)) == nil){
                       if(debug)
                               fprint(2, "Bopen write %s: %r", jar->file);
                       if(i >= 0)
                               close(i);
                       close(fd);
                       return -1;
               }
               Bprint(b, "# webcookies cookie jar\n");
               Bprint(b, "# comments and non-standard fields will be lost\n");
               for(i=0; i<jar->nc; i++){
                       if(jar->c[i].expire == ~0)
                               continue;
                       Bprint(b, "%K\n", &jar->c[i]);
                       jar->c[i].ondisk = 1;
               }
               Bflush(b);
               if((d = dirfstat(Bfildes(b))) != nil){
                       q = d->qid;
                       free(d);
               }
               Bterm(b);
       }

       jar->qid = q;
       jar->dirty = 0;

       close(fd);
       return 0;
}

void
closejar(Jar *jar)
{
       int i;

       if(jar == nil)
               return;
       expirejar(jar, 0);
       if(jar->dirty)
               if(syncjar(jar) < 0)
                       fprint(2, "warning: cannot rewrite cookie jar: %r\n");

       for(i=0; i<jar->nc; i++)
               freecookie(&jar->c[i]);

       free(jar->lockfile);
       free(jar->file);
       free(jar->c);
       free(jar);
}

Jar*
readjar(char *file)
{
       char *lock, *p;
       Jar *jar;

       jar = newjar();
       file = estrdup9p(file);
       lock = emalloc9p(strlen(file)+10);
       strcpy(lock, file);
       if((p = strrchr(lock, '/')) != nil)
               p++;
       else
               p = lock;
       memmove(p+2, p, strlen(p)+1);
       p[0] = 'L';
       p[1] = '.';
       jar->lockfile = lock;
       jar->file = file;
       jar->dirty = 0;

       if(syncjar(jar) < 0){
               closejar(jar);
               return nil;
       }
       return jar;
}


/*
* Domain name matching is per RFC2109, section 2:
*
* Hosts names can be specified either as an IP address or a FQHN
* string.  Sometimes we compare one host name with another.  Host A's
* name domain-matches host B's if
*
* * both host names are IP addresses and their host name strings match
*   exactly; or
*
* * both host names are FQDN strings and their host name strings match
*   exactly; or
*
* * A is a FQDN string and has the form NB, where N is a non-empty name
*   string, B has the form .B', and B' is a FQDN string.  (So, x.y.com
*   domain-matches .y.com but not y.com.)
*
* Note that domain-match is not a commutative operation: a.b.c.com
* domain-matches .c.com, but not the reverse.
*
* (This does not verify that IP addresses and FQDN's are well-formed.)
*/
int
isdomainmatch(char *name, char *pattern)
{
       int lname, lpattern;

       if(cistrcmp(name, pattern + (pattern[0]=='.'))==0)
               return 1;

       if(strcmp(ipattr(name), "dom")==0 && pattern[0]=='.'){
               lname = strlen(name);
               lpattern = strlen(pattern);
               if(lname >= lpattern && cistrcmp(name+lname-lpattern, pattern)==0)
                       return 1;
       }

       return 0;
}

/*
* RFC2109 4.3.4:
*      - domain must match
*      - path in cookie must be a prefix of request path
*      - cookie must not have expired
*/
int
iscookiematch(Cookie *c, char *dom, char *path, uint now)
{
       return isdomainmatch(dom, c->dom)
               && strncmp(c->path, path, strlen(c->path))==0
               && c->expire >= now;
}

/*
* Produce a subjar of matching cookies.
* Secure cookies are only included if secure is set.
*/
Jar*
cookiesearch(Jar *jar, char *dom, char *path, int issecure)
{
       int i;
       Jar *j;
       Cookie *c;
       uint now;

       now = time(0);
       j = newjar();
       for(i=0; i<jar->nc; i++){
               c = &jar->c[i];
               if(!c->deleted && (issecure || !c->secure) && iscookiematch(c, dom, path, now))
                       addcookie(j, c);
       }
       if(j->nc == 0){
               closejar(j);
               werrstr("no cookies found");
               return nil;
       }
       qsort(j->c, j->nc, sizeof(j->c[0]), (int(*)(const void*, const void*))cookiecmp);
       return j;
}

/*
* RFC2109 4.3.2 security checks
*/
char*
isbadcookie(Cookie *c, char *dom, char *path)
{
       if(strncmp(c->path, path, strlen(c->path)) != 0)
               return "cookie path is not a prefix of the request path";

       if(c->explicitdom && c->dom[0] != '.')
               return "cookie domain doesn't start with dot";

       if(strlen(c->dom)<=2 || memchr(c->dom+1, '.', strlen(c->dom)-2) == nil)
               return "cookie domain doesn't have embedded dots";

       if(!isdomainmatch(dom, c->dom))
               return "request host does not match cookie domain";

       if(strcmp(ipattr(dom), "dom")==0 && strlen(dom)>strlen(c->dom)
       && memchr(dom, '.', strlen(dom)-strlen(c->dom)) != nil)
               return "request host contains dots before cookie domain";

       return 0;
}

/*
* Sunday, 25-Jan-2002 12:24:36 GMT
* Sunday, 25 Jan 2002 12:24:36 GMT
* Sun, 25 Jan 02 12:24:36 GMT
*/
int
isleap(int year)
{
       return year%4==0 && (year%100!=0 || year%400==0);
}

uint
strtotime(char *s)
{
       char *os;
       int i;
       Tm tm;

       static int mday[2][12] = {
               31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
               31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
       };
       static char *wday[] = {
               "Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday",
       };
       static char *mon[] = {
               "Jan", "Feb", "Mar", "Apr", "May", "Jun",
               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
       };

       memset(&tm, 0, sizeof(tm));

       os = s;
       /* Sunday, */
       for(i=0; i<nelem(wday); i++){
               if(cistrncmp(s, wday[i], strlen(wday[i])) == 0){
                       s += strlen(wday[i]);
                       break;
               }
               if(cistrncmp(s, wday[i], 3) == 0){
                       s += 3;
                       break;
               }
       }
       if(i==nelem(wday)){
               if(debug)
                       fprint(2, "bad wday (%s)\n", os);
               return -1;
       }
       if(*s++ != ',' || *s++ != ' '){
               if(debug)
                       fprint(2, "bad wday separator (%s)\n", os);
               return -1;
       }

       /* 25- */
       if(!isdigit(s[0]) || !isdigit(s[1]) || (s[2]!='-' && s[2]!=' ')){
               if(debug)
                       fprint(2, "bad day of month (%s)\n", os);
               return -1;
       }
       tm.mday = strtol(s, 0, 10);
       s += 3;

       /* Jan- */
       for(i=0; i<nelem(mon); i++)
               if(cistrncmp(s, mon[i], 3) == 0){
                       tm.mon = i;
                       s += 3;
                       break;
               }
       if(i==nelem(mon)){
               if(debug)
                       fprint(2, "bad month (%s)\n", os);
               return -1;
       }
       if(s[0] != '-' && s[0] != ' '){
               if(debug)
                       fprint(2, "bad month separator (%s)\n", os);
               return -1;
       }
       s++;

       /* 2002 */
       if(!isdigit(s[0]) || !isdigit(s[1])){
               if(debug)
                       fprint(2, "bad year (%s)\n", os);
               return -1;
       }
       tm.year = strtol(s, 0, 10);
       s += 2;
       if(isdigit(s[0]) && isdigit(s[1]))
               s += 2;
       else{
               if(tm.year <= 68)
                       tm.year += 2000;
               else
                       tm.year += 1900;
       }
       if(tm.mday==0 || tm.mday > mday[isleap(tm.year)][tm.mon]){
               if(debug)
                       fprint(2, "invalid day of month (%s)\n", os);
               return -1;
       }
       tm.year -= 1900;
       if(*s++ != ' '){
               if(debug)
                       fprint(2, "bad year separator (%s)\n", os);
               return -1;
       }

       if(!isdigit(s[0]) || !isdigit(s[1]) || s[2]!=':'
       || !isdigit(s[3]) || !isdigit(s[4]) || s[5]!=':'
       || !isdigit(s[6]) || !isdigit(s[7]) || s[8]!=' '){
               if(debug)
                       fprint(2, "bad time (%s)\n", os);
               return -1;
       }

       tm.hour = strtol(s, 0, 10);
       tm.min = strtol(s+3, 0, 10);
       tm.sec = strtol(s+6, 0, 10);
       if(tm.hour >= 24 || tm.min >= 60 || tm.sec >= 60){
               if(debug)
                       fprint(2, "invalid time (%s)\n", os);
               return -1;
       }
       s += 9;

       if(cistrcmp(s, "GMT") != 0){
               if(debug)
                       fprint(2, "time zone not GMT (%s)\n", os);
               return -1;
       }
       strcpy(tm.zone, "GMT");
       tm.yday = 0;
       return tm2sec(&tm);
}

/*
* skip linear whitespace.  we're a bit more lenient than RFC2616 2.2.
*/
char*
skipspace(char *s)
{
       while(*s=='\r' || *s=='\n' || *s==' ' || *s=='\t')
               s++;
       return s;
}

/*
* Try to identify old netscape headers.
* The old headers:
*      - didn't allow spaces around the '='
*      - used an 'Expires' attribute
*      - had no 'Version' attribute
*      - had no quotes
*      - allowed whitespace in values
*      - apparently separated attr/value pairs with ';' exclusively
*/
int
isnetscape(char *hdr)
{
       char *s;

       for(s=hdr; (s=strchr(s, '=')) != nil; s++){
               if(isspace(s[1]) || (s > hdr && isspace(s[-1])))
                       return 0;
               if(s[1]=='"')
                       return 0;
       }
       if(cistrstr(hdr, "version="))
               return 0;
       return 1;
}

/*
* Parse HTTP response headers, adding cookies to jar.
* Overwrites the headers.  May overwrite path.
*/
char* parsecookie(Cookie*, char*, char**, int, char*, char*);
int
parsehttp(Jar *jar, char *hdr, char *dom, char *path)
{
       static char setcookie[] = "Set-Cookie:";
       char *e, *p, *nextp;
       Cookie c;
       int isns, n;

       isns = isnetscape(hdr);
       n = 0;
       for(p=hdr; p; p=nextp){
               p = skipspace(p);
               if(*p == '\0')
                       break;
               nextp = strchr(p, '\n');
               if(nextp != nil)
                       *nextp++ = '\0';
               if(debug)
                       fprint(2, "?%s\n", p);
               if(cistrncmp(p, setcookie, strlen(setcookie)) != 0)
                       continue;
               if(debug)
                       fprint(2, "%s\n", p);
               p = skipspace(p+strlen(setcookie));
               for(; *p; p=skipspace(p)){
                       if((e = parsecookie(&c, p, &p, isns, dom, path)) != nil){
                               if(debug)
                                       fprint(2, "parse cookie: %s\n", e);
                               break;
                       }
                       if((e = isbadcookie(&c, dom, path)) != nil){
                               if(debug)
                                       fprint(2, "reject cookie; %s\n", e);
                               continue;
                       }
                       addcookie(jar, &c);
                       n++;
               }
       }
       return n;
}

static char*
skipquoted(char *s)
{
       /*
        * Sec 2.2 of RFC2616 defines a "quoted-string" as:
        *
        *  quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
        *  qdtext         = <any TEXT except <">>
        *  quoted-pair    = "\" CHAR
        *
        * TEXT is any octet except CTLs, but including LWS;
        * LWS is [CR LF] 1*(SP | HT);
        * CHARs are ASCII octets 0-127;  (NOTE: we reject 0's)
        * CTLs are octets 0-31 and 127;
        */
       if(*s != '"')
               return s;

       for(s++; 32 <= *s && *s < 127 && *s != '"'; s++)
               if(*s == '\\' && *(s+1) != '\0')
                       s++;
       return s;
}

static char*
skiptoken(char *s)
{
       /*
        * Sec 2.2 of RFC2616 defines a "token" as
        *  1*<any CHAR except CTLs or separators>;
        * CHARs are ASCII octets 0-127;
        * CTLs are octets 0-31 and 127;
        * separators are "()<>@,;:\/[]?={}", double-quote, SP (32), and HT (9)
        */
       while(32 <= *s && *s < 127 && strchr("()<>@,;:[]?={}\" \t\\", *s)==nil)
               s++;

       return s;
}

static char*
skipvalue(char *s, int isns)
{
       char *t;

       /*
        * An RFC2109 value is an HTTP token or an HTTP quoted string.
        * Netscape servers ignore the spec and rely on semicolons, apparently.
        */
       if(isns){
               if((t = strchr(s, ';')) == nil)
                       t = s+strlen(s);
               return t;
       }
       if(*s == '"')
               return skipquoted(s);
       return skiptoken(s);
}

/*
* RMID=80b186bb64c03c65fab767f8; expires=Monday, 10-Feb-2003 04:44:39 GMT;
*      path=/; domain=.nytimes.com
*/
char*
parsecookie(Cookie *c, char *p, char **e, int isns, char *dom, char *path)
{
       int i, done;
       char *t, *u, *attr, *val;

       memset(c, 0, sizeof *c);
       c->expire = ~0;

       /* NAME=VALUE */
       t = skiptoken(p);
       c->name = p;
       p = skipspace(t);
       if(*p != '='){
       Badname:
               return "malformed cookie: no NAME=VALUE";
       }
       *t = '\0';
       p = skipspace(p+1);
       t = skipvalue(p, isns);
       if(*t)
               *t++ = '\0';
       c->value = p;
       p = skipspace(t);
       if(c->name[0]=='\0' || c->value[0]=='\0')
               goto Badname;

       done = 0;
       for(; *p && !done; p=skipspace(p)){
               attr = p;
               t = skiptoken(p);
               u = skipspace(t);
               switch(*u){
               case '\0':
                       *t = '\0';
                       p = val = u;
                       break;
               case ';':
                       *t = '\0';
                       val = "";
                       p = u+1;
                       break;
               case '=':
                       *t = '\0';
                       val = skipspace(u+1);
                       p = skipvalue(val, isns);
                       if(*p==',')
                               done = 1;
                       if(*p)
                               *p++ = '\0';
                       break;
               case ',':
                       if(!isns){
                               val = "";
                               p = u;
                               *p++ = '\0';
                               done = 1;
                               break;
                       }
               default:
                       if(debug)
                               fprint(2, "syntax: %s\n", p);
                       return "syntax error";
               }
               for(i=0; i<nelem(stab); i++)
                       if(stab[i].ishttp && cistrcmp(stab[i].s, attr)==0)
                               *(char**)((char*)c+stab[i].offset) = val;
               if(cistrcmp(attr, "expires") == 0){
                       if(!isns)
                               return "non-netscape cookie has Expires tag";
                       if(!val[0])
                               return "bad expires tag";
                       c->expire = strtotime(val);
                       if(c->expire == ~0)
                               return "cannot parse netscape expires tag";
               }
               if(cistrcmp(attr, "max-age") == 0)
                       c->expire = time(0)+atoi(val);
               if(cistrcmp(attr, "secure") == 0)
                       c->secure = 1;
       }
       *e = p;

       if(c->dom){
               /* add leading dot for explicit domain */
               if(c->dom[0] != '.' && strcmp(ipattr(c->dom), "dom") == 0){
                       static char ddom[1024];

                       ddom[0] = '.';
                       ddom[sizeof(ddom)-1] = '\0';
                       strncpy(ddom+1, c->dom, sizeof(ddom)-2);
                       c->dom = ddom;
               }
               c->explicitdom = 1;
       }else
               c->dom = dom;
       if(c->path)
               c->explicitpath = 1;
       else {
               static char dpath[1024];

               /* implicit path is "directory" of request-uri's path component */
               dpath[sizeof(dpath)-1] = '\0';
               strncpy(dpath, path, sizeof(dpath)-1);
               if((t = strrchr(dpath, '/')) != nil)
                       t[1] = '\0';
               c->path = dpath;
       }
       c->netscapestyle = isns;

       return nil;
}

Jar *jar;

enum
{
       Xhttp = 1,
       Xcookies,

       NeedUrl = 0,
       HaveUrl,
};

typedef struct Aux Aux;
struct Aux
{
       int state;
       char *dom;
       char *path;
       char *inhttp;
       char *outhttp;
       char *ctext;
       int rdoff;
};
enum
{
       AuxBuf = 4096,
       MaxCtext = 16*1024*1024,
};

void
fsopen(Req *r)
{
       char *s, *es;
       int i, sz;
       Aux *a;

       switch((uintptr)r->fid->file->aux){
       case Xhttp:
               syncjar(jar);
               a = emalloc9p(sizeof(Aux));
               r->fid->aux = a;
               a->inhttp = emalloc9p(AuxBuf);
               a->outhttp = emalloc9p(AuxBuf);
               break;

       case Xcookies:
               syncjar(jar);
               a = emalloc9p(sizeof(Aux));
               r->fid->aux = a;
               if(r->ifcall.mode&OTRUNC){
                       a->ctext = emalloc9p(1);
                       a->ctext[0] = '\0';
               }else{
                       sz = 256*jar->nc+1024;  /* BUG should do better */
                       a->ctext = emalloc9p(sz);
                       a->ctext[0] = '\0';
                       s = a->ctext;
                       es = s+sz;
                       for(i=0; i<jar->nc; i++)
                               s = seprint(s, es, "%K\n", &jar->c[i]);
               }
               break;
       }
       respond(r, nil);
}

void
fsread(Req *r)
{
       Aux *a;

       a = r->fid->aux;
       switch((uintptr)r->fid->file->aux){
       case Xhttp:
               if(a->state == NeedUrl){
                       respond(r, "must write url before read");
                       return;
               }
               r->ifcall.offset = a->rdoff;
               readstr(r, a->outhttp);
               a->rdoff += r->ofcall.count;
               respond(r, nil);
               return;

       case Xcookies:
               readstr(r, a->ctext);
               respond(r, nil);
               return;

       default:
               respond(r, "bug in webcookies");
               return;
       }
}

void
fswrite(Req *r)
{
       Aux *a;
       int i, sz, hlen, issecure;
       char buf[1024], *p;
       Jar *j;

       a = r->fid->aux;
       switch((uintptr)r->fid->file->aux){
       case Xhttp:
               if(a->state == NeedUrl){
                       if(r->ifcall.count >= sizeof buf){
                               respond(r, "url too long");
                               return;
                       }
                       memmove(buf, r->ifcall.data, r->ifcall.count);
                       buf[r->ifcall.count] = '\0';
                       issecure = 0;
                       if(cistrncmp(buf, "http://", 7) == 0)
                               hlen = 7;
                       else if(cistrncmp(buf, "https://", 8) == 0){
                               hlen = 8;
                               issecure = 1;
                       }else{
                               respond(r, "url must begin http:// or https://");
                               return;
                       }
                       if(buf[hlen]=='/'){
                               respond(r, "url without host name");
                               return;
                       }
                       p = strchr(buf+hlen, '/');
                       if(p == nil)
                               a->path = estrdup9p("/");
                       else {
                               a->path = estrdup9p(p);
                               *p = '\0';

                               if((p = strchr(a->path, '#')) != nil)
                                       *p = '\0';
                               if((p = strchr(a->path, '?')) != nil)
                                       *p = '\0';
                       }
                       a->dom = estrdup9p(buf+hlen);
                       a->state = HaveUrl;
                       j = cookiesearch(jar, a->dom, a->path, issecure);
                       if(debug){
                               fprint(2, "search %s %s got %p\n", a->dom, a->path, j);
                               if(j){
                                       fprint(2, "%d cookies\n", j->nc);
                                       for(i=0; i<j->nc; i++)
                                               fprint(2, "%K\n", &j->c[i]);
                               }
                       }
                       snprint(a->outhttp, AuxBuf, "%J", j);
                       closejar(j);
               }else{
                       if(strlen(a->inhttp)+r->ifcall.count >= AuxBuf){
                               respond(r, "http headers too large");
                               return;
                       }
                       memmove(a->inhttp+strlen(a->inhttp), r->ifcall.data, r->ifcall.count);
               }
               r->ofcall.count = r->ifcall.count;
               respond(r, nil);
               return;

       case Xcookies:
               sz = r->ifcall.count+r->ifcall.offset;
               if(sz > strlen(a->ctext)){
                       if(sz >= MaxCtext){
                               respond(r, "cookie file too large");
                               return;
                       }
                       a->ctext = erealloc9p(a->ctext, sz+1);
                       a->ctext[sz] = '\0';
               }
               memmove(a->ctext+r->ifcall.offset, r->ifcall.data, r->ifcall.count);
               r->ofcall.count = r->ifcall.count;
               respond(r, nil);
               return;

       default:
               respond(r, "bug in webcookies");
               return;
       }
}

void
fsdestroyfid(Fid *fid)
{
       char *p, *nextp;
       Aux *a;
       int i;

       a = fid->aux;
       if(a == nil)
               return;
       switch((uintptr)fid->file->aux){
       case Xhttp:
               parsehttp(jar, a->inhttp, a->dom, a->path);
               break;
       case Xcookies:
               for(i=0; i<jar->nc; i++)
                       jar->c[i].mark = 1;
               for(p=a->ctext; *p; p=nextp){
                       if((nextp = strchr(p, '\n')) != nil)
                               *nextp++ = '\0';
                       else
                               nextp = "";
                       addtojar(jar, p, 0);
               }
               for(i=0; i<jar->nc; i++)
                       if(jar->c[i].mark)
                               delcookie(jar, &jar->c[i]);
               break;
       }
       if(jar->dirty)
               syncjar(jar);
       free(a->dom);
       free(a->path);
       free(a->inhttp);
       free(a->outhttp);
       free(a->ctext);
       free(a);
}

void
fsend(Srv*)
{
       closejar(jar);
}

Srv fs =
{
open=           fsopen,
read=           fsread,
write=          fswrite,
destroyfid=     fsdestroyfid,
end=            fsend,
};

void
usage(void)
{
       fprint(2, "usage: webcookies [-f file] [-m mtpt] [-s service]\n");
       exits("usage");
}

void
main(int argc, char **argv)
{
       char *file, *mtpt, *home, *srv;

       file = nil;
       srv = nil;
       mtpt = "/mnt/webcookies";
       ARGBEGIN{
       case 'D':
               chatty9p++;
               break;
       case 'd':
               debug = 1;
               break;
       case 'f':
               file = EARGF(usage());
               break;
       case 's':
               srv = EARGF(usage());
               break;
       case 'm':
               mtpt = EARGF(usage());
               break;
       default:
               usage();
       }ARGEND

       if(argc != 0)
               usage();

       quotefmtinstall();
       fmtinstall('J', jarfmt);
       fmtinstall('K', cookiefmt);

       if(file == nil){
               home = getenv("home");
               if(home == nil)
                       sysfatal("no cookie file specified and no $home");
               file = emalloc9p(strlen(home)+30);
               strcpy(file, home);
               strcat(file, "/lib/webcookies");
       }

       jar = readjar(file);
       if(jar == nil)
               sysfatal("readjar: %r");

       fs.tree = alloctree("cookie", "cookie", DMDIR|0555, nil);
       closefile(createfile(fs.tree->root, "http", "cookie", 0666, (void*)Xhttp));
       closefile(createfile(fs.tree->root, "cookies", "cookie", 0666, (void*)Xcookies));

       postmountsrv(&fs, srv, mtpt, MREPL);
       exits(nil);
}