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 }