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