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