1 module hunt.http.Cookie; 2 3 import hunt.collection.List; 4 import hunt.Exceptions; 5 import hunt.text.Common; 6 import hunt.util.StringBuilder; 7 import hunt.text.StringUtils; 8 import hunt.util.DateTime; 9 10 import std.array; 11 import std.container.array; 12 import std.conv; 13 import std.datetime; 14 import std.string; 15 16 import core.stdc.stdio; 17 import core.stdc.time; 18 19 alias HttpCookie = Cookie; 20 21 /** 22 * An HttpCookie object represents an HTTP cookie, which carries state 23 * information between server and user agent. Cookie is widely adopted 24 * to create stateful sessions. 25 * 26 * <p> There are 3 HTTP cookie specifications: 27 * <blockquote> 28 * Netscape draft<br> 29 * RFC 2109 - <a href="http://www.ietf.org/rfc/rfc2109.txt"> 30 * <i>http://www.ietf.org/rfc/rfc2109.txt</i></a><br> 31 * RFC 2965 - <a href="http://www.ietf.org/rfc/rfc2965.txt"> 32 * <i>http://www.ietf.org/rfc/rfc2965.txt</i></a> 33 * </blockquote> 34 * 35 * <p> HttpCookie class can accept all these 3 forms of syntax. 36 * 37 * @author Edward Wang 38 */ 39 class Cookie { 40 41 // Since the positive and zero max-age have their meanings, 42 // this value serves as a hint as 'not specify max-age' 43 private enum MAX_AGE_UNSPECIFIED = -1; 44 // 45 // The value of the cookie itself. 46 // 47 48 private string name; // NAME= ... "$Name" style is reserved 49 private string value; // value of NAME 50 51 // 52 // Attributes encoded in the header's cookie fields. 53 // 54 55 private string comment; // ;Comment=VALUE ... describes cookie's use 56 // ;Discard ... implied by maxAge < 0 57 private string domain; // ;Domain=VALUE ... domain that sees cookie 58 private int maxAge = MAX_AGE_UNSPECIFIED; // ;Max-Age=VALUE ... cookies auto-expire 59 private string path; // ;Path=VALUE ... URLs that see the cookie 60 private bool secure; // ;Secure ... e.g. use SSL 61 private int _version = 1; // ;Version=1 ... means RFC 2109++ style 62 private bool _isHttpOnly = false; 63 64 // Hold the creation time (in seconds) of the http cookie for later 65 // expiration calculation 66 private long whenCreated; 67 68 // date formats used by Netscape's cookie draft 69 // as well as formats seen on various sites 70 private enum string[] COOKIE_DATE_FORMATS = [ 71 "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'", 72 "EEE',' dd MMM yyyy HH:mm:ss 'GMT'", 73 "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", 74 "EEE',' dd-MMM-yy HH:mm:ss 'GMT'", 75 "EEE',' dd MMM yy HH:mm:ss 'GMT'", 76 "EEE MMM dd yy HH:mm:ss 'GMT'Z" 77 ]; 78 79 this() { 80 81 } 82 83 /** 84 * Constructs a cookie with the specified name and value. 85 * 86 * <p> 87 * The name must conform to RFC 2109. However, vendors may provide a 88 * configuration option that allows cookie names conforming to the original 89 * Netscape Cookie Specification to be accepted. 90 * 91 * <p> 92 * The name of a cookie cannot be changed once the cookie has been created. 93 * 94 * <p> 95 * The value can be anything the server chooses to send. Its value is 96 * probably of interest only to the server. The cookie's value can be 97 * changed after creation with the <code>setValue</code> method. 98 * 99 * <p> 100 * By default, cookies are created according to the Netscape cookie 101 * specification. The version can be changed with the 102 * <code>setVersion</code> method. 103 * 104 * @param name 105 * the name of the cookie 106 * 107 * @param value 108 * the value of the cookie 109 * 110 * @throws IllegalArgumentException 111 * if the cookie name is null or empty or contains any illegal 112 * characters (for example, a comma, space, or semicolon) or 113 * matches a token reserved for use by the cookie protocol 114 * 115 * @see #setValue 116 * @see #setVersion 117 */ 118 this(string name, string value, int expires=-1, 119 string path = "/", string domain = null, 120 bool secure = false, bool httpOnly = true) { 121 if (name.empty) { 122 throw new IllegalArgumentException("the cookie name is empty"); 123 } 124 125 this.name = name; 126 this.value = value; 127 this.maxAge = expires; 128 this.path = path; 129 this.secure = secure; 130 this.domain = domain; 131 this._isHttpOnly = httpOnly; 132 this.whenCreated = hunt.util.DateTime.DateTime.currentTimeMillis(); 133 } 134 135 /** 136 * Specifies a comment that describes a cookie's purpose. The comment is 137 * useful if the browser presents the cookie to the user. Comments are not 138 * supported by Netscape Version 0 cookies. 139 * 140 * @param purpose 141 * a <code>string</code> specifying the comment to display to the 142 * user 143 * 144 * @see #getComment 145 */ 146 void setComment(string purpose) { 147 comment = purpose; 148 } 149 150 /** 151 * Returns the comment describing the purpose of this cookie, or 152 * <code>null</code> if the cookie has no comment. 153 * 154 * @return the comment of the cookie, or <code>null</code> if unspecified 155 * 156 * @see #setComment 157 */ 158 string getComment() { 159 return comment; 160 } 161 162 /** 163 * 164 * Specifies the domain within which this cookie should be presented. 165 * 166 * <p> 167 * The form of the domain name is specified by RFC 2109. A domain name 168 * begins with a dot (<code>.foo.com</code>) and means that the cookie is 169 * visible to servers in a specified Domain Name System (DNS) zone (for 170 * example, <code>www.foo.com</code>, but not <code>a.b.foo.com</code>). By 171 * default, cookies are only returned to the server that sent them. 172 * 173 * @param domain 174 * the domain name within which this cookie is visible; form is 175 * according to RFC 2109 176 * 177 * @see #getDomain 178 */ 179 void setDomain(string domain) { 180 this.domain = domain.toLower(); // IE allegedly needs 181 // this 182 } 183 184 /** 185 * Gets the domain name of this Cookie. 186 * 187 * <p> 188 * Domain names are formatted according to RFC 2109. 189 * 190 * @return the domain name of this Cookie 191 * 192 * @see #setDomain 193 */ 194 string getDomain() nothrow { 195 return domain; 196 } 197 198 /** 199 * Sets the maximum age in seconds for this Cookie. 200 * 201 * <p> 202 * A positive value indicates that the cookie will expire after that many 203 * seconds have passed. Note that the value is the <i>maximum</i> age when 204 * the cookie will expire, not the cookie's current age. 205 * 206 * <p> 207 * A negative value means that the cookie is not stored persistently and 208 * will be deleted when the Web browser exits. A zero value causes the 209 * cookie to be deleted. 210 * 211 * @param expiry 212 * an integer specifying the maximum age of the cookie in 213 * seconds; if negative, means the cookie is not stored; if zero, 214 * deletes the cookie 215 * 216 * @see #getMaxAge 217 */ 218 void setMaxAge(int expiry) { 219 maxAge = expiry; 220 } 221 222 /** 223 * Gets the maximum age in seconds of this Cookie. 224 * 225 * <p> 226 * By default, <code>-1</code> is returned, which indicates that the cookie 227 * will persist until browser shutdown. 228 * 229 * @return an integer specifying the maximum age of the cookie in seconds; 230 * if negative, means the cookie persists until browser shutdown 231 * 232 * @see #setMaxAge 233 */ 234 int getMaxAge() { 235 return maxAge; 236 } 237 238 /** 239 * Specifies a path for the cookie to which the client should return the 240 * cookie. 241 * 242 * <p> 243 * The cookie is visible to all the pages in the directory you specify, and 244 * all the pages in that directory's subdirectories. A cookie's path, for 245 * example, <i>/catalog</i>, which makes the cookie visible to all 246 * directories on the server under <i>/catalog</i>. 247 * 248 * <p> 249 * Consult RFC 2109 (available on the Internet) for more information on 250 * setting path names for cookies. 251 * 252 * 253 * @param uri 254 * a <code>string</code> specifying a path 255 * 256 * @see #getPath 257 */ 258 void setPath(string uri) { 259 path = uri; 260 } 261 262 /** 263 * Returns the path on the server to which the browser returns this cookie. 264 * The cookie is visible to all subpaths on the server. 265 * 266 * @return a <code>string</code> specifying a path , for example, 267 * <i>/catalog</i> 268 * 269 * @see #setPath 270 */ 271 string getPath() nothrow { 272 return path; 273 } 274 275 /** 276 * Indicates to the browser whether the cookie should only be sent using a 277 * secure protocol, such as HTTPS or SSL. 278 * 279 * <p> 280 * The default value is <code>false</code>. 281 * 282 * @param flag 283 * if <code>true</code>, sends the cookie from the browser to the 284 * server only when using a secure protocol; if 285 * <code>false</code>, sent on any protocol 286 * 287 * @see #getSecure 288 */ 289 void setSecure(bool flag) { 290 secure = flag; 291 } 292 293 /** 294 * Returns <code>true</code> if the browser is sending cookies only over a 295 * secure protocol, or <code>false</code> if the browser can send cookies 296 * using any protocol. 297 * 298 * @return <code>true</code> if the browser uses a secure protocol, 299 * <code>false</code> otherwise 300 * 301 * @see #setSecure 302 */ 303 bool getSecure() { 304 return secure; 305 } 306 307 void setName(string name) { 308 this.name = name; 309 } 310 311 /** 312 * Returns the name of the cookie. The name cannot be changed after 313 * creation. 314 * 315 * @return the name of the cookie 316 */ 317 string getName() nothrow { 318 return name; 319 } 320 321 /** 322 * Assigns a new value to this Cookie. 323 * 324 * <p> 325 * If you use a binary value, you may want to use BASE64 encoding. 326 * 327 * <p> 328 * With Version 0 cookies, values should not contain white space, brackets, 329 * parentheses, equals signs, commas, double quotes, slashes, question 330 * marks, at signs, colons, and semicolons. Empty values may not behave the 331 * same way on all browsers. 332 * 333 * @param newValue 334 * the new value of the cookie 335 * 336 * @see #getValue 337 */ 338 void setValue(string newValue) { 339 value = newValue; 340 } 341 342 /** 343 * Gets the current value of this Cookie. 344 * 345 * @return the current value of this Cookie 346 * 347 * @see #setValue 348 */ 349 string getValue() { 350 return value; 351 } 352 353 /** 354 * Returns the version of the protocol this cookie complies with. Version 1 355 * complies with RFC 2109, and version 0 complies with the original cookie 356 * specification drafted by Netscape. Cookies provided by a browser use and 357 * identify the browser's cookie version. 358 * 359 * @return 0 if the cookie complies with the original Netscape 360 * specification; 1 if the cookie complies with RFC 2109 361 * 362 * @see #setVersion 363 */ 364 int getVersion() { 365 return _version; 366 } 367 368 /** 369 * Sets the version of the cookie protocol that this Cookie complies with. 370 * 371 * <p> 372 * Version 0 complies with the original Netscape cookie specification. 373 * Version 1 complies with RFC 2109. 374 * 375 * <p> 376 * Since RFC 2109 is still somewhat new, consider version 1 as experimental; 377 * do not use it yet on production sites. 378 * 379 * @param v 380 * 0 if the cookie should comply with the original Netscape 381 * specification; 1 if the cookie should comply with RFC 2109 382 * 383 * @see #getVersion 384 */ 385 void setVersion(int v) { 386 _version = v; 387 } 388 389 /** 390 * Overrides the standard <code>java.lang.Object.clone</code> method to 391 * return a copy of this Cookie. 392 */ 393 // Object clone() { 394 // try { 395 // return super.clone(); 396 // } catch (NotSupportedException e) { 397 // throw new RuntimeException(e.getMessage()); 398 // } 399 // } 400 401 /** 402 * Marks or unmarks this Cookie as <i>HttpOnly</i>. 403 * 404 * <p> 405 * If <tt>isHttpOnly</tt> is set to <tt>true</tt>, this cookie is marked as 406 * <i>HttpOnly</i>, by adding the <tt>HttpOnly</tt> attribute to it. 407 * 408 * <p> 409 * <i>HttpOnly</i> cookies are not supposed to be exposed to client-side 410 * scripting code, and may therefore help mitigate certain kinds of 411 * cross-site scripting attacks. 412 * 413 * @param isHttpOnly 414 * true if this cookie is to be marked as <i>HttpOnly</i>, false 415 * otherwise 416 * 417 */ 418 void setHttpOnly(bool isHttpOnly) { 419 this._isHttpOnly = isHttpOnly; 420 } 421 422 /** 423 * Checks whether this Cookie has been marked as <i>HttpOnly</i>. 424 * 425 * @return true if this Cookie has been marked as <i>HttpOnly</i>, false 426 * otherwise 427 * 428 */ 429 bool isHttpOnly() { 430 return _isHttpOnly; 431 } 432 433 /** 434 * Reports whether this HTTP cookie has expired or not. 435 * 436 * @return {@code true} to indicate this HTTP cookie has expired; 437 * otherwise, {@code false} 438 */ 439 bool isExpired() { 440 // if not specify max-age, this cookie should be 441 // discarded when user agent is to be closed, but 442 // it is not expired. 443 if (maxAge < 0) return false; 444 if (maxAge == 0) return true; 445 446 long deltaSecond = (hunt.util.DateTime.DateTime.currentTimeMillis() - whenCreated) / 1000; 447 return deltaSecond > maxAge; 448 } 449 450 override 451 string toString() { 452 return "Cookie [name=" ~ name ~ ", value=" ~ value ~ ", comment=" ~ comment ~ 453 ", domain=" ~ domain ~ ", maxAge=" ~ maxAge.to!string ~ ", path=" ~ path ~ ", secure=" ~ 454 to!string(secure) ~ ", version=" ~ to!string(_version) ~ ", isHttpOnly=" ~ 455 to!string(_isHttpOnly) ~ "]"; 456 } 457 458 } 459 460 /* ----------------------------- CookieGenerator ---------------------------- */ 461 462 string generateCookies(Cookie[] cookies) { 463 if (cookies is null) { 464 throw new IllegalArgumentException("the cookie list is null"); 465 } 466 467 if (cookies.length == 1) { 468 return generateCookie(cookies[0]); 469 } else if (cookies.length > 1) { 470 StringBuilder sb = new StringBuilder(); 471 472 sb.append(generateCookie(cookies[0])); 473 for (size_t i = 1; i < cookies.length; i++) { 474 sb.append(';').append(generateCookie(cookies[i])); 475 } 476 477 return sb.toString(); 478 } else { 479 throw new IllegalArgumentException("the cookie list size is 0"); 480 } 481 } 482 483 string generateCookies(List!Cookie cookies) { 484 if (cookies is null) { 485 throw new IllegalArgumentException("the cookie list is null"); 486 } 487 488 if (cookies.size() == 1) { 489 return generateCookie(cookies.get(0)); 490 } else if (cookies.size() > 1) { 491 StringBuilder sb = new StringBuilder(); 492 493 sb.append(generateCookie(cookies.get(0))); 494 for (int i = 1; i < cookies.size(); i++) { 495 sb.append(';').append(generateCookie(cookies.get(i))); 496 } 497 498 return sb.toString(); 499 } else { 500 throw new IllegalArgumentException("the cookie list size is 0"); 501 } 502 } 503 504 string generateCookie(Cookie cookie) { 505 if (cookie is null) { 506 throw new IllegalArgumentException("the cookie is null"); 507 } else { 508 return cookie.getName() ~ "=" ~ cookie.getValue(); 509 } 510 } 511 512 string generateSetCookie(Cookie cookie) { 513 if (cookie is null) { 514 throw new IllegalArgumentException("the cookie is null"); 515 } else { 516 StringBuilder sb = new StringBuilder(); 517 518 sb.append(cookie.getName()).append('=').append(cookie.getValue()); 519 520 if (!empty(cookie.getComment())) { 521 sb.append(";Comment=").append(cookie.getComment()); 522 } 523 524 if (!empty(cookie.getDomain())) { 525 sb.append(";Domain=").append(cookie.getDomain()); 526 } 527 if (cookie.getMaxAge() >= 0) { 528 sb.append(";Max-Age=").append(cookie.getMaxAge()); 529 } 530 531 string path = empty(cookie.getPath()) ? "/" : cookie.getPath(); 532 sb.append(";Path=").append(path); 533 534 if (cookie.getSecure()) { 535 sb.append(";Secure"); 536 } 537 538 sb.append(";Version=").append(cookie.getVersion()); 539 540 return sb.toString(); 541 } 542 } 543 544 // string generateServletSetCookie(javax.servlet.http.Cookie cookie) { 545 // if (cookie == null) { 546 // throw new IllegalArgumentException("the cookie is null"); 547 // } else { 548 // StringBuilder sb = new StringBuilder(); 549 550 // sb.append(cookie.getName()).append('=').append(cookie.getValue()); 551 552 // if (VerifyUtils.isNotEmpty(cookie.getComment())) { 553 // sb.append(";Comment=").append(cookie.getComment()); 554 // } 555 556 // if (VerifyUtils.isNotEmpty(cookie.getDomain())) { 557 // sb.append(";Domain=").append(cookie.getDomain()); 558 // } 559 // if (cookie.getMaxAge() >= 0) { 560 // sb.append(";Max-Age=").append(cookie.getMaxAge()); 561 // } 562 563 // string path = VerifyUtils.isEmpty(cookie.getPath()) ? "/" : cookie.getPath(); 564 // sb.append(";Path=").append(path); 565 566 // if (cookie.getSecure()) { 567 // sb.append(";Secure"); 568 // } 569 570 // sb.append(";Version=").append(cookie.getVersion()); 571 572 // return sb.toString(); 573 // } 574 // } 575 576 /* ------------------------------ CookieParser ------------------------------ */ 577 578 alias CookieParsingHandler = void delegate(string name, string value); 579 580 void parseCookies(string cookieStr, CookieParsingHandler callback) { 581 if (empty(cookieStr)) { 582 throw new IllegalArgumentException("the cookie string is empty"); 583 } else { 584 string[] cookieKeyValues = StringUtils.split(cookieStr, ";"); 585 foreach (string cookieKeyValue ; cookieKeyValues) { 586 string[] kv = StringUtils.split(cookieKeyValue, "=", 2); 587 if (kv != null) { 588 if (kv.length == 2) { 589 callback(kv[0].strip(), kv[1].strip()); 590 } else if (kv.length == 1) { 591 callback(kv[0].strip(), ""); 592 } else { 593 throw new IllegalStateException("the cookie string format error"); 594 } 595 } else { 596 throw new IllegalStateException("the cookie string format error"); 597 } 598 } 599 } 600 } 601 602 Cookie parseSetCookie(string cookieStr) { 603 Cookie cookie = new Cookie(); 604 605 parseCookies(cookieStr, (name, value) { 606 607 if("expires".equalsIgnoreCase(name)) { 608 // "Mon, 20-Apr-2020 12:25:04 GMT" 609 char[3] week; 610 char[3] month; 611 tm t; 612 int r = sscanf(value.ptr, "%3s, %2d-%3s-%4d %2d:%2d:%2d GMT", week.ptr, 613 &t.tm_mday, month.ptr, &t.tm_year, &t.tm_hour, &t.tm_min, &t.tm_sec); 614 if(r == 7) { 615 t.tm_mon = monthByName(cast(string) month) + 1; 616 t.tm_wday = weekDayByName(cast(string) week); 617 618 SysTime dt = SysTime(std.datetime.DateTime(t.tm_year, t.tm_mon, 619 t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec), UTC()); 620 SysTime now = Clock.currTime(); 621 Duration dur = dt - Clock.currTime(); 622 long sec = dur.total!"seconds"(); 623 cookie.setMaxAge(cast(int)sec); 624 } else { 625 // FIXME: Needing refactor or cleanup -@zhangxueping at 2020-04-21T09:38:27+08:00 626 // 627 // int sec; 628 // r = sscanf(str.ptr, "%d", &sec); 629 // if(r == 1) { 630 // SysTime now = Clock.currTime(); 631 // cookie.setMaxAge(sec); 632 // } 633 } 634 635 } else if("HttpOnly".equalsIgnoreCase(name)) { 636 cookie.setHttpOnly(true); 637 } else if ("Comment".equalsIgnoreCase(name)) { 638 cookie.setComment(value); 639 } else if ("Domain".equalsIgnoreCase(name)) { 640 cookie.setDomain(value); 641 } else if ("Max-Age".equalsIgnoreCase(name)) { 642 cookie.setMaxAge(to!int(value)); 643 } else if ("Path".equalsIgnoreCase(name)) { 644 cookie.setPath(value); 645 } else if ("Secure".equalsIgnoreCase(name)) { 646 cookie.setSecure(true); 647 } else if ("Version".equalsIgnoreCase(name)) { 648 cookie.setVersion(to!int(value)); 649 } else { 650 cookie.setName(name); 651 cookie.setValue(value); 652 } 653 654 }); 655 return cookie; 656 } 657 658 Cookie[] parseCookie(string cookieStr) { 659 Array!(Cookie) list; 660 parseCookies(cookieStr, (name, value) { list.insertBack(new Cookie(name, value)); }); 661 return list.array(); 662 } 663 664 // List<javax.servlet.http.Cookie> parserServletCookie(string cookieStr) { 665 // List<javax.servlet.http.Cookie> list = new ArrayList<>(); 666 // parseCookies(cookieStr, (name, value) -> list.add(new javax.servlet.http.Cookie(name, value))); 667 // return list; 668 // }