1 /******************************************************************************* 2 3 Copyright: 4 Copyright (c) 2004 Kris Bell. 5 Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH. 6 All rights reserved. 7 8 License: 9 Tango Dual License: 3-Clause BSD License / Academic Free License v3.0. 10 See LICENSE_TANGO.txt for details. 11 12 Version: Initial release: April 2004 13 14 Authors: Kris 15 16 *******************************************************************************/ 17 18 module ocean.net.http.HttpCookies; 19 20 import ocean.meta.types.Qualifiers; 21 22 import core.stdc.ctype; 23 24 import ocean.io.device.Array; 25 26 import ocean.io.model.IConduit; 27 28 import ocean.io.stream.Iterator; 29 30 import ocean.net.http.HttpHeaders; 31 32 import Integer = ocean.text.convert.Integer_tango; 33 34 /******************************************************************************* 35 36 Defines the Cookie class, and the means for reading & writing them. 37 Cookie implementation conforms with RFC 2109, but supports parsing 38 of server-side cookies only. Client-side cookies are supported in 39 terms of output, but response parsing is not yet implemented ... 40 41 See over <A HREF="http://www.faqs.org/rfcs/rfc2109.html">here</A> 42 for the RFC document. 43 44 *******************************************************************************/ 45 46 class Cookie //: IWritable 47 { 48 char[] name, 49 path, 50 value, 51 domain, 52 comment; 53 uint vrsn=1; // 'version' is a reserved word 54 bool secure=false; 55 long maxAge=long.min; 56 57 /*********************************************************************** 58 59 Construct an empty client-side cookie. You add these 60 to an output request using HttpClient.addCookie(), or 61 the equivalent. 62 63 ***********************************************************************/ 64 65 this () {} 66 67 /*********************************************************************** 68 69 Construct a cookie with the provided attributes. You add 70 these to an output request using HttpClient.addCookie(), 71 or the equivalent. 72 73 ***********************************************************************/ 74 75 this (char[] name, char[] value) 76 { 77 setName (name); 78 setValue (value); 79 } 80 81 /*********************************************************************** 82 83 Set the name of this cookie 84 85 ***********************************************************************/ 86 87 Cookie setName (char[] name) 88 { 89 this.name = name; 90 return this; 91 } 92 93 /*********************************************************************** 94 95 Set the value of this cookie 96 97 ***********************************************************************/ 98 99 Cookie setValue (char[] value) 100 { 101 this.value = value; 102 return this; 103 } 104 105 /*********************************************************************** 106 107 Set the version of this cookie 108 109 ***********************************************************************/ 110 111 Cookie setVersion (uint vrsn) 112 { 113 this.vrsn = vrsn; 114 return this; 115 } 116 117 /*********************************************************************** 118 119 Set the path of this cookie 120 121 ***********************************************************************/ 122 123 Cookie setPath (char[] path) 124 { 125 this.path = path; 126 return this; 127 } 128 129 /*********************************************************************** 130 131 Set the domain of this cookie 132 133 ***********************************************************************/ 134 135 Cookie setDomain (char[] domain) 136 { 137 this.domain = domain; 138 return this; 139 } 140 141 /*********************************************************************** 142 143 Set the comment associated with this cookie 144 145 ***********************************************************************/ 146 147 Cookie setComment (char[] comment) 148 { 149 this.comment = comment; 150 return this; 151 } 152 153 /*********************************************************************** 154 155 Set the maximum duration of this cookie 156 157 ***********************************************************************/ 158 159 Cookie setMaxAge (long maxAge) 160 { 161 this.maxAge = maxAge; 162 return this; 163 } 164 165 /*********************************************************************** 166 167 Indicate whether this cookie should be considered secure or not 168 169 ***********************************************************************/ 170 171 Cookie setSecure (bool secure) 172 { 173 this.secure = secure; 174 return this; 175 } 176 /+ 177 /*********************************************************************** 178 179 Output the cookie as a text stream, via the provided IWriter 180 181 ***********************************************************************/ 182 183 void write (IWriter writer) 184 { 185 produce (&writer.buffer.consume); 186 } 187 +/ 188 /*********************************************************************** 189 190 Output the cookie as a text stream, via the provided consumer 191 192 ***********************************************************************/ 193 194 void produce (scope size_t delegate(const(void)[]) consume) 195 { 196 consume (name); 197 198 if (value.length) 199 consume ("="), consume (value); 200 201 if (path.length) 202 consume (";Path="), consume (path); 203 204 if (domain.length) 205 consume (";Domain="), consume (domain); 206 207 if (vrsn) 208 { 209 char[16] tmp = void; 210 211 consume (";Version="); 212 consume (Integer.format (tmp, vrsn)); 213 214 if (comment.length) 215 consume (";Comment=\""), consume(comment), consume("\""); 216 217 if (secure) 218 consume (";Secure"); 219 220 if (maxAge != maxAge.min) 221 consume (";Max-Age="c), consume (Integer.format (tmp, maxAge)); 222 } 223 } 224 225 /*********************************************************************** 226 227 Reset this cookie 228 229 ***********************************************************************/ 230 231 Cookie clear () 232 { 233 vrsn = 1; 234 secure = false; 235 maxAge = maxAge.min; 236 name = path = domain = comment = null; 237 return this; 238 } 239 } 240 241 242 243 /******************************************************************************* 244 245 Implements a stack of cookies. Each cookie is pushed onto the 246 stack by a parser, which takes its input from HttpHeaders. The 247 stack can be populated for both client and server side cookies. 248 249 *******************************************************************************/ 250 251 class CookieStack 252 { 253 private int depth; 254 private Cookie[] cookies; 255 256 /********************************************************************** 257 258 Construct a cookie stack with the specified initial extent. 259 The stack will grow as necessary over time. 260 261 **********************************************************************/ 262 263 this (int size) 264 { 265 cookies = new Cookie[0]; 266 resize (cookies, size); 267 } 268 269 /********************************************************************** 270 271 Pop the stack all the way to zero 272 273 **********************************************************************/ 274 275 final void reset () 276 { 277 depth = 0; 278 } 279 280 /********************************************************************** 281 282 Return a fresh cookie from the stack 283 284 **********************************************************************/ 285 286 final Cookie push () 287 { 288 if (depth == cookies.length) 289 resize (cookies, depth * 2); 290 return cookies [depth++]; 291 } 292 293 /********************************************************************** 294 295 Resize the stack such that it has more room. 296 297 **********************************************************************/ 298 299 private final static void resize (ref Cookie[] cookies, int size) 300 { 301 auto i = cookies.length; 302 303 for (cookies.length=size; i < cookies.length; ++i) 304 cookies[i] = new Cookie(); 305 } 306 307 /********************************************************************** 308 309 Iterate over all cookies in stack 310 311 **********************************************************************/ 312 313 int opApply (scope int delegate(ref Cookie) dg) 314 { 315 int result = 0; 316 317 for (int i=0; i < depth; ++i) 318 if ((result = dg (cookies[i])) != 0) 319 break; 320 return result; 321 } 322 } 323 324 325 326 /******************************************************************************* 327 328 This is the support point for server-side cookies. It wraps a 329 CookieStack together with a set of HttpHeaders, along with the 330 appropriate cookie parser. One would do something very similar 331 for client side cookie parsing also. 332 333 *******************************************************************************/ 334 335 class HttpCookiesView //: IWritable 336 { 337 private bool parsed; 338 private CookieStack stack; 339 private CookieParser parser; 340 private HttpHeadersView headers; 341 342 /********************************************************************** 343 344 Construct cookie wrapper with the provided headers. 345 346 **********************************************************************/ 347 348 this (HttpHeadersView headers) 349 { 350 this.headers = headers; 351 352 // create a stack for parsed cookies 353 stack = new CookieStack (10); 354 355 // create a parser 356 parser = new CookieParser (stack); 357 } 358 /+ 359 /********************************************************************** 360 361 Output each of the cookies parsed to the provided IWriter. 362 363 **********************************************************************/ 364 365 void write (IWriter writer) 366 { 367 produce (&writer.buffer.consume, HttpConst.Eol); 368 } 369 +/ 370 /********************************************************************** 371 372 Output the token list to the provided consumer 373 374 **********************************************************************/ 375 376 void produce (scope size_t delegate(const(void)[]) consume, istring eol = HttpConst.Eol) 377 { 378 foreach (cookie; parse) 379 cookie.produce (consume), consume (eol); 380 } 381 382 /********************************************************************** 383 384 Reset these cookies for another parse 385 386 **********************************************************************/ 387 388 void reset () 389 { 390 stack.reset; 391 parsed = false; 392 } 393 394 /********************************************************************** 395 396 Parse all cookies from our HttpHeaders, pushing each onto 397 the CookieStack as we go. 398 399 **********************************************************************/ 400 401 CookieStack parse () 402 { 403 if (! parsed) 404 { 405 parsed = true; 406 407 foreach (HeaderElement header; headers) 408 if (header.name.value == HttpHeader.Cookie.value) 409 parser.parse (header.value.dup); 410 } 411 return stack; 412 } 413 } 414 415 416 417 /******************************************************************************* 418 419 Handles a set of output cookies by writing them into the list of 420 output headers. 421 422 *******************************************************************************/ 423 424 class HttpCookies 425 { 426 private HttpHeaderName name; 427 private HttpHeaders headers; 428 429 /********************************************************************** 430 431 Construct an output cookie wrapper upon the provided 432 output headers. Each cookie added is converted to an 433 addition to those headers. 434 435 **********************************************************************/ 436 437 this (HttpHeaders headers, HttpHeaderName name = HttpHeader.SetCookie) 438 { 439 this.headers = headers; 440 this.name = name; 441 } 442 443 /********************************************************************** 444 445 Add a cookie to our output headers. 446 447 **********************************************************************/ 448 449 void add (Cookie cookie) 450 { 451 // add the cookie header via our callback 452 headers.add (name, (OutputBuffer buf){cookie.produce (&buf.write);}); 453 } 454 } 455 456 457 458 /******************************************************************************* 459 460 Server-side cookie parser. See RFC 2109 for details. 461 462 *******************************************************************************/ 463 464 class CookieParser : Iterator 465 { 466 private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote}; 467 468 private CookieStack stack; 469 private Array array; 470 private static bool[128] charMap; 471 472 /*********************************************************************** 473 474 populate a map of token separators 475 476 ***********************************************************************/ 477 478 static this () 479 { 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 charMap['/'] = true; 491 charMap['['] = true; 492 charMap[']'] = true; 493 charMap['?'] = true; 494 charMap['='] = true; 495 charMap['{'] = true; 496 charMap['}'] = true; 497 } 498 499 /*********************************************************************** 500 501 ***********************************************************************/ 502 503 this (CookieStack stack) 504 { 505 super(); 506 this.stack = stack; 507 array = new Array(0); 508 } 509 510 /*********************************************************************** 511 512 Callback for iterator.next(). We scan for name-value 513 pairs, populating Cookie instances along the way. 514 515 ***********************************************************************/ 516 517 protected override size_t scan (const(void)[] data) 518 { 519 char c; 520 int mark, 521 vrsn; 522 char[] name, 523 token; 524 Cookie cookie; 525 526 State state = State.Begin; 527 char[] content = cast(char[]) data; 528 529 /*************************************************************** 530 531 Found a value; set that also 532 533 ***************************************************************/ 534 535 void setValue (int i) 536 { 537 token = content [mark..i]; 538 //Print ("::name '%.*s'\n", name); 539 //Print ("::value '%.*s'\n", token); 540 541 if (name[0] != '$') 542 { 543 cookie = stack.push; 544 cookie.setName (name); 545 cookie.setValue (token); 546 cookie.setVersion (vrsn); 547 } 548 else 549 switch (toLower (name)) 550 { 551 case "$path": 552 if (cookie) 553 cookie.setPath (token); 554 break; 555 556 case "$domain": 557 if (cookie) 558 cookie.setDomain (token); 559 break; 560 561 case "$version": 562 vrsn = cast(int) Integer.parse (token); 563 break; 564 565 default: 566 break; 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 (cast(int) 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 (char[] header) 675 { 676 super.set (array.assign (header)); 677 return next.ptr > null; 678 } 679 680 /********************************************************************** 681 682 in-place conversion to lowercase 683 684 **********************************************************************/ 685 686 final static char[] toLower (ref char[] src) 687 { 688 foreach (size_t 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 }