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