1 /** 2 Common classes for HTTP clients and servers. 3 4 Copyright: © 2012-2015 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Jan Krüger 7 */ 8 module vibe.http.common; 9 10 public import vibe.http.status; 11 12 import vibe.core.log; 13 import vibe.core.net; 14 import vibe.inet.message; 15 import vibe.stream.operations; 16 import vibe.textfilter.urlencode : urlEncode, urlDecode; 17 import vibe.utils.array; 18 import vibe.internal.freelistref; 19 import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy; 20 import vibe.internal.allocator; 21 import vibe.utils.dictionarylist; 22 import vibe.utils..string; 23 24 import std.algorithm; 25 import std.array; 26 import std.conv; 27 import std.datetime; 28 import std.exception; 29 import std.format; 30 import std.range : isOutputRange; 31 import std..string; 32 import std.typecons; 33 34 35 enum HTTPVersion { 36 HTTP_1_0, 37 HTTP_1_1, 38 HTTP_2 39 } 40 41 42 enum HTTPMethod { 43 // HTTP standard, RFC 2616 44 GET, 45 HEAD, 46 PUT, 47 POST, 48 PATCH, 49 DELETE, 50 OPTIONS, 51 TRACE, 52 CONNECT, 53 54 // WEBDAV extensions, RFC 2518 55 PROPFIND, 56 PROPPATCH, 57 MKCOL, 58 COPY, 59 MOVE, 60 LOCK, 61 UNLOCK, 62 63 // Versioning Extensions to WebDAV, RFC 3253 64 VERSIONCONTROL, 65 REPORT, 66 CHECKOUT, 67 CHECKIN, 68 UNCHECKOUT, 69 MKWORKSPACE, 70 UPDATE, 71 LABEL, 72 MERGE, 73 BASELINECONTROL, 74 MKACTIVITY, 75 76 // Ordered Collections Protocol, RFC 3648 77 ORDERPATCH, 78 79 // Access Control Protocol, RFC 3744 80 ACL 81 } 82 83 84 /** 85 Returns the string representation of the given HttpMethod. 86 */ 87 string httpMethodString(HTTPMethod m) 88 @safe nothrow { 89 switch(m){ 90 case HTTPMethod.BASELINECONTROL: return "BASELINE-CONTROL"; 91 case HTTPMethod.VERSIONCONTROL: return "VERSION-CONTROL"; 92 default: 93 try return to!string(m); 94 catch (Exception e) assert(false, e.msg); 95 } 96 } 97 98 /** 99 Returns the HttpMethod value matching the given HTTP method string. 100 */ 101 HTTPMethod httpMethodFromString(string str) 102 @safe { 103 switch(str){ 104 default: throw new Exception("Invalid HTTP method: "~str); 105 // HTTP standard, RFC 2616 106 case "GET": return HTTPMethod.GET; 107 case "HEAD": return HTTPMethod.HEAD; 108 case "PUT": return HTTPMethod.PUT; 109 case "POST": return HTTPMethod.POST; 110 case "PATCH": return HTTPMethod.PATCH; 111 case "DELETE": return HTTPMethod.DELETE; 112 case "OPTIONS": return HTTPMethod.OPTIONS; 113 case "TRACE": return HTTPMethod.TRACE; 114 case "CONNECT": return HTTPMethod.CONNECT; 115 116 // WEBDAV extensions, RFC 2518 117 case "PROPFIND": return HTTPMethod.PROPFIND; 118 case "PROPPATCH": return HTTPMethod.PROPPATCH; 119 case "MKCOL": return HTTPMethod.MKCOL; 120 case "COPY": return HTTPMethod.COPY; 121 case "MOVE": return HTTPMethod.MOVE; 122 case "LOCK": return HTTPMethod.LOCK; 123 case "UNLOCK": return HTTPMethod.UNLOCK; 124 125 // Versioning Extensions to WebDAV, RFC 3253 126 case "VERSION-CONTROL": return HTTPMethod.VERSIONCONTROL; 127 case "REPORT": return HTTPMethod.REPORT; 128 case "CHECKOUT": return HTTPMethod.CHECKOUT; 129 case "CHECKIN": return HTTPMethod.CHECKIN; 130 case "UNCHECKOUT": return HTTPMethod.UNCHECKOUT; 131 case "MKWORKSPACE": return HTTPMethod.MKWORKSPACE; 132 case "UPDATE": return HTTPMethod.UPDATE; 133 case "LABEL": return HTTPMethod.LABEL; 134 case "MERGE": return HTTPMethod.MERGE; 135 case "BASELINE-CONTROL": return HTTPMethod.BASELINECONTROL; 136 case "MKACTIVITY": return HTTPMethod.MKACTIVITY; 137 138 // Ordered Collections Protocol, RFC 3648 139 case "ORDERPATCH": return HTTPMethod.ORDERPATCH; 140 141 // Access Control Protocol, RFC 3744 142 case "ACL": return HTTPMethod.ACL; 143 } 144 } 145 146 unittest 147 { 148 assert(httpMethodString(HTTPMethod.GET) == "GET"); 149 assert(httpMethodString(HTTPMethod.UNLOCK) == "UNLOCK"); 150 assert(httpMethodString(HTTPMethod.VERSIONCONTROL) == "VERSION-CONTROL"); 151 assert(httpMethodString(HTTPMethod.BASELINECONTROL) == "BASELINE-CONTROL"); 152 assert(httpMethodFromString("GET") == HTTPMethod.GET); 153 assert(httpMethodFromString("UNLOCK") == HTTPMethod.UNLOCK); 154 assert(httpMethodFromString("VERSION-CONTROL") == HTTPMethod.VERSIONCONTROL); 155 } 156 157 158 /** 159 Utility function that throws a HTTPStatusException if the _condition is not met. 160 */ 161 T enforceHTTP(T)(T condition, HTTPStatus statusCode, lazy string message = null, string file = __FILE__, typeof(__LINE__) line = __LINE__) 162 { 163 return enforce(condition, new HTTPStatusException(statusCode, message, file, line)); 164 } 165 166 /** 167 Utility function that throws a HTTPStatusException with status code "400 Bad Request" if the _condition is not met. 168 */ 169 T enforceBadRequest(T)(T condition, lazy string message = null, string file = __FILE__, typeof(__LINE__) line = __LINE__) 170 { 171 return enforceHTTP(condition, HTTPStatus.badRequest, message, file, line); 172 } 173 174 175 /** 176 Represents an HTTP request made to a server. 177 */ 178 class HTTPRequest { 179 @safe: 180 181 protected { 182 InterfaceProxy!Stream m_conn; 183 } 184 185 public { 186 /// The HTTP protocol version used for the request 187 HTTPVersion httpVersion = HTTPVersion.HTTP_1_1; 188 189 /// The HTTP _method of the request 190 HTTPMethod method = HTTPMethod.GET; 191 192 /** The request URI 193 194 Note that the request URI usually does not include the global 195 'http://server' part, but only the local path and a query string. 196 A possible exception is a proxy server, which will get full URLs. 197 */ 198 string requestURI = "/"; 199 200 /// Compatibility alias - scheduled for deprecation 201 alias requestURL = requestURI; 202 203 /// All request _headers 204 InetHeaderMap headers; 205 } 206 207 protected this(InterfaceProxy!Stream conn) 208 { 209 m_conn = conn; 210 } 211 212 protected this() 213 { 214 } 215 216 public override string toString() 217 { 218 return httpMethodString(method) ~ " " ~ requestURL ~ " " ~ getHTTPVersionString(httpVersion); 219 } 220 221 /** Shortcut to the 'Host' header (always present for HTTP 1.1) 222 */ 223 @property string host() const { auto ph = "Host" in headers; return ph ? *ph : null; } 224 /// ditto 225 @property void host(string v) { headers["Host"] = v; } 226 227 /** Returns the mime type part of the 'Content-Type' header. 228 229 This function gets the pure mime type (e.g. "text/plain") 230 without any supplimentary parameters such as "charset=...". 231 Use contentTypeParameters to get any parameter string or 232 headers["Content-Type"] to get the raw value. 233 */ 234 @property string contentType() 235 const { 236 auto pv = "Content-Type" in headers; 237 if( !pv ) return null; 238 auto idx = std..string.indexOf(*pv, ';'); 239 return idx >= 0 ? (*pv)[0 .. idx] : *pv; 240 } 241 /// ditto 242 @property void contentType(string ct) { headers["Content-Type"] = ct; } 243 244 /** Returns any supplementary parameters of the 'Content-Type' header. 245 246 This is a semicolon separated ist of key/value pairs. Usually, if set, 247 this contains the character set used for text based content types. 248 */ 249 @property string contentTypeParameters() 250 const { 251 auto pv = "Content-Type" in headers; 252 if( !pv ) return null; 253 auto idx = std..string.indexOf(*pv, ';'); 254 return idx >= 0 ? (*pv)[idx+1 .. $] : null; 255 } 256 257 /** Determines if the connection persists across requests. 258 */ 259 @property bool persistent() const 260 { 261 auto ph = "connection" in headers; 262 switch(httpVersion) { 263 case HTTPVersion.HTTP_1_0: 264 if (ph && toLower(*ph) == "keep-alive") return true; 265 return false; 266 case HTTPVersion.HTTP_1_1: 267 if (ph && toLower(*ph) != "keep-alive") return false; 268 return true; 269 default: 270 return false; 271 } 272 } 273 } 274 275 276 /** 277 Represents the HTTP response from the server back to the client. 278 */ 279 class HTTPResponse { 280 @safe: 281 282 protected DictionaryList!Cookie m_cookies; 283 284 public { 285 /// The protocol version of the response - should not be changed 286 HTTPVersion httpVersion = HTTPVersion.HTTP_1_1; 287 288 /// The status code of the response, 200 by default 289 int statusCode = HTTPStatus.OK; 290 291 /** The status phrase of the response 292 293 If no phrase is set, a default one corresponding to the status code will be used. 294 */ 295 string statusPhrase; 296 297 /// The response header fields 298 InetHeaderMap headers; 299 300 /// All cookies that shall be set on the client for this request 301 @property ref DictionaryList!Cookie cookies() { return m_cookies; } 302 } 303 304 public override string toString() 305 { 306 auto app = appender!string(); 307 formattedWrite(app, "%s %d %s", getHTTPVersionString(this.httpVersion), this.statusCode, this.statusPhrase); 308 return app.data; 309 } 310 311 /** Shortcut to the "Content-Type" header 312 */ 313 @property string contentType() const { auto pct = "Content-Type" in headers; return pct ? *pct : "application/octet-stream"; } 314 /// ditto 315 @property void contentType(string ct) { headers["Content-Type"] = ct; } 316 } 317 318 319 /** 320 Respresents a HTTP response status. 321 322 Throwing this exception from within a request handler will produce a matching error page. 323 */ 324 class HTTPStatusException : Exception { 325 @safe: 326 327 private { 328 int m_status; 329 } 330 331 this(int status, string message = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) 332 { 333 super(message != "" ? message : httpStatusText(status), file, line, next); 334 m_status = status; 335 } 336 337 /// The HTTP status code 338 @property int status() const { return m_status; } 339 340 string debugMessage; 341 } 342 343 344 final class MultiPart { 345 string contentType; 346 347 InputStream stream; 348 //JsonValue json; 349 string[string] form; 350 } 351 352 string getHTTPVersionString(HTTPVersion ver) 353 @safe nothrow { 354 final switch(ver){ 355 case HTTPVersion.HTTP_1_0: return "HTTP/1.0"; 356 case HTTPVersion.HTTP_1_1: return "HTTP/1.1"; 357 case HTTPVersion.HTTP_2: return "HTTP/2"; 358 } 359 } 360 361 362 HTTPVersion parseHTTPVersion(ref string str) 363 @safe { 364 enforceBadRequest(str.startsWith("HTTP/")); 365 str = str[5 .. $]; 366 int majorVersion = parse!int(str); 367 enforceBadRequest(str.startsWith(".")); 368 str = str[1 .. $]; 369 int minorVersion = parse!int(str); 370 371 enforceBadRequest( majorVersion == 1 && (minorVersion == 0 || minorVersion == 1) ); 372 return minorVersion == 0 ? HTTPVersion.HTTP_1_0 : HTTPVersion.HTTP_1_1; 373 } 374 375 376 /** 377 Takes an input stream that contains data in HTTP chunked format and outputs the raw data. 378 */ 379 final class ChunkedInputStream : InputStream 380 { 381 @safe: 382 383 private { 384 InterfaceProxy!InputStream m_in; 385 ulong m_bytesInCurrentChunk = 0; 386 } 387 388 deprecated("Use createChunkedInputStream() instead.") 389 this(InputStream stream) 390 { 391 this(interfaceProxy!InputStream(stream), true); 392 } 393 394 /// private 395 this(InterfaceProxy!InputStream stream, bool dummy) 396 { 397 assert(!!stream); 398 m_in = stream; 399 readChunk(); 400 } 401 402 @property bool empty() const { return m_bytesInCurrentChunk == 0; } 403 404 @property ulong leastSize() const { return m_bytesInCurrentChunk; } 405 406 @property bool dataAvailableForRead() { return m_bytesInCurrentChunk > 0 && m_in.dataAvailableForRead; } 407 408 const(ubyte)[] peek() 409 { 410 auto dt = m_in.peek(); 411 return dt[0 .. min(dt.length, m_bytesInCurrentChunk)]; 412 } 413 414 size_t read(scope ubyte[] dst, IOMode mode) 415 { 416 enforceBadRequest(!empty, "Read past end of chunked stream."); 417 size_t nbytes = 0; 418 419 while (dst.length > 0) { 420 enforceBadRequest(m_bytesInCurrentChunk > 0, "Reading past end of chunked HTTP stream."); 421 422 auto sz = cast(size_t)min(m_bytesInCurrentChunk, dst.length); 423 m_in.read(dst[0 .. sz]); 424 dst = dst[sz .. $]; 425 m_bytesInCurrentChunk -= sz; 426 nbytes += sz; 427 428 // FIXME: this blocks, but shouldn't for IOMode.once/immediat 429 if( m_bytesInCurrentChunk == 0 ){ 430 // skip current chunk footer and read next chunk 431 ubyte[2] crlf; 432 m_in.read(crlf); 433 enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n'); 434 readChunk(); 435 } 436 437 if (mode != IOMode.all) break; 438 } 439 440 return nbytes; 441 } 442 443 alias read = InputStream.read; 444 445 private void readChunk() 446 { 447 assert(m_bytesInCurrentChunk == 0); 448 // read chunk header 449 logTrace("read next chunk header"); 450 auto ln = () @trusted { return cast(string)m_in.readLine(); } (); 451 logTrace("got chunk header: %s", ln); 452 m_bytesInCurrentChunk = parse!ulong(ln, 16u); 453 454 if( m_bytesInCurrentChunk == 0 ){ 455 // empty chunk denotes the end 456 // skip final chunk footer 457 ubyte[2] crlf; 458 m_in.read(crlf); 459 enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n'); 460 } 461 } 462 } 463 464 /// Creates a new `ChunkedInputStream` instance. 465 ChunkedInputStream chunkedInputStream(IS)(IS source_stream) if (isInputStream!IS) 466 { 467 return new ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); 468 } 469 470 /// Creates a new `ChunkedInputStream` instance. 471 FreeListRef!ChunkedInputStream createChunkedInputStreamFL(IS)(IS source_stream) if (isInputStream!IS) 472 { 473 return () @trusted { return FreeListRef!ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); } (); 474 } 475 476 477 /** 478 Outputs data to an output stream in HTTP chunked format. 479 */ 480 final class ChunkedOutputStream : OutputStream { 481 @safe: 482 483 alias ChunkExtensionCallback = string delegate(in ubyte[] data); 484 private { 485 InterfaceProxy!OutputStream m_out; 486 AllocAppender!(ubyte[]) m_buffer; 487 size_t m_maxBufferSize = 4*1024; 488 bool m_finalized = false; 489 ChunkExtensionCallback m_chunkExtensionCallback = null; 490 } 491 492 deprecated("Use createChunkedOutputStream() instead.") 493 this(OutputStream stream, IAllocator alloc = vibeThreadAllocator()) 494 { 495 this(interfaceProxy!OutputStream(stream), alloc, true); 496 } 497 498 /// private 499 this(InterfaceProxy!OutputStream stream, IAllocator alloc, bool dummy) 500 { 501 m_out = stream; 502 m_buffer = AllocAppender!(ubyte[])(alloc); 503 } 504 505 /** Maximum buffer size used to buffer individual chunks. 506 507 A size of zero means unlimited buffer size. Explicit flush is required 508 in this case to empty the buffer. 509 */ 510 @property size_t maxBufferSize() const { return m_maxBufferSize; } 511 /// ditto 512 @property void maxBufferSize(size_t bytes) { m_maxBufferSize = bytes; if (m_buffer.data.length >= m_maxBufferSize) flush(); } 513 514 /** A delegate used to specify the extensions for each chunk written to the underlying stream. 515 516 The delegate has to be of type `string delegate(in const(ubyte)[] data)` and gets handed the 517 data of each chunk before it is written to the underlying stream. If it's return value is non-empty, 518 it will be added to the chunk's header line. 519 520 The returned chunk extension string should be of the format `key1=value1;key2=value2;[...];keyN=valueN` 521 and **not contain any carriage return or newline characters**. 522 523 Also note that the delegate should accept the passed data through a scoped argument. Thus, **no references 524 to the provided data should be stored in the delegate**. If the data has to be stored for later use, 525 it needs to be copied first. 526 */ 527 @property ChunkExtensionCallback chunkExtensionCallback() const { return m_chunkExtensionCallback; } 528 /// ditto 529 @property void chunkExtensionCallback(ChunkExtensionCallback cb) { m_chunkExtensionCallback = cb; } 530 531 private void append(scope void delegate(scope ubyte[] dst) @safe del, size_t nbytes) 532 { 533 assert(del !is null); 534 auto sz = nbytes; 535 if (m_maxBufferSize > 0 && m_maxBufferSize < m_buffer.data.length + sz) 536 sz = m_maxBufferSize - min(m_buffer.data.length, m_maxBufferSize); 537 538 if (sz > 0) 539 { 540 m_buffer.reserve(sz); 541 () @trusted { 542 m_buffer.append((scope ubyte[] dst) { 543 debug assert(dst.length >= sz); 544 del(dst[0..sz]); 545 return sz; 546 }); 547 } (); 548 } 549 } 550 551 size_t write(in ubyte[] bytes_, IOMode mode) 552 { 553 assert(!m_finalized); 554 const(ubyte)[] bytes = bytes_; 555 size_t nbytes = 0; 556 while (bytes.length > 0) { 557 append((scope ubyte[] dst) { 558 auto n = dst.length; 559 dst[] = bytes[0..n]; 560 bytes = bytes[n..$]; 561 nbytes += n; 562 }, bytes.length); 563 if (mode == IOMode.immediate) break; 564 if (mode == IOMode.once && nbytes > 0) break; 565 if (bytes.length > 0) 566 flush(); 567 } 568 return nbytes; 569 } 570 571 alias write = OutputStream.write; 572 573 void flush() 574 { 575 assert(!m_finalized); 576 auto data = m_buffer.data(); 577 if( data.length ){ 578 writeChunk(data); 579 } 580 m_out.flush(); 581 () @trusted { m_buffer.reset(AppenderResetMode.reuseData); } (); 582 } 583 584 void finalize() 585 { 586 if (m_finalized) return; 587 flush(); 588 () @trusted { m_buffer.reset(AppenderResetMode.freeData); } (); 589 m_finalized = true; 590 writeChunk([]); 591 m_out.flush(); 592 } 593 594 private void writeChunk(in ubyte[] data) 595 { 596 import vibe.stream.wrapper; 597 auto rng = streamOutputRange(m_out); 598 formattedWrite(() @trusted { return &rng; } (), "%x", data.length); 599 if (m_chunkExtensionCallback !is null) 600 { 601 rng.put(';'); 602 auto extension = m_chunkExtensionCallback(data); 603 assert(!extension.startsWith(';')); 604 debug assert(extension.indexOf('\r') < 0); 605 debug assert(extension.indexOf('\n') < 0); 606 rng.put(extension); 607 } 608 rng.put("\r\n"); 609 rng.put(data); 610 rng.put("\r\n"); 611 } 612 } 613 614 /// Creates a new `ChunkedInputStream` instance. 615 ChunkedOutputStream createChunkedOutputStream(OS)(OS destination_stream, IAllocator allocator = vibeThreadAllocator()) if (isOutputStream!OS) 616 { 617 return new ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true); 618 } 619 620 /// Creates a new `ChunkedOutputStream` instance. 621 FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS)(OS destination_stream, IAllocator allocator = vibeThreadAllocator()) if (isOutputStream!OS) 622 { 623 return FreeListRef!ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true); 624 } 625 626 627 /// Parses the cookie from a header field, returning the name of the cookie. 628 /// Implements an algorithm equivalent to https://tools.ietf.org/html/rfc6265#section-5.2 629 /// Returns: the cookie name as return value, populates the dst argument or allocates on the GC for the tuple overload. 630 string parseHTTPCookie(string header_string, scope Cookie dst) 631 @safe 632 in { 633 assert(dst !is null); 634 } do { 635 import std.uni : sicmp; 636 637 if (!header_string.length) 638 return typeof(return).init; 639 640 auto parts = header_string.splitter(';'); 641 auto idx = parts.front.indexOf('='); 642 if (idx == -1) 643 return typeof(return).init; 644 645 auto name = parts.front[0 .. idx].strip(); 646 dst.m_value = parts.front[name.length + 1 .. $].strip(); 647 parts.popFront(); 648 649 if (!name.length) 650 return typeof(return).init; 651 652 foreach(part; parts) { 653 if (!part.length) 654 continue; 655 656 idx = part.indexOf('='); 657 if (idx == -1) { 658 idx = part.length; 659 } 660 auto key = part[0 .. idx].strip(); 661 auto value = part[min(idx + 1, $) .. $].strip(); 662 663 try { 664 if (key.sicmp("httponly") == 0) { 665 dst.m_httpOnly = true; 666 } else if (key.sicmp("secure") == 0) { 667 dst.m_secure = true; 668 } else if (key.sicmp("expires") == 0) { 669 // RFC 822 got updated by RFC 1123 (which is to be used) but is valid for this 670 // this parsing is just for validation 671 parseRFC822DateTimeString(value); 672 dst.m_expires = value; 673 } else if (key.sicmp("max-age") == 0) { 674 if (value.length && value[0] != '-') 675 dst.m_maxAge = value.to!long; 676 } else if (key.sicmp("domain") == 0) { 677 if (value.length && value[0] == '.') 678 value = value[1 .. $]; // the leading . must be stripped (5.2.3) 679 680 enforce!ConvException(value.all!(a => a >= 32), "Cookie Domain must not contain any control characters"); 681 dst.m_domain = value.toLower; // must be lower (5.2.3) 682 } else if (key.sicmp("path") == 0) { 683 if (value.length && value[0] == '/') { 684 enforce!ConvException(value.all!(a => a >= 32), "Cookie Path must not contain any control characters"); 685 dst.m_path = value; 686 } else { 687 dst.m_path = null; 688 } 689 } // else extension value... 690 } catch (DateTimeException) { 691 } catch (ConvException) { 692 } 693 // RFC 6265 says to ignore invalid values on all of these fields 694 } 695 return name; 696 } 697 698 /// ditto 699 Tuple!(string, Cookie) parseHTTPCookie(string header_string) 700 @safe { 701 Cookie cookie = new Cookie(); 702 auto name = parseHTTPCookie(header_string, cookie); 703 return tuple(name, cookie); 704 } 705 706 final class Cookie { 707 @safe: 708 709 private { 710 string m_value; 711 string m_domain; 712 string m_path; 713 string m_expires; 714 long m_maxAge; 715 bool m_secure; 716 bool m_httpOnly; 717 } 718 719 enum Encoding { 720 url, 721 raw, 722 none = raw 723 } 724 725 /// Cookie payload 726 @property void value(string value) { m_value = urlEncode(value); } 727 /// ditto 728 @property string value() const { return urlDecode(m_value); } 729 730 /// Undecoded cookie payload 731 @property void rawValue(string value) { m_value = value; } 732 /// ditto 733 @property string rawValue() const { return m_value; } 734 735 /// The domain for which the cookie is valid 736 @property void domain(string value) { m_domain = value; } 737 /// ditto 738 @property string domain() const { return m_domain; } 739 740 /// The path/local URI for which the cookie is valid 741 @property void path(string value) { m_path = value; } 742 /// ditto 743 @property string path() const { return m_path; } 744 745 /// Expiration date of the cookie 746 @property void expires(string value) { m_expires = value; } 747 /// ditto 748 @property void expires(SysTime value) { m_expires = value.toRFC822DateTimeString(); } 749 /// ditto 750 @property string expires() const { return m_expires; } 751 752 /** Maximum life time of the cookie 753 754 This is the modern variant of `expires`. For backwards compatibility it 755 is recommended to set both properties, or to use the `expire` method. 756 */ 757 @property void maxAge(long value) { m_maxAge = value; } 758 /// ditto 759 @property void maxAge(Duration value) { m_maxAge = value.total!"seconds"; } 760 /// ditto 761 @property long maxAge() const { return m_maxAge; } 762 763 /** Require a secure connection for transmission of this cookie 764 */ 765 @property void secure(bool value) { m_secure = value; } 766 /// ditto 767 @property bool secure() const { return m_secure; } 768 769 /** Prevents access to the cookie from scripts. 770 */ 771 @property void httpOnly(bool value) { m_httpOnly = value; } 772 /// ditto 773 @property bool httpOnly() const { return m_httpOnly; } 774 775 /** Sets the "expires" and "max-age" attributes to limit the life time of 776 the cookie. 777 */ 778 void expire(Duration max_age) 779 { 780 this.expires = Clock.currTime(UTC()) + max_age; 781 this.maxAge = max_age; 782 } 783 /// ditto 784 void expire(SysTime expire_time) 785 { 786 this.expires = expire_time; 787 this.maxAge = expire_time - Clock.currTime(UTC()); 788 } 789 790 /// Sets the cookie value encoded with the specified encoding. 791 void setValue(string value, Encoding encoding) 792 { 793 final switch (encoding) { 794 case Encoding.url: m_value = urlEncode(value); break; 795 case Encoding.none: validateValue(value); m_value = value; break; 796 } 797 } 798 799 /// Writes out the full cookie in HTTP compatible format. 800 void writeString(R)(R dst, string name) 801 if (isOutputRange!(R, char)) 802 { 803 import vibe.textfilter.urlencode; 804 dst.put(name); 805 dst.put('='); 806 validateValue(this.value); 807 dst.put(this.value); 808 if (this.domain && this.domain != "") { 809 dst.put("; Domain="); 810 dst.put(this.domain); 811 } 812 if (this.path != "") { 813 dst.put("; Path="); 814 dst.put(this.path); 815 } 816 if (this.expires != "") { 817 dst.put("; Expires="); 818 dst.put(this.expires); 819 } 820 if (this.maxAge) dst.formattedWrite("; Max-Age=%s", this.maxAge); 821 if (this.secure) dst.put("; Secure"); 822 if (this.httpOnly) dst.put("; HttpOnly"); 823 } 824 825 private static void validateValue(string value) 826 { 827 enforce(!value.canFind(';') && !value.canFind('"')); 828 } 829 } 830 831 unittest { 832 import std.exception : assertThrown; 833 834 auto c = new Cookie; 835 c.value = "foo"; 836 assert(c.value == "foo"); 837 assert(c.rawValue == "foo"); 838 839 c.value = "foo$"; 840 assert(c.value == "foo$"); 841 assert(c.rawValue == "foo%24", c.rawValue); 842 843 c.value = "foo&bar=baz?"; 844 assert(c.value == "foo&bar=baz?"); 845 assert(c.rawValue == "foo%26bar%3Dbaz%3F", c.rawValue); 846 847 c.setValue("foo%", Cookie.Encoding.raw); 848 assert(c.rawValue == "foo%"); 849 assertThrown(c.value); 850 851 assertThrown(c.setValue("foo;bar", Cookie.Encoding.raw)); 852 } 853 854 855 /** 856 */ 857 struct CookieValueMap { 858 @safe: 859 860 struct Cookie { 861 /// Name of the cookie 862 string name; 863 864 /// The raw cookie value as transferred over the wire 865 string rawValue; 866 867 this(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url) 868 { 869 this.name = name; 870 this.setValue(value, encoding); 871 } 872 873 /// Treats the value as URL encoded 874 string value() const { return urlDecode(rawValue); } 875 /// ditto 876 void value(string val) { rawValue = urlEncode(val); } 877 878 /// Sets the cookie value, applying the specified encoding. 879 void setValue(string value, .Cookie.Encoding encoding = .Cookie.Encoding.url) 880 { 881 final switch (encoding) { 882 case .Cookie.Encoding.none: this.rawValue = value; break; 883 case .Cookie.Encoding.url: this.rawValue = urlEncode(value); break; 884 } 885 } 886 } 887 888 private { 889 Cookie[] m_entries; 890 } 891 892 auto length(){ 893 return m_entries.length; 894 } 895 896 string get(string name, string def_value = null) 897 const { 898 foreach (ref c; m_entries) 899 if (c.name == name) 900 return c.value; 901 return def_value; 902 } 903 904 string[] getAll(string name) 905 const { 906 string[] ret; 907 foreach(c; m_entries) 908 if( c.name == name ) 909 ret ~= c.value; 910 return ret; 911 } 912 913 void add(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url){ 914 m_entries ~= Cookie(name, value, encoding); 915 } 916 917 void opIndexAssign(string value, string name) 918 { 919 m_entries ~= Cookie(name, value); 920 } 921 922 string opIndex(string name) 923 const { 924 import core.exception : RangeError; 925 foreach (ref c; m_entries) 926 if (c.name == name) 927 return c.value; 928 throw new RangeError("Non-existent cookie: "~name); 929 } 930 931 int opApply(scope int delegate(ref Cookie) @safe del) 932 { 933 foreach(ref c; m_entries) 934 if( auto ret = del(c) ) 935 return ret; 936 return 0; 937 } 938 939 int opApply(scope int delegate(ref Cookie) @safe del) 940 const { 941 foreach(Cookie c; m_entries) 942 if( auto ret = del(c) ) 943 return ret; 944 return 0; 945 } 946 947 int opApply(scope int delegate(string name, string value) @safe del) 948 { 949 foreach(ref c; m_entries) 950 if( auto ret = del(c.name, c.value) ) 951 return ret; 952 return 0; 953 } 954 955 int opApply(scope int delegate(string name, string value) @safe del) 956 const { 957 foreach(Cookie c; m_entries) 958 if( auto ret = del(c.name, c.value) ) 959 return ret; 960 return 0; 961 } 962 963 auto opBinaryRight(string op)(string name) if(op == "in") 964 { 965 return Ptr(&this, name); 966 } 967 968 auto opBinaryRight(string op)(string name) const if(op == "in") 969 { 970 return const(Ptr)(&this, name); 971 } 972 973 private static struct Ref { 974 private { 975 CookieValueMap* map; 976 string name; 977 } 978 979 @property string get() const { return (*map)[name]; } 980 void opAssign(string newval) { 981 foreach (ref c; *map) 982 if (c.name == name) { 983 c.value = newval; 984 return; 985 } 986 assert(false); 987 } 988 alias get this; 989 } 990 private static struct Ptr { 991 private { 992 CookieValueMap* map; 993 string name; 994 } 995 bool opCast() const { 996 foreach (ref c; map.m_entries) 997 if (c.name == name) 998 return true; 999 return false; 1000 } 1001 inout(Ref) opUnary(string op : "*")() inout { return inout(Ref)(map, name); } 1002 } 1003 } 1004 1005 unittest { 1006 CookieValueMap m; 1007 m["foo"] = "bar;baz%1"; 1008 assert(m["foo"] == "bar;baz%1"); 1009 1010 m["foo"] = "bar"; 1011 assert(m.getAll("foo") == ["bar;baz%1", "bar"]); 1012 1013 assert("foo" in m); 1014 if (auto val = "foo" in m) { 1015 assert(*val == "bar;baz%1"); 1016 } else assert(false); 1017 *("foo" in m) = "baz"; 1018 assert(m["foo"] == "baz"); 1019 }