1 /**
2 	Common classes for HTTP clients and servers.
3 
4 	Copyright: © 2012-2015 Sönke Ludwig
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.utils.dictionarylist;
19 import vibe.internal.allocator;
20 import vibe.internal.freelistref;
21 import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy;
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 import std.uni: asLowerCase, sicmp;
34 
35 
36 enum HTTPVersion {
37 	HTTP_1_0,
38 	HTTP_1_1
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 	scope:
217 
218 	public override string toString()
219 	{
220 		return httpMethodString(method) ~ " " ~ requestURL ~ " " ~ getHTTPVersionString(httpVersion);
221 	}
222 
223 	/** Shortcut to the 'Host' header (always present for HTTP 1.1)
224 	*/
225 	@property string host() const { auto ph = "Host" in headers; return ph ? *ph : null; }
226 	/// ditto
227 	@property void host(string v) { headers["Host"] = v; }
228 
229 	/** Returns the mime type part of the 'Content-Type' header.
230 
231 		This function gets the pure mime type (e.g. "text/plain")
232 		without any supplimentary parameters such as "charset=...".
233 		Use contentTypeParameters to get any parameter string or
234 		headers["Content-Type"] to get the raw value.
235 	*/
236 	@property string contentType()
237 	const {
238 		auto pv = "Content-Type" in headers;
239 		if( !pv ) return null;
240 		auto idx = std..string.indexOf(*pv, ';');
241 		return idx >= 0 ? (*pv)[0 .. idx] : *pv;
242 	}
243 	/// ditto
244 	@property void contentType(string ct) { headers["Content-Type"] = ct; }
245 
246 	/** Returns any supplementary parameters of the 'Content-Type' header.
247 
248 		This is a semicolon separated ist of key/value pairs. Usually, if set,
249 		this contains the character set used for text based content types.
250 	*/
251 	@property string contentTypeParameters()
252 	const {
253 		auto pv = "Content-Type" in headers;
254 		if( !pv ) return null;
255 		auto idx = std..string.indexOf(*pv, ';');
256 		return idx >= 0 ? (*pv)[idx+1 .. $] : null;
257 	}
258 
259 	/** Determines if the connection persists across requests.
260 	*/
261 	@property bool persistent() const
262 	{
263 		auto ph = "connection" in headers;
264 		switch(httpVersion) {
265 			case HTTPVersion.HTTP_1_0:
266 				if (ph && asLowerCase(*ph).equal("keep-alive")) return true;
267 				return false;
268 			case HTTPVersion.HTTP_1_1:
269 				if (ph && !(asLowerCase(*ph).equal("keep-alive"))) return false;
270 				return true;
271 			default:
272 				return false;
273 		}
274 	}
275 }
276 
277 
278 /**
279 	Represents the HTTP response from the server back to the client.
280 */
281 class HTTPResponse {
282 	@safe:
283 
284 	protected DictionaryList!Cookie m_cookies;
285 
286 	public {
287 		/// The protocol version of the response - should not be changed
288 		HTTPVersion httpVersion = HTTPVersion.HTTP_1_1;
289 
290 		/// The status code of the response, 200 by default
291 		int statusCode = HTTPStatus.ok;
292 
293 		/** The status phrase of the response
294 
295 			If no phrase is set, a default one corresponding to the status code will be used.
296 		*/
297 		string statusPhrase;
298 
299 		/// The response header fields
300 		InetHeaderMap headers;
301 
302 		/// All cookies that shall be set on the client for this request
303 		@property ref DictionaryList!Cookie cookies() { return m_cookies; }
304 	}
305 
306 	scope:
307 
308 	public override string toString()
309 	{
310 		auto app = appender!string();
311 		formattedWrite(app, "%s %d %s", getHTTPVersionString(this.httpVersion), this.statusCode, this.statusPhrase);
312 		return app.data;
313 	}
314 
315 	/** Shortcut to the "Content-Type" header
316 	*/
317 	@property string contentType() const { auto pct = "Content-Type" in headers; return pct ? *pct : "application/octet-stream"; }
318 	/// ditto
319 	@property void contentType(string ct) { headers["Content-Type"] = ct; }
320 }
321 
322 
323 /**
324 	Respresents a HTTP response status.
325 
326 	Throwing this exception from within a request handler will produce a matching error page.
327 */
328 class HTTPStatusException : Exception {
329 	pure nothrow @safe @nogc:
330 
331 	private {
332 		int m_status;
333 	}
334 
335 	this(int status, string message = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
336 	{
337 		super(message.length ? message : httpStatusText(status), file, line, next);
338 		m_status = status;
339 	}
340 
341 	/// The HTTP status code
342 	@property int status() const { return m_status; }
343 
344 	string debugMessage;
345 }
346 
347 
348 final class MultiPart {
349 	string contentType;
350 
351 	InputStream stream;
352 	//JsonValue json;
353 	string[string] form;
354 }
355 
356 /**
357  * Returns:
358  *     The version string corresponding to the `ver`,
359  *     suitable for usage in the start line of the request.
360  */
361 string getHTTPVersionString(HTTPVersion ver)
362 nothrow pure @nogc @safe {
363 	final switch(ver){
364 		case HTTPVersion.HTTP_1_0: return "HTTP/1.0";
365 		case HTTPVersion.HTTP_1_1: return "HTTP/1.1";
366 	}
367 }
368 
369 
370 HTTPVersion parseHTTPVersion(ref string str)
371 @safe {
372 	enforceBadRequest(str.startsWith("HTTP/1."));
373 	str = str[7 .. $];
374 	int minorVersion = parse!int(str);
375 
376 	enforceBadRequest( minorVersion == 0 || minorVersion == 1 );
377 	return minorVersion == 0 ? HTTPVersion.HTTP_1_0 : HTTPVersion.HTTP_1_1;
378 }
379 
380 
381 /**
382 	Takes an input stream that contains data in HTTP chunked format and outputs the raw data.
383 */
384 final class ChunkedInputStream : InputStream
385 {
386 	@safe:
387 
388 	private {
389 		InterfaceProxy!InputStream m_in;
390 		ulong m_bytesInCurrentChunk = 0;
391 	}
392 
393 	/// private
394 	this(InterfaceProxy!InputStream stream, bool dummy)
395 	{
396 		assert(!!stream);
397 		m_in = stream;
398 		readChunk();
399 	}
400 
401 	@property bool empty() const { return m_bytesInCurrentChunk == 0; }
402 
403 	@property ulong leastSize() const { return m_bytesInCurrentChunk; }
404 
405 	@property bool dataAvailableForRead() { return m_bytesInCurrentChunk > 0 && m_in.dataAvailableForRead; }
406 
407 	const(ubyte)[] peek()
408 	{
409 		auto dt = m_in.peek();
410 		return dt[0 .. min(dt.length, m_bytesInCurrentChunk)];
411 	}
412 
413 	size_t read(scope ubyte[] dst, IOMode mode)
414 	{
415 		enforceBadRequest(!empty, "Read past end of chunked stream.");
416 		size_t nbytes = 0;
417 
418 		while (dst.length > 0) {
419 			enforceBadRequest(m_bytesInCurrentChunk > 0, "Reading past end of chunked HTTP stream.");
420 
421 			auto sz = cast(size_t)min(m_bytesInCurrentChunk, dst.length);
422 			m_in.read(dst[0 .. sz]);
423 			dst = dst[sz .. $];
424 			m_bytesInCurrentChunk -= sz;
425 			nbytes += sz;
426 
427 			// FIXME: this blocks, but shouldn't for IOMode.once/immediat
428 			if( m_bytesInCurrentChunk == 0 ){
429 				// skip current chunk footer and read next chunk
430 				ubyte[2] crlf;
431 				m_in.read(crlf);
432 				enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n');
433 				readChunk();
434 			}
435 
436 			if (mode != IOMode.all) break;
437 		}
438 
439 		return nbytes;
440 	}
441 
442 	alias read = InputStream.read;
443 
444 	private void readChunk()
445 	{
446 		assert(m_bytesInCurrentChunk == 0);
447 		// read chunk header
448 		logTrace("read next chunk header");
449 		auto ln = () @trusted { return cast(string)m_in.readLine(); } ();
450 		logTrace("got chunk header: %s", ln);
451 		m_bytesInCurrentChunk = parse!ulong(ln, 16u);
452 
453 		if( m_bytesInCurrentChunk == 0 ){
454 			// empty chunk denotes the end
455 			// skip final chunk footer
456 			ubyte[2] crlf;
457 			m_in.read(crlf);
458 			enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n');
459 		}
460 	}
461 }
462 
463 /// Creates a new `ChunkedInputStream` instance.
464 ChunkedInputStream chunkedInputStream(IS)(IS source_stream) if (isInputStream!IS)
465 {
466 	return new ChunkedInputStream(interfaceProxy!InputStream(source_stream), true);
467 }
468 
469 /// Creates a new `ChunkedInputStream` instance.
470 FreeListRef!ChunkedInputStream createChunkedInputStreamFL(IS)(IS source_stream) if (isInputStream!IS)
471 {
472 	return () @trusted { return FreeListRef!ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); } ();
473 }
474 
475 
476 /**
477 	Outputs data to an output stream in HTTP chunked format.
478 */
479 final class ChunkedOutputStream : OutputStream {
480 	@safe:
481 
482 	alias ChunkExtensionCallback = string delegate(in ubyte[] data);
483 	private {
484 		InterfaceProxy!OutputStream m_out;
485 		AllocAppender!(ubyte[]) m_buffer;
486 		size_t m_maxBufferSize = 4*1024;
487 		bool m_finalized = false;
488 		ChunkExtensionCallback m_chunkExtensionCallback = null;
489 	}
490 
491 	/// private
492 	this(InterfaceProxy!OutputStream stream, IAllocator alloc, bool dummy)
493 	{
494 		m_out = stream;
495 		m_buffer = AllocAppender!(ubyte[])(alloc);
496 	}
497 
498 	/** Maximum buffer size used to buffer individual chunks.
499 
500 		A size of zero means unlimited buffer size. Explicit flush is required
501 		in this case to empty the buffer.
502 	*/
503 	@property size_t maxBufferSize() const { return m_maxBufferSize; }
504 	/// ditto
505 	@property void maxBufferSize(size_t bytes) { m_maxBufferSize = bytes; if (m_buffer.data.length >= m_maxBufferSize) flush(); }
506 
507 	/** A delegate used to specify the extensions for each chunk written to the underlying stream.
508 
509 	 	The delegate has to be of type `string delegate(in const(ubyte)[] data)` and gets handed the
510 	 	data of each chunk before it is written to the underlying stream. If it's return value is non-empty,
511 	 	it will be added to the chunk's header line.
512 
513 	 	The returned chunk extension string should be of the format `key1=value1;key2=value2;[...];keyN=valueN`
514 	 	and **not contain any carriage return or newline characters**.
515 
516 	 	Also note that the delegate should accept the passed data through a scoped argument. Thus, **no references
517 	 	to the provided data should be stored in the delegate**. If the data has to be stored for later use,
518 	 	it needs to be copied first.
519 	 */
520 	@property ChunkExtensionCallback chunkExtensionCallback() const { return m_chunkExtensionCallback; }
521 	/// ditto
522 	@property void chunkExtensionCallback(ChunkExtensionCallback cb) { m_chunkExtensionCallback = cb; }
523 
524 	private void append(scope void delegate(scope ubyte[] dst) @safe del, size_t nbytes)
525 	{
526 		assert(del !is null);
527 		auto sz = nbytes;
528 		if (m_maxBufferSize > 0 && m_maxBufferSize < m_buffer.data.length + sz)
529 			sz = m_maxBufferSize - min(m_buffer.data.length, m_maxBufferSize);
530 
531 		if (sz > 0)
532 		{
533 			m_buffer.reserve(sz);
534 			() @trusted {
535 				m_buffer.append((scope ubyte[] dst) {
536 					debug assert(dst.length >= sz);
537 					del(dst[0..sz]);
538 					return sz;
539 				});
540 			} ();
541 		}
542 	}
543 
544 	static if (is(typeof(.OutputStream.outputStreamVersion)) && .OutputStream.outputStreamVersion > 1) {
545 		override size_t write(scope const(ubyte)[] bytes_, IOMode mode) { return doWrite(bytes_, mode); }
546 	} else {
547 		override size_t write(in ubyte[] bytes_, IOMode mode) { return doWrite(bytes_, mode); }
548 	}
549 
550 	alias write = OutputStream.write;
551 
552 	private size_t doWrite(scope const(ubyte)[] bytes_, IOMode mode)
553 	{
554 		assert(!m_finalized);
555 		const(ubyte)[] bytes = bytes_;
556 		size_t nbytes = 0;
557 		while (bytes.length > 0) {
558 			append((scope ubyte[] dst) {
559 					auto n = dst.length;
560 					dst[] = bytes[0..n];
561 					bytes = bytes[n..$];
562 					nbytes += n;
563 				}, bytes.length);
564 			if (mode == IOMode.immediate) break;
565 			if (mode == IOMode.once && nbytes > 0) break;
566 			if (bytes.length > 0)
567 				flush();
568 		}
569 		return nbytes;
570 	}
571 
572 	void flush()
573 	{
574 		assert(!m_finalized);
575 		auto data = m_buffer.data();
576 		if( data.length ){
577 			writeChunk(data);
578 		}
579 		m_out.flush();
580 		() @trusted { m_buffer.reset(AppenderResetMode.reuseData); } ();
581 	}
582 
583 	void finalize()
584 	{
585 		if (m_finalized) return;
586 		flush();
587 		() @trusted { m_buffer.reset(AppenderResetMode.freeData); } ();
588 		m_finalized = true;
589 		writeChunk([]);
590 		m_out.flush();
591 	}
592 
593 	private void writeChunk(in ubyte[] data)
594 	{
595 		import vibe.stream.wrapper;
596 		auto rng = streamOutputRange(m_out);
597 		formattedWrite(() @trusted { return &rng; } (), "%x", data.length);
598 		if (m_chunkExtensionCallback !is null)
599 		{
600 			rng.put(';');
601 			auto extension = m_chunkExtensionCallback(data);
602 			assert(!extension.startsWith(';'));
603 			debug assert(extension.indexOf('\r') < 0);
604 			debug assert(extension.indexOf('\n') < 0);
605 			rng.put(extension);
606 		}
607 		rng.put("\r\n");
608 		rng.put(data);
609 		rng.put("\r\n");
610 	}
611 }
612 
613 /// Creates a new `ChunkedInputStream` instance.
614 ChunkedOutputStream createChunkedOutputStream(OS)(OS destination_stream, IAllocator allocator = theAllocator()) if (isOutputStream!OS)
615 {
616 	return new ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true);
617 }
618 
619 /// Creates a new `ChunkedOutputStream` instance.
620 FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS)(OS destination_stream, IAllocator allocator = theAllocator()) if (isOutputStream!OS)
621 {
622 	return FreeListRef!ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true);
623 }
624 
625 /// Parses the cookie from a header field, returning the name of the cookie.
626 /// Implements an algorithm equivalent to https://tools.ietf.org/html/rfc6265#section-5.2
627 /// Returns: the cookie name as return value, populates the dst argument or allocates on the GC for the tuple overload.
628 string parseHTTPCookie(string header_string, scope Cookie dst)
629 @safe
630 in {
631 	assert(dst !is null);
632 } do {
633 	if (!header_string.length)
634 		return typeof(return).init;
635 
636 	auto parts = header_string.splitter(';');
637 	auto idx = parts.front.indexOf('=');
638 	if (idx == -1)
639 		return typeof(return).init;
640 
641 	auto name = parts.front[0 .. idx].strip();
642 	dst.m_value = parts.front[name.length + 1 .. $].strip();
643 	parts.popFront();
644 
645 	if (!name.length)
646 		return typeof(return).init;
647 
648 	foreach(part; parts) {
649 		if (!part.length)
650 			continue;
651 
652 		idx = part.indexOf('=');
653 		if (idx == -1) {
654 			idx = part.length;
655 		}
656 		auto key = part[0 .. idx].strip();
657 		auto value = part[min(idx + 1, $) .. $].strip();
658 
659 		try {
660 			if (key.sicmp("httponly") == 0) {
661 				dst.m_httpOnly = true;
662 			} else if (key.sicmp("secure") == 0) {
663 				dst.m_secure = true;
664 			} else if (key.sicmp("expires") == 0) {
665 				// RFC 822 got updated by RFC 1123 (which is to be used) but is valid for this
666 				// this parsing is just for validation
667 				parseRFC822DateTimeString(value);
668 				dst.m_expires = value;
669 			} else if (key.sicmp("max-age") == 0) {
670 				if (value.length && value[0] != '-')
671 					dst.m_maxAge = value.to!long;
672 			} else if (key.sicmp("domain") == 0) {
673 				if (value.length && value[0] == '.')
674 					value = value[1 .. $]; // the leading . must be stripped (5.2.3)
675 
676 				enforce!ConvException(value.all!(a => a >= 32), "Cookie Domain must not contain any control characters");
677 				dst.m_domain = value.toLower; // must be lower (5.2.3)
678 			} else if (key.sicmp("path") == 0) {
679 				if (value.length && value[0] == '/') {
680 					enforce!ConvException(value.all!(a => a >= 32), "Cookie Path must not contain any control characters");
681 					dst.m_path = value;
682 				} else {
683 					dst.m_path = null;
684 				}
685 			} // else extension value...
686 		} catch (DateTimeException) {
687 		} catch (ConvException) {
688 		}
689 		// RFC 6265 says to ignore invalid values on all of these fields
690 	}
691 	return name;
692 }
693 
694 /// ditto
695 Tuple!(string, Cookie) parseHTTPCookie(string header_string)
696 @safe {
697 	Cookie cookie = new Cookie();
698 	auto name = parseHTTPCookie(header_string, cookie);
699 	return tuple(name, cookie);
700 }
701 
702 final class Cookie {
703 	@safe:
704 
705 	private {
706 		string m_value;
707 		string m_domain;
708 		string m_path;
709 		string m_expires;
710 		long m_maxAge;
711 		bool m_secure;
712 		bool m_httpOnly;
713 		SameSite m_sameSite;
714 	}
715 
716 	enum Encoding {
717 		url,
718 		raw,
719 		none = raw
720 	}
721 
722 	enum SameSite {
723 		default_,
724 		lax,
725 		strict,
726 	}
727 
728 	/// Cookie payload
729 	@property void value(string value) { m_value = urlEncode(value); }
730 	/// ditto
731 	@property string value() const { return urlDecode(m_value); }
732 
733 	/// Undecoded cookie payload
734 	@property void rawValue(string value) { m_value = value; }
735 	/// ditto
736 	@property string rawValue() const { return m_value; }
737 
738 	/// The domain for which the cookie is valid
739 	@property void domain(string value) { m_domain = value; }
740 	/// ditto
741 	@property string domain() const { return m_domain; }
742 
743 	/// The path/local URI for which the cookie is valid
744 	@property void path(string value) { m_path = value; }
745 	/// ditto
746 	@property string path() const { return m_path; }
747 
748 	/// Expiration date of the cookie
749 	@property void expires(string value) { m_expires = value; }
750 	/// ditto
751 	@property void expires(SysTime value) { m_expires = value.toRFC822DateTimeString(); }
752 	/// ditto
753 	@property string expires() const { return m_expires; }
754 
755 	/** Maximum life time of the cookie
756 
757 		This is the modern variant of `expires`. For backwards compatibility it
758 		is recommended to set both properties, or to use the `expire` method.
759 	*/
760 	@property void maxAge(long value) { m_maxAge = value; }
761 	/// ditto
762 	@property void maxAge(Duration value) { m_maxAge = value.total!"seconds"; }
763 	/// ditto
764 	@property long maxAge() const { return m_maxAge; }
765 
766 	/** Require a secure connection for transmission of this cookie
767 	*/
768 	@property void secure(bool value) { m_secure = value; }
769 	/// ditto
770 	@property bool secure() const { return m_secure; }
771 
772 	/** Prevents access to the cookie from scripts.
773 	*/
774 	@property void httpOnly(bool value) { m_httpOnly = value; }
775 	/// ditto
776 	@property bool httpOnly() const { return m_httpOnly; }
777 
778 	/** Prevent cross-site request forgery.
779 	*/
780 	@property void sameSite(Cookie.SameSite value) { m_sameSite = value; }
781 	/// ditto
782 	@property Cookie.SameSite sameSite() const { return m_sameSite; }
783 
784 	/** Sets the "expires" and "max-age" attributes to limit the life time of
785 		the cookie.
786 	*/
787 	void expire(Duration max_age)
788 	{
789 		this.expires = Clock.currTime(UTC()) + max_age;
790 		this.maxAge = max_age;
791 	}
792 	/// ditto
793 	void expire(SysTime expire_time)
794 	{
795 		this.expires = expire_time;
796 		this.maxAge = expire_time - Clock.currTime(UTC());
797 	}
798 
799 	/// Sets the cookie value encoded with the specified encoding.
800 	void setValue(string value, Encoding encoding)
801 	{
802 		final switch (encoding) {
803 			case Encoding.url: m_value = urlEncode(value); break;
804 			case Encoding.none: validateValue(value); m_value = value; break;
805 		}
806 	}
807 
808 	/// Writes out the full cookie in HTTP compatible format.
809 	void writeString(R)(R dst, string name)
810 		if (isOutputRange!(R, char))
811 	{
812 		import vibe.textfilter.urlencode;
813 		dst.put(name);
814 		dst.put('=');
815 		validateValue(this.value);
816 		dst.put(this.value);
817 		if (this.domain && this.domain != "") {
818 			dst.put("; Domain=");
819 			dst.put(this.domain);
820 		}
821 		if (this.path != "") {
822 			dst.put("; Path=");
823 			dst.put(this.path);
824 		}
825 		if (this.expires != "") {
826 			dst.put("; Expires=");
827 			dst.put(this.expires);
828 		}
829 		if (this.maxAge) dst.formattedWrite("; Max-Age=%s", this.maxAge);
830 		if (this.secure) dst.put("; Secure");
831 		if (this.httpOnly) dst.put("; HttpOnly");
832 		with(Cookie.SameSite)
833 		final switch(this.sameSite) {
834 			case default_: break;
835 			case lax: dst.put("; SameSite=Lax"); break;
836 			case strict: dst.put("; SameSite=Strict"); break;
837 		}
838 	}
839 
840 	private static void validateValue(string value)
841 	{
842 		enforce(!value.canFind(';') && !value.canFind('"'));
843 	}
844 }
845 
846 unittest {
847 	import std.exception : assertThrown;
848 
849 	auto c = new Cookie;
850 	c.value = "foo";
851 	assert(c.value == "foo");
852 	assert(c.rawValue == "foo");
853 
854 	c.value = "foo$";
855 	assert(c.value == "foo$");
856 	assert(c.rawValue == "foo%24", c.rawValue);
857 
858 	c.value = "foo&bar=baz?";
859 	assert(c.value == "foo&bar=baz?");
860 	assert(c.rawValue == "foo%26bar%3Dbaz%3F", c.rawValue);
861 
862 	c.setValue("foo%", Cookie.Encoding.raw);
863 	assert(c.rawValue == "foo%");
864 	assertThrown(c.value);
865 
866 	assertThrown(c.setValue("foo;bar", Cookie.Encoding.raw));
867 
868 	auto tup = parseHTTPCookie("foo=bar; HttpOnly; Secure; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=60000; Domain=foo.com; Path=/users");
869 	assert(tup[0] == "foo");
870 	assert(tup[1].value == "bar");
871 	assert(tup[1].httpOnly == true);
872 	assert(tup[1].secure == true);
873 	assert(tup[1].expires == "Wed, 09 Jun 2021 10:18:14 GMT");
874 	assert(tup[1].maxAge == 60000L);
875 	assert(tup[1].domain == "foo.com");
876 	assert(tup[1].path == "/users");
877 
878 	tup = parseHTTPCookie("SESSIONID=0123456789ABCDEF0123456789ABCDEF; Path=/site; HttpOnly");
879 	assert(tup[0] == "SESSIONID");
880 	assert(tup[1].value == "0123456789ABCDEF0123456789ABCDEF");
881 	assert(tup[1].httpOnly == true);
882 	assert(tup[1].secure == false);
883 	assert(tup[1].expires == "");
884 	assert(tup[1].maxAge == 0);
885 	assert(tup[1].domain == "");
886 	assert(tup[1].path == "/site");
887 
888 	tup = parseHTTPCookie("invalid");
889 	assert(!tup[0].length);
890 
891 	tup = parseHTTPCookie("valid=");
892 	assert(tup[0] == "valid");
893 	assert(tup[1].value == "");
894 
895 	tup = parseHTTPCookie("valid=;Path=/bar;Path=foo;Expires=14   ; Something   ; Domain=..example.org");
896 	assert(tup[0] == "valid");
897 	assert(tup[1].value == "");
898 	assert(tup[1].httpOnly == false);
899 	assert(tup[1].secure == false);
900 	assert(tup[1].expires == "");
901 	assert(tup[1].maxAge == 0);
902 	assert(tup[1].domain == ".example.org"); // spec says you must strip only the first leading dot
903 	assert(tup[1].path == "");
904 }
905 
906 
907 /**
908 */
909 struct CookieValueMap {
910 	@safe:
911 
912 	struct Cookie {
913 		/// Name of the cookie
914 		string name;
915 
916 		/// The raw cookie value as transferred over the wire
917 		string rawValue;
918 
919 		this(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url)
920 		{
921 			this.name = name;
922 			this.setValue(value, encoding);
923 		}
924 
925 		/// Treats the value as URL encoded
926 		string value() const { return urlDecode(rawValue); }
927 		/// ditto
928 		void value(string val) { rawValue = urlEncode(val); }
929 
930 		/// Sets the cookie value, applying the specified encoding.
931 		void setValue(string value, .Cookie.Encoding encoding = .Cookie.Encoding.url)
932 		{
933 			final switch (encoding) {
934 				case .Cookie.Encoding.none: this.rawValue = value; break;
935 				case .Cookie.Encoding.url: this.rawValue = urlEncode(value); break;
936 			}
937 		}
938 	}
939 
940 	private {
941 		Cookie[] m_entries;
942 	}
943 
944 	auto length(){
945 		return m_entries.length;
946 	}
947 
948 	string get(string name, string def_value = null)
949 	const {
950 		foreach (ref c; m_entries)
951 			if (c.name == name)
952 				return c.value;
953 		return def_value;
954 	}
955 
956 	string[] getAll(string name)
957 	const {
958 		string[] ret;
959 		foreach(c; m_entries)
960 			if( c.name == name )
961 				ret ~= c.value;
962 		return ret;
963 	}
964 
965 	void add(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url){
966 		m_entries ~= Cookie(name, value, encoding);
967 	}
968 
969 	void opIndexAssign(string value, string name)
970 	{
971 		m_entries ~= Cookie(name, value);
972 	}
973 
974 	string opIndex(string name)
975 	const {
976 		import core.exception : RangeError;
977 		foreach (ref c; m_entries)
978 			if (c.name == name)
979 				return c.value;
980 		throw new RangeError("Non-existent cookie: "~name);
981 	}
982 
983 	int opApply(scope int delegate(ref Cookie) @safe del)
984 	{
985 		foreach(ref c; m_entries)
986 			if( auto ret = del(c) )
987 				return ret;
988 		return 0;
989 	}
990 
991 	int opApply(scope int delegate(ref Cookie) @safe del)
992 	const {
993 		foreach(Cookie c; m_entries)
994 			if( auto ret = del(c) )
995 				return ret;
996 		return 0;
997 	}
998 
999 	int opApply(scope int delegate(string name, string value) @safe del)
1000 	{
1001 		foreach(ref c; m_entries)
1002 			if( auto ret = del(c.name, c.value) )
1003 				return ret;
1004 		return 0;
1005 	}
1006 
1007 	int opApply(scope int delegate(string name, string value) @safe del)
1008 	const {
1009 		foreach(Cookie c; m_entries)
1010 			if( auto ret = del(c.name, c.value) )
1011 				return ret;
1012 		return 0;
1013 	}
1014 
1015 	auto opBinaryRight(string op)(string name) if(op == "in")
1016 	{
1017 		return Ptr(&this, name);
1018 	}
1019 
1020 	auto opBinaryRight(string op)(string name) const if(op == "in")
1021 	{
1022 		return const(Ptr)(&this, name);
1023 	}
1024 
1025 	private static struct Ref {
1026 		private {
1027 			CookieValueMap* map;
1028 			string name;
1029 		}
1030 
1031 		@property string get() const { return (*map)[name]; }
1032 		void opAssign(string newval) {
1033 			foreach (ref c; *map)
1034 				if (c.name == name) {
1035 					c.value = newval;
1036 					return;
1037 				}
1038 			assert(false);
1039 		}
1040 		alias get this;
1041 	}
1042 	private static struct Ptr {
1043 		private {
1044 			CookieValueMap* map;
1045 			string name;
1046 		}
1047 		bool opCast() const {
1048 			foreach (ref c; map.m_entries)
1049 				if (c.name == name)
1050 					return true;
1051 			return false;
1052 		}
1053 		inout(Ref) opUnary(string op : "*")() inout { return inout(Ref)(map, name); }
1054 	}
1055 }
1056 
1057 unittest {
1058 	CookieValueMap m;
1059 	m["foo"] = "bar;baz%1";
1060 	assert(m["foo"] == "bar;baz%1");
1061 
1062 	m["foo"] = "bar";
1063 	assert(m.getAll("foo") == ["bar;baz%1", "bar"]);
1064 
1065 	assert("foo" in m);
1066 	if (auto val = "foo" in m) {
1067 		assert(*val == "bar;baz%1");
1068 	} else assert(false);
1069 	*("foo" in m) = "baz";
1070 	assert(m["foo"] == "baz");
1071 }