1 /******************************************************************************* 2 3 copyright: Copyright (c) 2004 Kris Bell. All rights reserved 4 5 license: BSD style: $(LICENSE) 6 7 version: Initial release: April 2004 8 9 author: Kris 10 11 *******************************************************************************/ 12 13 module tango.net.http.HttpCookies; 14 15 private import tango.stdc.ctype; 16 17 private import tango.io.device.Array; 18 19 private import tango.io.model.IConduit; 20 21 private import tango.io.stream.Iterator; 22 23 private import tango.net.http.HttpHeaders; 24 25 private import Integer = tango.text.convert.Integer; 26 27 /******************************************************************************* 28 29 Defines the Cookie class, and the means for reading & writing them. 30 Cookie implementation conforms with RFC 2109, but supports parsing 31 of server-side cookies only. Client-side cookies are supported in 32 terms of output, but response parsing is not yet implemented ... 33 34 See over <A HREF="http://www.faqs.org/rfcs/rfc2109.html">here</A> 35 for the RFC document. 36 37 *******************************************************************************/ 38 39 class Cookie //: IWritable 40 { 41 const(char)[] name, 42 path, 43 value, 44 domain, 45 comment; 46 uint vrsn=1; // 'version' is a reserved word 47 bool secure=false; 48 long maxAge=long.min; 49 50 /*********************************************************************** 51 52 Construct an empty client-side cookie. You add these 53 to an output request using HttpClient.addCookie(), or 54 the equivalent. 55 56 ***********************************************************************/ 57 58 this () {} 59 60 /*********************************************************************** 61 62 Construct a cookie with the provided attributes. You add 63 these to an output request using HttpClient.addCookie(), 64 or the equivalent. 65 66 ***********************************************************************/ 67 68 this (const(char)[] name, const(char)[] value) 69 { 70 setName (name); 71 setValue (value); 72 } 73 74 /*********************************************************************** 75 76 Set the name of this cookie 77 78 ***********************************************************************/ 79 80 Cookie setName (const(char)[] name) 81 { 82 this.name = name; 83 return this; 84 } 85 86 /*********************************************************************** 87 88 Set the value of this cookie 89 90 ***********************************************************************/ 91 92 Cookie setValue (const(char)[] value) 93 { 94 this.value = value; 95 return this; 96 } 97 98 /*********************************************************************** 99 100 Set the version of this cookie 101 102 ***********************************************************************/ 103 104 Cookie setVersion (uint vrsn) 105 { 106 this.vrsn = vrsn; 107 return this; 108 } 109 110 /*********************************************************************** 111 112 Set the path of this cookie 113 114 ***********************************************************************/ 115 116 Cookie setPath (const(char)[] path) 117 { 118 this.path = path; 119 return this; 120 } 121 122 /*********************************************************************** 123 124 Set the domain of this cookie 125 126 ***********************************************************************/ 127 128 Cookie setDomain (const(char)[] domain) 129 { 130 this.domain = domain; 131 return this; 132 } 133 134 /*********************************************************************** 135 136 Set the comment associated with this cookie 137 138 ***********************************************************************/ 139 140 Cookie setComment (const(char)[] comment) 141 { 142 this.comment = comment; 143 return this; 144 } 145 146 /*********************************************************************** 147 148 Set the maximum duration of this cookie 149 150 ***********************************************************************/ 151 152 Cookie setMaxAge (long maxAge) 153 { 154 this.maxAge = maxAge; 155 return this; 156 } 157 158 /*********************************************************************** 159 160 Indicate whether this cookie should be considered secure or not 161 162 ***********************************************************************/ 163 164 Cookie setSecure (bool secure) 165 { 166 this.secure = secure; 167 return this; 168 } 169 /+ 170 /*********************************************************************** 171 172 Output the cookie as a text stream, via the provided IWriter 173 174 ***********************************************************************/ 175 176 void write (IWriter writer) 177 { 178 produce (&writer.buffer.consume); 179 } 180 +/ 181 /*********************************************************************** 182 183 Output the cookie as a text stream, via the provided consumer 184 185 ***********************************************************************/ 186 187 void produce (scope size_t delegate(const(void)[]) consume) 188 { 189 consume (name); 190 191 if (value.length) 192 consume ("="), consume (value); 193 194 if (path.length) 195 consume (";Path="), consume (path); 196 197 if (domain.length) 198 consume (";Domain="), consume (domain); 199 200 if (vrsn) 201 { 202 char[16] tmp = void; 203 204 consume (";Version="); 205 consume (Integer.format (tmp, vrsn)); 206 207 if (comment.length) 208 consume (";Comment=\""), consume(comment), consume("\""); 209 210 if (secure) 211 consume (";Secure"); 212 213 if (maxAge != maxAge.min) 214 consume (";Max-Age="c), consume (Integer.format (tmp, maxAge)); 215 } 216 } 217 218 /*********************************************************************** 219 220 Reset this cookie 221 222 ***********************************************************************/ 223 224 Cookie clear () 225 { 226 vrsn = 1; 227 secure = false; 228 maxAge = maxAge.min; 229 name = path = domain = comment = null; 230 return this; 231 } 232 } 233 234 235 236 /******************************************************************************* 237 238 Implements a stack of cookies. Each cookie is pushed onto the 239 stack by a parser, which takes its input from HttpHeaders. The 240 stack can be populated for both client and server side cookies. 241 242 *******************************************************************************/ 243 244 class CookieStack 245 { 246 private int depth; 247 private Cookie[] cookies; 248 249 /********************************************************************** 250 251 Construct a cookie stack with the specified initial extent. 252 The stack will grow as necessary over time. 253 254 **********************************************************************/ 255 256 this (int size) 257 { 258 cookies = new Cookie[0]; 259 resize (cookies, size); 260 } 261 262 /********************************************************************** 263 264 Pop the stack all the way to zero 265 266 **********************************************************************/ 267 268 final void reset () 269 { 270 depth = 0; 271 } 272 273 /********************************************************************** 274 275 Return a fresh cookie from the stack 276 277 **********************************************************************/ 278 279 final Cookie push () 280 { 281 if (depth == cookies.length) 282 resize (cookies, depth * 2); 283 return cookies [depth++]; 284 } 285 286 /********************************************************************** 287 288 Resize the stack such that it has more room. 289 290 **********************************************************************/ 291 292 private final static void resize (ref Cookie[] cookies, size_t size) 293 { 294 size_t i = cookies.length; 295 296 for (cookies.length=size; i < cookies.length; ++i) 297 cookies[i] = new Cookie(); 298 } 299 300 /********************************************************************** 301 302 Iterate over all cookies in stack 303 304 **********************************************************************/ 305 306 int opApply (scope int delegate(ref Cookie) dg) 307 { 308 int result = 0; 309 310 for (int i=0; i < depth; ++i) 311 if ((result = dg (cookies[i])) != 0) 312 break; 313 return result; 314 } 315 } 316 317 318 319 /******************************************************************************* 320 321 This is the support point for server-side cookies. It wraps a 322 CookieStack together with a set of HttpHeaders, along with the 323 appropriate cookie parser. One would do something very similar 324 for client side cookie parsing also. 325 326 *******************************************************************************/ 327 328 class HttpCookiesView //: IWritable 329 { 330 private bool parsed; 331 private CookieStack stack; 332 private CookieParser parser; 333 private HttpHeadersView headers; 334 335 /********************************************************************** 336 337 Construct cookie wrapper with the provided headers. 338 339 **********************************************************************/ 340 341 this (HttpHeadersView headers) 342 { 343 this.headers = headers; 344 345 // create a stack for parsed cookies 346 stack = new CookieStack (10); 347 348 // create a parser 349 parser = new CookieParser (stack); 350 } 351 /+ 352 /********************************************************************** 353 354 Output each of the cookies parsed to the provided IWriter. 355 356 **********************************************************************/ 357 358 void write (IWriter writer) 359 { 360 produce (&writer.buffer.consume, HttpConst.Eol); 361 } 362 +/ 363 /********************************************************************** 364 365 Output the token list to the provided consumer 366 367 **********************************************************************/ 368 369 void produce (scope size_t delegate(const(void)[]) consume, const(char)[] eol = HttpConst.Eol) 370 { 371 foreach (cookie; parse()) 372 cookie.produce (consume), consume (eol); 373 } 374 375 /********************************************************************** 376 377 Reset these cookies for another parse 378 379 **********************************************************************/ 380 381 void reset () 382 { 383 stack.reset(); 384 parsed = false; 385 } 386 387 /********************************************************************** 388 389 Parse all cookies from our HttpHeaders, pushing each onto 390 the CookieStack as we go. 391 392 **********************************************************************/ 393 394 CookieStack parse () 395 { 396 if (! parsed) 397 { 398 parsed = true; 399 400 foreach (HeaderElement header; headers) 401 if (header.name.value == HttpHeader.Cookie.value) 402 parser.parse (header.value); 403 } 404 return stack; 405 } 406 } 407 408 409 410 /******************************************************************************* 411 412 Handles a set of output cookies by writing them into the list of 413 output headers. 414 415 *******************************************************************************/ 416 417 class HttpCookies 418 { 419 private HttpHeaderName name; 420 private HttpHeaders headers; 421 422 /********************************************************************** 423 424 Construct an output cookie wrapper upon the provided 425 output headers. Each cookie added is converted to an 426 addition to those headers. 427 428 **********************************************************************/ 429 430 this (HttpHeaders headers, const(HttpHeaderName) name = HttpHeader.SetCookie) 431 { 432 this.headers = headers; 433 this.name = name; 434 } 435 436 /********************************************************************** 437 438 Add a cookie to our output headers. 439 440 **********************************************************************/ 441 442 void add (Cookie cookie) 443 { 444 // add the cookie header via our callback 445 headers.add (name, (OutputBuffer buf){cookie.produce (&buf.write);}); 446 } 447 } 448 449 450 451 /******************************************************************************* 452 453 Server-side cookie parser. See RFC 2109 for details. 454 455 *******************************************************************************/ 456 457 class CookieParser : Iterator!(char) 458 { 459 private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote}; 460 461 private CookieStack stack; 462 private Array array; 463 private static __gshared bool[128] charMap; 464 465 /*********************************************************************** 466 467 populate a map of token separators 468 469 ***********************************************************************/ 470 471 shared static this () 472 { 473 charMap['('] = true; 474 charMap[')'] = true; 475 charMap['<'] = true; 476 charMap['>'] = true; 477 charMap['@'] = true; 478 charMap[','] = true; 479 charMap[';'] = true; 480 charMap[':'] = true; 481 charMap['\\'] = true; 482 charMap['"'] = true; 483 charMap['/'] = true; 484 charMap['['] = true; 485 charMap[']'] = true; 486 charMap['?'] = true; 487 charMap['='] = true; 488 charMap['{'] = true; 489 charMap['}'] = true; 490 } 491 492 /*********************************************************************** 493 494 ***********************************************************************/ 495 496 this (CookieStack stack) 497 { 498 super(); 499 this.stack = stack; 500 array = new Array(0); 501 } 502 503 /*********************************************************************** 504 505 Callback for iterator.next(). We scan for name-value 506 pairs, populating Cookie instances along the way. 507 508 ***********************************************************************/ 509 510 protected override size_t scan (const(void)[] data) 511 { 512 char c; 513 int mark, 514 vrsn; 515 const(char)[] name, 516 token; 517 Cookie cookie; 518 519 State state = State.Begin; 520 const(char)[] content = cast(const(char)[]) data; 521 522 /*************************************************************** 523 524 Found a value; set that also 525 526 ***************************************************************/ 527 528 void setValue (size_t i) 529 { 530 token = content [mark..i]; 531 //Print ("::name '%.*s'\n", name); 532 //Print ("::value '%.*s'\n", token); 533 534 if (name[0] != '$') 535 { 536 cookie = stack.push(); 537 cookie.setName (name); 538 cookie.setValue (token); 539 cookie.setVersion (vrsn); 540 } 541 else 542 { 543 if(name.length < 9) 544 { 545 char[8] temp; 546 temp[0..name.length] = name[]; 547 switch (toLower (temp[0..name.length])) 548 { 549 case "$path": 550 if (cookie) 551 cookie.setPath (token); 552 break; 553 554 case "$domain": 555 if (cookie) 556 cookie.setDomain (token); 557 break; 558 559 case "$version": 560 vrsn = cast(int) Integer.parse (token); 561 break; 562 563 default: 564 break; 565 } 566 } 567 } 568 state = State.Begin; 569 } 570 571 /*************************************************************** 572 573 Scan content looking for cookie fields 574 575 ***************************************************************/ 576 577 for (int i; i < content.length; ++i) 578 { 579 c = content [i]; 580 switch (state) 581 { 582 // look for an lValue 583 case State.Begin: 584 mark = i; 585 if (isToken(c)) 586 state = State.LValue; 587 continue; 588 589 // scan until we have all lValue chars 590 case State.LValue: 591 if (! isToken(c)) 592 { 593 state = State.Equals; 594 name = content [mark..i]; 595 --i; 596 } 597 continue; 598 599 // should now have either a '=', ';', or ',' 600 case State.Equals: 601 if (c is '=') 602 state = State.RValue; 603 else 604 if (c is ',' || c is ';') 605 // get next NVPair 606 state = State.Begin; 607 continue; 608 609 // look for a quoted token, or a plain one 610 case State.RValue: 611 mark = i; 612 if (c is '\'') 613 state = State.SQuote; 614 else 615 if (c is '"') 616 state = State.DQuote; 617 else 618 if (isToken(c)) 619 state = State.Token; 620 continue; 621 622 // scan for all plain token chars 623 case State.Token: 624 if (! isToken(c)) 625 { 626 setValue (i); 627 --i; 628 } 629 continue; 630 631 // scan until the next ' 632 case State.SQuote: 633 if (c is '\'') 634 ++mark, setValue (i); 635 continue; 636 637 // scan until the next " 638 case State.DQuote: 639 if (c is '"') 640 ++mark, setValue (i); 641 continue; 642 643 default: 644 continue; 645 } 646 } 647 648 // we ran out of content; patch partial cookie values 649 if (state is State.Token) 650 setValue (content.length); 651 652 // go home 653 return IConduit.Eof; 654 } 655 656 /*********************************************************************** 657 658 Locate the next token from the provided buffer, and map a 659 buffer reference into token. Returns true if a token was 660 located, false otherwise. 661 662 Note that the buffer content is not duplicated. Instead, a 663 slice of the buffer is referenced by the token. You can use 664 Token.clone() or Token.toString().dup() to copy content per 665 your application needs. 666 667 Note also that there may still be one token left in a buffer 668 that was not terminated correctly (as in eof conditions). In 669 such cases, tokens are mapped onto remaining content and the 670 buffer will have no more readable content. 671 672 ***********************************************************************/ 673 674 bool parse (const(char)[] header) 675 { 676 super.set (array.assign (cast(void[]) header)); 677 return next.ptr > null; 678 } 679 680 /********************************************************************** 681 682 in-place conversion to lowercase 683 684 **********************************************************************/ 685 686 final static char[] toLower (char[] src) 687 { 688 foreach (i, char c; src) 689 if (c >= 'A' && c <= 'Z') 690 src[i] = cast(char)(c + ('a' - 'A')); 691 return src; 692 } 693 694 /*********************************************************************** 695 696 Is 'c' a valid token character? 697 698 ***********************************************************************/ 699 700 private static bool isToken (char c) 701 { 702 return (c > 32 && c < 127 && !charMap[c]); 703 } 704 } 705 706