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