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 	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 && asLowerCase(*ph).equal("keep-alive")) return true;
265 				return false;
266 			case HTTPVersion.HTTP_1_1:
267 				if (ph && !(asLowerCase(*ph).equal("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 	pure nothrow @safe @nogc:
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.length ? 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 /**
353  * Returns:
354  *     The version string corresponding to the `ver`,
355  *     suitable for usage in the start line of the request.
356  */
357 string getHTTPVersionString(HTTPVersion ver)
358 nothrow pure @nogc @safe {
359 	final switch(ver){
360 		case HTTPVersion.HTTP_1_0: return "HTTP/1.0";
361 		case HTTPVersion.HTTP_1_1: return "HTTP/1.1";
362 	}
363 }
364 
365 
366 HTTPVersion parseHTTPVersion(ref string str)
367 @safe {
368 	enforceBadRequest(str.startsWith("HTTP/1."));
369 	str = str[7 .. $];
370 	int minorVersion = parse!int(str);
371 
372 	enforceBadRequest( minorVersion == 0 || minorVersion == 1 );
373 	return minorVersion == 0 ? HTTPVersion.HTTP_1_0 : HTTPVersion.HTTP_1_1;
374 }
375 
376 
377 /**
378 	Takes an input stream that contains data in HTTP chunked format and outputs the raw data.
379 */
380 final class ChunkedInputStream : InputStream
381 {
382 	@safe:
383 
384 	private {
385 		InterfaceProxy!InputStream m_in;
386 		ulong m_bytesInCurrentChunk = 0;
387 	}
388 
389 	deprecated("Use createChunkedInputStream() instead.")
390 	this(InputStream stream)
391 	{
392 		this(interfaceProxy!InputStream(stream), true);
393 	}
394 
395 	/// private
396 	this(InterfaceProxy!InputStream stream, bool dummy)
397 	{
398 		assert(!!stream);
399 		m_in = stream;
400 		readChunk();
401 	}
402 
403 	@property bool empty() const { return m_bytesInCurrentChunk == 0; }
404 
405 	@property ulong leastSize() const { return m_bytesInCurrentChunk; }
406 
407 	@property bool dataAvailableForRead() { return m_bytesInCurrentChunk > 0 && m_in.dataAvailableForRead; }
408 
409 	const(ubyte)[] peek()
410 	{
411 		auto dt = m_in.peek();
412 		return dt[0 .. min(dt.length, m_bytesInCurrentChunk)];
413 	}
414 
415 	size_t read(scope ubyte[] dst, IOMode mode)
416 	{
417 		enforceBadRequest(!empty, "Read past end of chunked stream.");
418 		size_t nbytes = 0;
419 
420 		while (dst.length > 0) {
421 			enforceBadRequest(m_bytesInCurrentChunk > 0, "Reading past end of chunked HTTP stream.");
422 
423 			auto sz = cast(size_t)min(m_bytesInCurrentChunk, dst.length);
424 			m_in.read(dst[0 .. sz]);
425 			dst = dst[sz .. $];
426 			m_bytesInCurrentChunk -= sz;
427 			nbytes += sz;
428 
429 			// FIXME: this blocks, but shouldn't for IOMode.once/immediat
430 			if( m_bytesInCurrentChunk == 0 ){
431 				// skip current chunk footer and read next chunk
432 				ubyte[2] crlf;
433 				m_in.read(crlf);
434 				enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n');
435 				readChunk();
436 			}
437 
438 			if (mode != IOMode.all) break;
439 		}
440 
441 		return nbytes;
442 	}
443 
444 	alias read = InputStream.read;
445 
446 	private void readChunk()
447 	{
448 		assert(m_bytesInCurrentChunk == 0);
449 		// read chunk header
450 		logTrace("read next chunk header");
451 		auto ln = () @trusted { return cast(string)m_in.readLine(); } ();
452 		logTrace("got chunk header: %s", ln);
453 		m_bytesInCurrentChunk = parse!ulong(ln, 16u);
454 
455 		if( m_bytesInCurrentChunk == 0 ){
456 			// empty chunk denotes the end
457 			// skip final chunk footer
458 			ubyte[2] crlf;
459 			m_in.read(crlf);
460 			enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n');
461 		}
462 	}
463 }
464 
465 /// Creates a new `ChunkedInputStream` instance.
466 ChunkedInputStream chunkedInputStream(IS)(IS source_stream) if (isInputStream!IS)
467 {
468 	return new ChunkedInputStream(interfaceProxy!InputStream(source_stream), true);
469 }
470 
471 /// Creates a new `ChunkedInputStream` instance.
472 FreeListRef!ChunkedInputStream createChunkedInputStreamFL(IS)(IS source_stream) if (isInputStream!IS)
473 {
474 	return () @trusted { return FreeListRef!ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); } ();
475 }
476 
477 
478 /**
479 	Outputs data to an output stream in HTTP chunked format.
480 */
481 final class ChunkedOutputStream : OutputStream {
482 	@safe:
483 
484 	alias ChunkExtensionCallback = string delegate(in ubyte[] data);
485 	private {
486 		InterfaceProxy!OutputStream m_out;
487 		AllocAppender!(ubyte[]) m_buffer;
488 		size_t m_maxBufferSize = 4*1024;
489 		bool m_finalized = false;
490 		ChunkExtensionCallback m_chunkExtensionCallback = null;
491 	}
492 
493 	deprecated("Use createChunkedOutputStream() instead.")
494 	this(OutputStream stream, IAllocator alloc = vibeThreadAllocator())
495 	{
496 		this(interfaceProxy!OutputStream(stream), alloc, true);
497 	}
498 
499 	/// private
500 	this(InterfaceProxy!OutputStream stream, IAllocator alloc, bool dummy)
501 	{
502 		m_out = stream;
503 		m_buffer = AllocAppender!(ubyte[])(alloc);
504 	}
505 
506 	/** Maximum buffer size used to buffer individual chunks.
507 
508 		A size of zero means unlimited buffer size. Explicit flush is required
509 		in this case to empty the buffer.
510 	*/
511 	@property size_t maxBufferSize() const { return m_maxBufferSize; }
512 	/// ditto
513 	@property void maxBufferSize(size_t bytes) { m_maxBufferSize = bytes; if (m_buffer.data.length >= m_maxBufferSize) flush(); }
514 
515 	/** A delegate used to specify the extensions for each chunk written to the underlying stream.
516 
517 	 	The delegate has to be of type `string delegate(in const(ubyte)[] data)` and gets handed the
518 	 	data of each chunk before it is written to the underlying stream. If it's return value is non-empty,
519 	 	it will be added to the chunk's header line.
520 
521 	 	The returned chunk extension string should be of the format `key1=value1;key2=value2;[...];keyN=valueN`
522 	 	and **not contain any carriage return or newline characters**.
523 
524 	 	Also note that the delegate should accept the passed data through a scoped argument. Thus, **no references
525 	 	to the provided data should be stored in the delegate**. If the data has to be stored for later use,
526 	 	it needs to be copied first.
527 	 */
528 	@property ChunkExtensionCallback chunkExtensionCallback() const { return m_chunkExtensionCallback; }
529 	/// ditto
530 	@property void chunkExtensionCallback(ChunkExtensionCallback cb) { m_chunkExtensionCallback = cb; }
531 
532 	private void append(scope void delegate(scope ubyte[] dst) @safe del, size_t nbytes)
533 	{
534 		assert(del !is null);
535 		auto sz = nbytes;
536 		if (m_maxBufferSize > 0 && m_maxBufferSize < m_buffer.data.length + sz)
537 			sz = m_maxBufferSize - min(m_buffer.data.length, m_maxBufferSize);
538 
539 		if (sz > 0)
540 		{
541 			m_buffer.reserve(sz);
542 			() @trusted {
543 				m_buffer.append((scope ubyte[] dst) {
544 					debug assert(dst.length >= sz);
545 					del(dst[0..sz]);
546 					return sz;
547 				});
548 			} ();
549 		}
550 	}
551 
552 	size_t write(in 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 	alias write = OutputStream.write;
573 
574 	void flush()
575 	{
576 		assert(!m_finalized);
577 		auto data = m_buffer.data();
578 		if( data.length ){
579 			writeChunk(data);
580 		}
581 		m_out.flush();
582 		() @trusted { m_buffer.reset(AppenderResetMode.reuseData); } ();
583 	}
584 
585 	void finalize()
586 	{
587 		if (m_finalized) return;
588 		flush();
589 		() @trusted { m_buffer.reset(AppenderResetMode.freeData); } ();
590 		m_finalized = true;
591 		writeChunk([]);
592 		m_out.flush();
593 	}
594 
595 	private void writeChunk(in ubyte[] data)
596 	{
597 		import vibe.stream.wrapper;
598 		auto rng = streamOutputRange(m_out);
599 		formattedWrite(() @trusted { return &rng; } (), "%x", data.length);
600 		if (m_chunkExtensionCallback !is null)
601 		{
602 			rng.put(';');
603 			auto extension = m_chunkExtensionCallback(data);
604 			assert(!extension.startsWith(';'));
605 			debug assert(extension.indexOf('\r') < 0);
606 			debug assert(extension.indexOf('\n') < 0);
607 			rng.put(extension);
608 		}
609 		rng.put("\r\n");
610 		rng.put(data);
611 		rng.put("\r\n");
612 	}
613 }
614 
615 /// Creates a new `ChunkedInputStream` instance.
616 ChunkedOutputStream createChunkedOutputStream(OS)(OS destination_stream, IAllocator allocator = theAllocator()) if (isOutputStream!OS)
617 {
618 	return new ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true);
619 }
620 
621 /// Creates a new `ChunkedOutputStream` instance.
622 FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS)(OS destination_stream, IAllocator allocator = theAllocator()) if (isOutputStream!OS)
623 {
624 	return FreeListRef!ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true);
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 	if (!header_string.length)
636 		return typeof(return).init;
637 
638 	auto parts = header_string.splitter(';');
639 	auto idx = parts.front.indexOf('=');
640 	if (idx == -1)
641 		return typeof(return).init;
642 
643 	auto name = parts.front[0 .. idx].strip();
644 	dst.m_value = parts.front[name.length + 1 .. $].strip();
645 	parts.popFront();
646 
647 	if (!name.length)
648 		return typeof(return).init;
649 
650 	foreach(part; parts) {
651 		if (!part.length)
652 			continue;
653 
654 		idx = part.indexOf('=');
655 		if (idx == -1) {
656 			idx = part.length;
657 		}
658 		auto key = part[0 .. idx].strip();
659 		auto value = part[min(idx + 1, $) .. $].strip();
660 
661 		try {
662 			if (key.sicmp("httponly") == 0) {
663 				dst.m_httpOnly = true;
664 			} else if (key.sicmp("secure") == 0) {
665 				dst.m_secure = true;
666 			} else if (key.sicmp("expires") == 0) {
667 				// RFC 822 got updated by RFC 1123 (which is to be used) but is valid for this
668 				// this parsing is just for validation
669 				parseRFC822DateTimeString(value);
670 				dst.m_expires = value;
671 			} else if (key.sicmp("max-age") == 0) {
672 				if (value.length && value[0] != '-')
673 					dst.m_maxAge = value.to!long;
674 			} else if (key.sicmp("domain") == 0) {
675 				if (value.length && value[0] == '.')
676 					value = value[1 .. $]; // the leading . must be stripped (5.2.3)
677 
678 				enforce!ConvException(value.all!(a => a >= 32), "Cookie Domain must not contain any control characters");
679 				dst.m_domain = value.toLower; // must be lower (5.2.3)
680 			} else if (key.sicmp("path") == 0) {
681 				if (value.length && value[0] == '/') {
682 					enforce!ConvException(value.all!(a => a >= 32), "Cookie Path must not contain any control characters");
683 					dst.m_path = value;
684 				} else {
685 					dst.m_path = null;
686 				}
687 			} // else extension value...
688 		} catch (DateTimeException) {
689 		} catch (ConvException) {
690 		}
691 		// RFC 6265 says to ignore invalid values on all of these fields
692 	}
693 	return name;
694 }
695 
696 /// ditto
697 Tuple!(string, Cookie) parseHTTPCookie(string header_string)
698 @safe {
699 	Cookie cookie = new Cookie();
700 	auto name = parseHTTPCookie(header_string, cookie);
701 	return tuple(name, cookie);
702 }
703 
704 final class Cookie {
705 	@safe:
706 
707 	private {
708 		string m_value;
709 		string m_domain;
710 		string m_path;
711 		string m_expires;
712 		long m_maxAge;
713 		bool m_secure;
714 		bool m_httpOnly;
715 		SameSite m_sameSite;
716 	}
717 
718 	enum Encoding {
719 		url,
720 		raw,
721 		none = raw
722 	}
723 
724 	enum SameSite {
725 		default_,
726 		lax,
727 		strict,
728 	}
729 
730 	/// Cookie payload
731 	@property void value(string value) { m_value = urlEncode(value); }
732 	/// ditto
733 	@property string value() const { return urlDecode(m_value); }
734 
735 	/// Undecoded cookie payload
736 	@property void rawValue(string value) { m_value = value; }
737 	/// ditto
738 	@property string rawValue() const { return m_value; }
739 
740 	/// The domain for which the cookie is valid
741 	@property void domain(string value) { m_domain = value; }
742 	/// ditto
743 	@property string domain() const { return m_domain; }
744 
745 	/// The path/local URI for which the cookie is valid
746 	@property void path(string value) { m_path = value; }
747 	/// ditto
748 	@property string path() const { return m_path; }
749 
750 	/// Expiration date of the cookie
751 	@property void expires(string value) { m_expires = value; }
752 	/// ditto
753 	@property void expires(SysTime value) { m_expires = value.toRFC822DateTimeString(); }
754 	/// ditto
755 	@property string expires() const { return m_expires; }
756 
757 	/** Maximum life time of the cookie
758 
759 		This is the modern variant of `expires`. For backwards compatibility it
760 		is recommended to set both properties, or to use the `expire` method.
761 	*/
762 	@property void maxAge(long value) { m_maxAge = value; }
763 	/// ditto
764 	@property void maxAge(Duration value) { m_maxAge = value.total!"seconds"; }
765 	/// ditto
766 	@property long maxAge() const { return m_maxAge; }
767 
768 	/** Require a secure connection for transmission of this cookie
769 	*/
770 	@property void secure(bool value) { m_secure = value; }
771 	/// ditto
772 	@property bool secure() const { return m_secure; }
773 
774 	/** Prevents access to the cookie from scripts.
775 	*/
776 	@property void httpOnly(bool value) { m_httpOnly = value; }
777 	/// ditto
778 	@property bool httpOnly() const { return m_httpOnly; }
779 
780 	/** Prevent cross-site request forgery.
781 	*/
782 	@property void sameSite(Cookie.SameSite value) { m_sameSite = value; }
783 	/// ditto
784 	@property Cookie.SameSite sameSite() const { return m_sameSite; }
785 
786 	/** Sets the "expires" and "max-age" attributes to limit the life time of
787 		the cookie.
788 	*/
789 	void expire(Duration max_age)
790 	{
791 		this.expires = Clock.currTime(UTC()) + max_age;
792 		this.maxAge = max_age;
793 	}
794 	/// ditto
795 	void expire(SysTime expire_time)
796 	{
797 		this.expires = expire_time;
798 		this.maxAge = expire_time - Clock.currTime(UTC());
799 	}
800 
801 	/// Sets the cookie value encoded with the specified encoding.
802 	void setValue(string value, Encoding encoding)
803 	{
804 		final switch (encoding) {
805 			case Encoding.url: m_value = urlEncode(value); break;
806 			case Encoding.none: validateValue(value); m_value = value; break;
807 		}
808 	}
809 
810 	/// Writes out the full cookie in HTTP compatible format.
811 	void writeString(R)(R dst, string name)
812 		if (isOutputRange!(R, char))
813 	{
814 		import vibe.textfilter.urlencode;
815 		dst.put(name);
816 		dst.put('=');
817 		validateValue(this.value);
818 		dst.put(this.value);
819 		if (this.domain && this.domain != "") {
820 			dst.put("; Domain=");
821 			dst.put(this.domain);
822 		}
823 		if (this.path != "") {
824 			dst.put("; Path=");
825 			dst.put(this.path);
826 		}
827 		if (this.expires != "") {
828 			dst.put("; Expires=");
829 			dst.put(this.expires);
830 		}
831 		if (this.maxAge) dst.formattedWrite("; Max-Age=%s", this.maxAge);
832 		if (this.secure) dst.put("; Secure");
833 		if (this.httpOnly) dst.put("; HttpOnly");
834 		with(Cookie.SameSite)
835 		final switch(this.sameSite) {
836 			case default_: break;
837 			case lax: dst.put("; SameSite=Lax"); break;
838 			case strict: dst.put("; SameSite=Strict"); break;
839 		}
840 	}
841 
842 	private static void validateValue(string value)
843 	{
844 		enforce(!value.canFind(';') && !value.canFind('"'));
845 	}
846 }
847 
848 unittest {
849 	import std.exception : assertThrown;
850 
851 	auto c = new Cookie;
852 	c.value = "foo";
853 	assert(c.value == "foo");
854 	assert(c.rawValue == "foo");
855 
856 	c.value = "foo$";
857 	assert(c.value == "foo$");
858 	assert(c.rawValue == "foo%24", c.rawValue);
859 
860 	c.value = "foo&bar=baz?";
861 	assert(c.value == "foo&bar=baz?");
862 	assert(c.rawValue == "foo%26bar%3Dbaz%3F", c.rawValue);
863 
864 	c.setValue("foo%", Cookie.Encoding.raw);
865 	assert(c.rawValue == "foo%");
866 	assertThrown(c.value);
867 
868 	assertThrown(c.setValue("foo;bar", Cookie.Encoding.raw));
869 
870 	auto tup = parseHTTPCookie("foo=bar; HttpOnly; Secure; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=60000; Domain=foo.com; Path=/users");
871 	assert(tup[0] == "foo");
872 	assert(tup[1].value == "bar");
873 	assert(tup[1].httpOnly == true);
874 	assert(tup[1].secure == true);
875 	assert(tup[1].expires == "Wed, 09 Jun 2021 10:18:14 GMT");
876 	assert(tup[1].maxAge == 60000L);
877 	assert(tup[1].domain == "foo.com");
878 	assert(tup[1].path == "/users");
879 
880 	tup = parseHTTPCookie("SESSIONID=0123456789ABCDEF0123456789ABCDEF; Path=/site; HttpOnly");
881 	assert(tup[0] == "SESSIONID");
882 	assert(tup[1].value == "0123456789ABCDEF0123456789ABCDEF");
883 	assert(tup[1].httpOnly == true);
884 	assert(tup[1].secure == false);
885 	assert(tup[1].expires == "");
886 	assert(tup[1].maxAge == 0);
887 	assert(tup[1].domain == "");
888 	assert(tup[1].path == "/site");
889 
890 	tup = parseHTTPCookie("invalid");
891 	assert(!tup[0].length);
892 
893 	tup = parseHTTPCookie("valid=");
894 	assert(tup[0] == "valid");
895 	assert(tup[1].value == "");
896 
897 	tup = parseHTTPCookie("valid=;Path=/bar;Path=foo;Expires=14   ; Something   ; Domain=..example.org");
898 	assert(tup[0] == "valid");
899 	assert(tup[1].value == "");
900 	assert(tup[1].httpOnly == false);
901 	assert(tup[1].secure == false);
902 	assert(tup[1].expires == "");
903 	assert(tup[1].maxAge == 0);
904 	assert(tup[1].domain == ".example.org"); // spec says you must strip only the first leading dot
905 	assert(tup[1].path == "");
906 }
907 
908 
909 /**
910 */
911 struct CookieValueMap {
912 	@safe:
913 
914 	struct Cookie {
915 		/// Name of the cookie
916 		string name;
917 
918 		/// The raw cookie value as transferred over the wire
919 		string rawValue;
920 
921 		this(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url)
922 		{
923 			this.name = name;
924 			this.setValue(value, encoding);
925 		}
926 
927 		/// Treats the value as URL encoded
928 		string value() const { return urlDecode(rawValue); }
929 		/// ditto
930 		void value(string val) { rawValue = urlEncode(val); }
931 
932 		/// Sets the cookie value, applying the specified encoding.
933 		void setValue(string value, .Cookie.Encoding encoding = .Cookie.Encoding.url)
934 		{
935 			final switch (encoding) {
936 				case .Cookie.Encoding.none: this.rawValue = value; break;
937 				case .Cookie.Encoding.url: this.rawValue = urlEncode(value); break;
938 			}
939 		}
940 	}
941 
942 	private {
943 		Cookie[] m_entries;
944 	}
945 
946 	auto length(){
947 		return m_entries.length;
948 	}
949 
950 	string get(string name, string def_value = null)
951 	const {
952 		foreach (ref c; m_entries)
953 			if (c.name == name)
954 				return c.value;
955 		return def_value;
956 	}
957 
958 	string[] getAll(string name)
959 	const {
960 		string[] ret;
961 		foreach(c; m_entries)
962 			if( c.name == name )
963 				ret ~= c.value;
964 		return ret;
965 	}
966 
967 	void add(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url){
968 		m_entries ~= Cookie(name, value, encoding);
969 	}
970 
971 	void opIndexAssign(string value, string name)
972 	{
973 		m_entries ~= Cookie(name, value);
974 	}
975 
976 	string opIndex(string name)
977 	const {
978 		import core.exception : RangeError;
979 		foreach (ref c; m_entries)
980 			if (c.name == name)
981 				return c.value;
982 		throw new RangeError("Non-existent cookie: "~name);
983 	}
984 
985 	int opApply(scope int delegate(ref Cookie) @safe del)
986 	{
987 		foreach(ref c; m_entries)
988 			if( auto ret = del(c) )
989 				return ret;
990 		return 0;
991 	}
992 
993 	int opApply(scope int delegate(ref Cookie) @safe del)
994 	const {
995 		foreach(Cookie c; m_entries)
996 			if( auto ret = del(c) )
997 				return ret;
998 		return 0;
999 	}
1000 
1001 	int opApply(scope int delegate(string name, string value) @safe del)
1002 	{
1003 		foreach(ref c; m_entries)
1004 			if( auto ret = del(c.name, c.value) )
1005 				return ret;
1006 		return 0;
1007 	}
1008 
1009 	int opApply(scope int delegate(string name, string value) @safe del)
1010 	const {
1011 		foreach(Cookie c; m_entries)
1012 			if( auto ret = del(c.name, c.value) )
1013 				return ret;
1014 		return 0;
1015 	}
1016 
1017 	auto opBinaryRight(string op)(string name) if(op == "in")
1018 	{
1019 		return Ptr(&this, name);
1020 	}
1021 
1022 	auto opBinaryRight(string op)(string name) const if(op == "in")
1023 	{
1024 		return const(Ptr)(&this, name);
1025 	}
1026 
1027 	private static struct Ref {
1028 		private {
1029 			CookieValueMap* map;
1030 			string name;
1031 		}
1032 
1033 		@property string get() const { return (*map)[name]; }
1034 		void opAssign(string newval) {
1035 			foreach (ref c; *map)
1036 				if (c.name == name) {
1037 					c.value = newval;
1038 					return;
1039 				}
1040 			assert(false);
1041 		}
1042 		alias get this;
1043 	}
1044 	private static struct Ptr {
1045 		private {
1046 			CookieValueMap* map;
1047 			string name;
1048 		}
1049 		bool opCast() const {
1050 			foreach (ref c; map.m_entries)
1051 				if (c.name == name)
1052 					return true;
1053 			return false;
1054 		}
1055 		inout(Ref) opUnary(string op : "*")() inout { return inout(Ref)(map, name); }
1056 	}
1057 }
1058 
1059 unittest {
1060 	CookieValueMap m;
1061 	m["foo"] = "bar;baz%1";
1062 	assert(m["foo"] == "bar;baz%1");
1063 
1064 	m["foo"] = "bar";
1065 	assert(m.getAll("foo") == ["bar;baz%1", "bar"]);
1066 
1067 	assert("foo" in m);
1068 	if (auto val = "foo" in m) {
1069 		assert(*val == "bar;baz%1");
1070 	} else assert(false);
1071 	*("foo" in m) = "baz";
1072 	assert(m["foo"] == "baz");
1073 }