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 }