/** Common classes for HTTP clients and servers. Copyright: © 2012-2015 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig, Jan Krüger */ module vibe.http.common; public import vibe.http.status; import vibe.container.dictionarylist; import vibe.container.internal.appender; import vibe.container.internal.utilallocator; import vibe.core.log; import vibe.core.net; import vibe.inet.message; import vibe.stream.operations; import vibe.textfilter.urlencode : urlEncode, urlDecode; import vibe.internal.freelistref; import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy; import std.algorithm; import std.array; import std.conv; import std.datetime; import std.exception; import std.format; import std.range : isOutputRange; import std.string; import std.typecons; import std.uni: asLowerCase, sicmp; enum HTTPVersion { HTTP_1_0, HTTP_1_1, HTTP_2 } enum HTTPMethod { // HTTP standard, RFC 2616 GET, HEAD, PUT, POST, PATCH, DELETE, OPTIONS, TRACE, CONNECT, // WEBDAV extensions, RFC 2518 PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, // Versioning Extensions to WebDAV, RFC 3253 VERSIONCONTROL, REPORT, CHECKOUT, CHECKIN, UNCHECKOUT, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINECONTROL, MKACTIVITY, // Ordered Collections Protocol, RFC 3648 ORDERPATCH, // Access Control Protocol, RFC 3744 ACL } /** Returns the string representation of the given HttpMethod. */ string httpMethodString(HTTPMethod m) @safe nothrow { switch(m){ case HTTPMethod.BASELINECONTROL: return "BASELINE-CONTROL"; case HTTPMethod.VERSIONCONTROL: return "VERSION-CONTROL"; default: try return to!string(m); catch (Exception e) assert(false, e.msg); } } /** Returns the HttpMethod value matching the given HTTP method string. */ HTTPMethod httpMethodFromString(string str) @safe { switch(str){ default: throw new Exception("Invalid HTTP method: "~str); // HTTP standard, RFC 2616 case "GET": return HTTPMethod.GET; case "HEAD": return HTTPMethod.HEAD; case "PUT": return HTTPMethod.PUT; case "POST": return HTTPMethod.POST; case "PATCH": return HTTPMethod.PATCH; case "DELETE": return HTTPMethod.DELETE; case "OPTIONS": return HTTPMethod.OPTIONS; case "TRACE": return HTTPMethod.TRACE; case "CONNECT": return HTTPMethod.CONNECT; // WEBDAV extensions, RFC 2518 case "PROPFIND": return HTTPMethod.PROPFIND; case "PROPPATCH": return HTTPMethod.PROPPATCH; case "MKCOL": return HTTPMethod.MKCOL; case "COPY": return HTTPMethod.COPY; case "MOVE": return HTTPMethod.MOVE; case "LOCK": return HTTPMethod.LOCK; case "UNLOCK": return HTTPMethod.UNLOCK; // Versioning Extensions to WebDAV, RFC 3253 case "VERSION-CONTROL": return HTTPMethod.VERSIONCONTROL; case "REPORT": return HTTPMethod.REPORT; case "CHECKOUT": return HTTPMethod.CHECKOUT; case "CHECKIN": return HTTPMethod.CHECKIN; case "UNCHECKOUT": return HTTPMethod.UNCHECKOUT; case "MKWORKSPACE": return HTTPMethod.MKWORKSPACE; case "UPDATE": return HTTPMethod.UPDATE; case "LABEL": return HTTPMethod.LABEL; case "MERGE": return HTTPMethod.MERGE; case "BASELINE-CONTROL": return HTTPMethod.BASELINECONTROL; case "MKACTIVITY": return HTTPMethod.MKACTIVITY; // Ordered Collections Protocol, RFC 3648 case "ORDERPATCH": return HTTPMethod.ORDERPATCH; // Access Control Protocol, RFC 3744 case "ACL": return HTTPMethod.ACL; } } unittest { assert(httpMethodString(HTTPMethod.GET) == "GET"); assert(httpMethodString(HTTPMethod.UNLOCK) == "UNLOCK"); assert(httpMethodString(HTTPMethod.VERSIONCONTROL) == "VERSION-CONTROL"); assert(httpMethodString(HTTPMethod.BASELINECONTROL) == "BASELINE-CONTROL"); assert(httpMethodFromString("GET") == HTTPMethod.GET); assert(httpMethodFromString("UNLOCK") == HTTPMethod.UNLOCK); assert(httpMethodFromString("VERSION-CONTROL") == HTTPMethod.VERSIONCONTROL); } /** Utility function that throws a HTTPStatusException if the _condition is not met. */ T enforceHTTP(T)(T condition, HTTPStatus statusCode, lazy string message = null, string file = __FILE__, typeof(__LINE__) line = __LINE__) { return enforce(condition, new HTTPStatusException(statusCode, message, file, line)); } /** Utility function that throws a HTTPStatusException with status code "400 Bad Request" if the _condition is not met. */ T enforceBadRequest(T)(T condition, lazy string message = null, string file = __FILE__, typeof(__LINE__) line = __LINE__) { return enforceHTTP(condition, HTTPStatus.badRequest, message, file, line); } /** Represents an HTTP request made to a server. */ class HTTPRequest { @safe: protected { InterfaceProxy!Stream m_conn; } public { /// The HTTP protocol version used for the request HTTPVersion httpVersion = HTTPVersion.HTTP_1_1; /// The HTTP _method of the request HTTPMethod method = HTTPMethod.GET; /** The request URI Note that the request URI usually does not include the global 'http://server' part, but only the local path and a query string. A possible exception is a proxy server, which will get full URLs. */ string requestURI = "/"; /// Compatibility alias - scheduled for deprecation alias requestURL = requestURI; /// All request _headers InetHeaderMap headers; } protected this(InterfaceProxy!Stream conn) { m_conn = conn; } protected this() { } scope: public override string toString() { return httpMethodString(method) ~ " " ~ requestURL ~ " " ~ getHTTPVersionString(httpVersion); } /** Shortcut to the 'Host' header (always present for HTTP 1.1) */ @property string host() const { auto ph = "Host" in headers; return ph ? *ph : null; } /// ditto @property void host(string v) { headers["Host"] = v; } /** Returns the mime type part of the 'Content-Type' header. This function gets the pure mime type (e.g. "text/plain") without any supplimentary parameters such as "charset=...". Use contentTypeParameters to get any parameter string or headers["Content-Type"] to get the raw value. */ @property string contentType() const { auto pv = "Content-Type" in headers; if( !pv ) return null; auto idx = std.string.indexOf(*pv, ';'); return idx >= 0 ? (*pv)[0 .. idx] : *pv; } /// ditto @property void contentType(string ct) { headers["Content-Type"] = ct; } /** Returns any supplementary parameters of the 'Content-Type' header. This is a semicolon separated ist of key/value pairs. Usually, if set, this contains the character set used for text based content types. */ @property string contentTypeParameters() const { auto pv = "Content-Type" in headers; if( !pv ) return null; auto idx = std.string.indexOf(*pv, ';'); return idx >= 0 ? (*pv)[idx+1 .. $] : null; } /** Determines if the connection persists across requests. */ @property bool persistent() const { auto ph = "connection" in headers; switch(httpVersion) { case HTTPVersion.HTTP_1_0: if (ph && asLowerCase(*ph).equal("keep-alive")) return true; return false; case HTTPVersion.HTTP_1_1: if (ph && !(asLowerCase(*ph).equal("keep-alive"))) return false; return true; default: return false; } } } /** Represents the HTTP response from the server back to the client. */ class HTTPResponse { @safe: protected DictionaryList!Cookie m_cookies; public { /// The protocol version of the response - should not be changed HTTPVersion httpVersion = HTTPVersion.HTTP_1_1; /// The status code of the response, 200 by default int statusCode = HTTPStatus.ok; /** The status phrase of the response If no phrase is set, a default one corresponding to the status code will be used. */ string statusPhrase; /// The response header fields InetHeaderMap headers; /// All cookies that shall be set on the client for this request @property ref DictionaryList!Cookie cookies() return scope { return m_cookies; } } scope: public override string toString() { auto app = appender!string(); formattedWrite(app, "%s %d %s", getHTTPVersionString(this.httpVersion), this.statusCode, this.statusPhrase); return app.data; } /** Shortcut to the "Content-Type" header */ @property string contentType() const { auto pct = "Content-Type" in headers; return pct ? *pct : "application/octet-stream"; } /// ditto @property void contentType(string ct) { headers["Content-Type"] = ct; } } /** Respresents a HTTP response status. Throwing this exception from within a request handler will produce a matching error page. */ class HTTPStatusException : Exception { pure nothrow @safe @nogc: private { int m_status; } this(int status, string message = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { super(message.length ? message : httpStatusText(status), file, line, next); m_status = status; } /// The HTTP status code @property int status() const { return m_status; } string debugMessage; } final class MultiPart { string contentType; InputStream stream; //JsonValue json; string[string] form; } /** * Returns: * The version string corresponding to the `ver`, * suitable for usage in the start line of the request. */ string getHTTPVersionString(HTTPVersion ver) nothrow pure @nogc @safe { final switch(ver){ case HTTPVersion.HTTP_1_0: return "HTTP/1.0"; case HTTPVersion.HTTP_1_1: return "HTTP/1.1"; case HTTPVersion.HTTP_2: return "HTTP/2"; } } HTTPVersion parseHTTPVersion(ref string str) @safe { enforceBadRequest(str.startsWith("HTTP/1.")); str = str[7 .. $]; int minorVersion = parse!int(str); enforceBadRequest( minorVersion == 0 || minorVersion == 1 ); return minorVersion == 0 ? HTTPVersion.HTTP_1_0 : HTTPVersion.HTTP_1_1; } /** Takes an input stream that contains data in HTTP chunked format and outputs the raw data. */ final class ChunkedInputStream : InputStream { @safe: private { InterfaceProxy!InputStream m_in; ulong m_bytesInCurrentChunk = 0; } /// private this(InterfaceProxy!InputStream stream, bool dummy) { assert(!!stream); m_in = stream; readChunk(); } @property bool empty() const { return m_bytesInCurrentChunk == 0; } @property ulong leastSize() const { return m_bytesInCurrentChunk; } @property bool dataAvailableForRead() { return m_bytesInCurrentChunk > 0 && m_in.dataAvailableForRead; } const(ubyte)[] peek() { auto dt = m_in.peek(); return dt[0 .. min(dt.length, m_bytesInCurrentChunk)]; } size_t read(scope ubyte[] dst, IOMode mode) { enforceBadRequest(!empty, "Read past end of chunked stream."); size_t nbytes = 0; while (dst.length > 0) { enforceBadRequest(m_bytesInCurrentChunk > 0, "Reading past end of chunked HTTP stream."); auto sz = cast(size_t)min(m_bytesInCurrentChunk, dst.length); m_in.read(dst[0 .. sz]); dst = dst[sz .. $]; m_bytesInCurrentChunk -= sz; nbytes += sz; // FIXME: this blocks, but shouldn't for IOMode.once/immediat if( m_bytesInCurrentChunk == 0 ){ // skip current chunk footer and read next chunk ubyte[2] crlf; m_in.read(crlf); enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n'); readChunk(); } if (mode != IOMode.all) break; } return nbytes; } alias read = InputStream.read; private void readChunk() { assert(m_bytesInCurrentChunk == 0); // read chunk header logTrace("read next chunk header"); auto ln = () @trusted { return cast(string)m_in.readLine(); } (); logTrace("got chunk header: %s", ln); m_bytesInCurrentChunk = parse!ulong(ln, 16u); if( m_bytesInCurrentChunk == 0 ){ // empty chunk denotes the end // skip final chunk footer ubyte[2] crlf; m_in.read(crlf); enforceBadRequest(crlf[0] == '\r' && crlf[1] == '\n'); } } } /// Creates a new `ChunkedInputStream` instance. ChunkedInputStream chunkedInputStream(IS)(IS source_stream) if (isInputStream!IS) { return new ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); } /// Creates a new `ChunkedInputStream` instance. FreeListRef!ChunkedInputStream createChunkedInputStreamFL(IS)(IS source_stream) if (isInputStream!IS) { return () @trusted { return FreeListRef!ChunkedInputStream(interfaceProxy!InputStream(source_stream), true); } (); } /** Outputs data to an output stream in HTTP chunked format. */ final class ChunkedOutputStream : OutputStream { @safe: alias ChunkExtensionCallback = string delegate(in ubyte[] data); private { InterfaceProxy!OutputStream m_out; AllocAppender!(ubyte[]) m_buffer; size_t m_maxBufferSize = 4*1024; bool m_finalized = false; ChunkExtensionCallback m_chunkExtensionCallback = null; } /// private this(Allocator)(InterfaceProxy!OutputStream stream, Allocator alloc, bool dummy) { m_out = stream; m_buffer = AllocAppender!(ubyte[])(alloc); } /** Maximum buffer size used to buffer individual chunks. A size of zero means unlimited buffer size. Explicit flush is required in this case to empty the buffer. */ @property size_t maxBufferSize() const { return m_maxBufferSize; } /// ditto @property void maxBufferSize(size_t bytes) { m_maxBufferSize = bytes; if (m_buffer.data.length >= m_maxBufferSize) flush(); } /** A delegate used to specify the extensions for each chunk written to the underlying stream. The delegate has to be of type `string delegate(in const(ubyte)[] data)` and gets handed the data of each chunk before it is written to the underlying stream. If it's return value is non-empty, it will be added to the chunk's header line. The returned chunk extension string should be of the format `key1=value1;key2=value2;[...];keyN=valueN` and **not contain any carriage return or newline characters**. Also note that the delegate should accept the passed data through a scoped argument. Thus, **no references to the provided data should be stored in the delegate**. If the data has to be stored for later use, it needs to be copied first. */ @property ChunkExtensionCallback chunkExtensionCallback() const { return m_chunkExtensionCallback; } /// ditto @property void chunkExtensionCallback(ChunkExtensionCallback cb) { m_chunkExtensionCallback = cb; } private void append(scope void delegate(scope ubyte[] dst) @safe del, size_t nbytes) { assert(del !is null); auto sz = nbytes; if (m_maxBufferSize > 0 && m_maxBufferSize < m_buffer.data.length + sz) sz = m_maxBufferSize - min(m_buffer.data.length, m_maxBufferSize); if (sz > 0) { m_buffer.reserve(sz); () @trusted { m_buffer.append((scope ubyte[] dst) { debug assert(dst.length >= sz); del(dst[0..sz]); return sz; }); } (); } } static if (is(typeof(.OutputStream.outputStreamVersion)) && .OutputStream.outputStreamVersion > 1) { override size_t write(scope const(ubyte)[] bytes_, IOMode mode) { return doWrite(bytes_, mode); } } else { override size_t write(in ubyte[] bytes_, IOMode mode) { return doWrite(bytes_, mode); } } alias write = OutputStream.write; private size_t doWrite(scope const(ubyte)[] bytes_, IOMode mode) { assert(!m_finalized); const(ubyte)[] bytes = bytes_; size_t nbytes = 0; while (bytes.length > 0) { append((scope ubyte[] dst) { auto n = dst.length; dst[] = bytes[0..n]; bytes = bytes[n..$]; nbytes += n; }, bytes.length); if (mode == IOMode.immediate) break; if (mode == IOMode.once && nbytes > 0) break; if (bytes.length > 0) flush(); } return nbytes; } void flush() { assert(!m_finalized); auto data = m_buffer.data(); if( data.length ){ writeChunk(data); } m_out.flush(); () @trusted { m_buffer.reset(AppenderResetMode.reuseData); } (); } void finalize() { if (m_finalized) return; flush(); () @trusted { m_buffer.reset(AppenderResetMode.freeData); } (); m_finalized = true; writeChunk([]); m_out.flush(); } private void writeChunk(in ubyte[] data) { import vibe.stream.wrapper; auto rng = streamOutputRange(m_out); formattedWrite(() @trusted { return &rng; } (), "%x", data.length); if (m_chunkExtensionCallback !is null) { rng.put(';'); auto extension = m_chunkExtensionCallback(data); assert(!extension.startsWith(';')); debug assert(extension.indexOf('\r') < 0); debug assert(extension.indexOf('\n') < 0); rng.put(extension); } rng.put("\r\n"); rng.put(data); rng.put("\r\n"); } } /// Creates a new `ChunkedInputStream` instance. ChunkedOutputStream createChunkedOutputStream(OS)(OS destination_stream) if (isOutputStream!OS) { return createChunkedOutputStream(destination_stream, theAllocator()); } /// ditto ChunkedOutputStream createChunkedOutputStream(OS, Allocator)(OS destination_stream, Allocator allocator) if (isOutputStream!OS) { return new ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true); } /// Creates a new `ChunkedOutputStream` instance. FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS)(OS destination_stream) if (isOutputStream!OS) { return createChunkedOutputStreamFL(destination_stream, theAllocator()); } /// ditto FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS, Allocator)(OS destination_stream, Allocator allocator) if (isOutputStream!OS) { return FreeListRef!ChunkedOutputStream(interfaceProxy!OutputStream(destination_stream), allocator, true); } /// Parses the cookie from a header field, returning the name of the cookie. /// Implements an algorithm equivalent to https://tools.ietf.org/html/rfc6265#section-5.2 /// Returns: the cookie name as return value, populates the dst argument or allocates on the GC for the tuple overload. string parseHTTPCookie(string header_string, scope Cookie dst) @safe in { assert(dst !is null); } do { if (!header_string.length) return typeof(return).init; auto parts = header_string.splitter(';'); auto idx = parts.front.indexOf('='); if (idx == -1) return typeof(return).init; auto name = parts.front[0 .. idx].strip(); dst.m_value = parts.front[name.length + 1 .. $].strip(); parts.popFront(); if (!name.length) return typeof(return).init; foreach(part; parts) { if (!part.length) continue; idx = part.indexOf('='); if (idx == -1) { idx = part.length; } auto key = part[0 .. idx].strip(); auto value = part[min(idx + 1, $) .. $].strip(); try { if (key.sicmp("httponly") == 0) { dst.m_httpOnly = true; } else if (key.sicmp("secure") == 0) { dst.m_secure = true; } else if (key.sicmp("expires") == 0) { // RFC 822 got updated by RFC 1123 (which is to be used) but is valid for this // this parsing is just for validation parseRFC822DateTimeString(value); dst.m_expires = value; } else if (key.sicmp("max-age") == 0) { if (value.length && value[0] != '-') dst.m_maxAge = value.to!long; } else if (key.sicmp("domain") == 0) { if (value.length && value[0] == '.') value = value[1 .. $]; // the leading . must be stripped (5.2.3) enforce!ConvException(value.all!(a => a >= 32), "Cookie Domain must not contain any control characters"); dst.m_domain = value.toLower; // must be lower (5.2.3) } else if (key.sicmp("path") == 0) { if (value.length && value[0] == '/') { enforce!ConvException(value.all!(a => a >= 32), "Cookie Path must not contain any control characters"); dst.m_path = value; } else { dst.m_path = null; } } // else extension value... } catch (DateTimeException) { } catch (ConvException) { } // RFC 6265 says to ignore invalid values on all of these fields } return name; } /// ditto Tuple!(string, Cookie) parseHTTPCookie(string header_string) @safe { Cookie cookie = new Cookie(); auto name = parseHTTPCookie(header_string, cookie); return tuple(name, cookie); } final class Cookie { @safe: private { string m_value; string m_domain; string m_path; string m_expires; long m_maxAge; bool m_secure; bool m_httpOnly; SameSite m_sameSite; } enum Encoding { url, raw, none = raw } enum SameSite { default_, lax, none, strict, } /// Cookie payload @property void value(string value) { m_value = urlEncode(value); } /// ditto @property string value() const { return urlDecode(m_value); } /// Undecoded cookie payload @property void rawValue(string value) { m_value = value; } /// ditto @property string rawValue() const { return m_value; } /// The domain for which the cookie is valid @property void domain(string value) { m_domain = value; } /// ditto @property string domain() const { return m_domain; } /// The path/local URI for which the cookie is valid @property void path(string value) { m_path = value; } /// ditto @property string path() const { return m_path; } /// Expiration date of the cookie @property void expires(string value) { m_expires = value; } /// ditto @property void expires(SysTime value) { m_expires = value.toRFC822DateTimeString(); } /// ditto @property string expires() const { return m_expires; } /** Maximum life time of the cookie This is the modern variant of `expires`. For backwards compatibility it is recommended to set both properties, or to use the `expire` method. */ @property void maxAge(long value) { m_maxAge = value; } /// ditto @property void maxAge(Duration value) { m_maxAge = value.total!"seconds"; } /// ditto @property long maxAge() const { return m_maxAge; } /** Require a secure connection for transmission of this cookie */ @property void secure(bool value) { m_secure = value; } /// ditto @property bool secure() const { return m_secure; } /** Prevents access to the cookie from scripts. */ @property void httpOnly(bool value) { m_httpOnly = value; } /// ditto @property bool httpOnly() const { return m_httpOnly; } /** Prevent cross-site request forgery. */ @property void sameSite(Cookie.SameSite value) { m_sameSite = value; } /// ditto @property Cookie.SameSite sameSite() const { return m_sameSite; } /** Sets the "expires" and "max-age" attributes to limit the life time of the cookie. */ void expire(Duration max_age) { this.expires = Clock.currTime(UTC()) + max_age; this.maxAge = max_age; } /// ditto void expire(SysTime expire_time) { this.expires = expire_time; this.maxAge = expire_time - Clock.currTime(UTC()); } /// Sets the cookie value encoded with the specified encoding. void setValue(string value, Encoding encoding) { final switch (encoding) { case Encoding.url: m_value = urlEncode(value); break; case Encoding.none: validateValue(value); m_value = value; break; } } /// Writes out the full cookie in HTTP compatible format. void writeString(R)(R dst, string name) if (isOutputRange!(R, char)) { import vibe.textfilter.urlencode; dst.put(name); dst.put('='); validateValue(this.value); dst.put(this.value); if (this.domain && this.domain != "") { dst.put("; Domain="); dst.put(this.domain); } if (this.path != "") { dst.put("; Path="); dst.put(this.path); } if (this.expires != "") { dst.put("; Expires="); dst.put(this.expires); } if (this.maxAge) dst.formattedWrite("; Max-Age=%s", this.maxAge); if (this.secure) dst.put("; Secure"); if (this.httpOnly) dst.put("; HttpOnly"); with(Cookie.SameSite) final switch(this.sameSite) { case default_: break; case lax: dst.put("; SameSite=Lax"); break; case strict: dst.put("; SameSite=Strict"); break; case none: dst.put("; SameSite=None"); break; } } private static void validateValue(string value) { enforce(!value.canFind(';') && !value.canFind('"')); } } unittest { import std.exception : assertThrown; auto c = new Cookie; c.value = "foo"; assert(c.value == "foo"); assert(c.rawValue == "foo"); c.value = "foo$"; assert(c.value == "foo$"); assert(c.rawValue == "foo%24", c.rawValue); c.value = "foo&bar=baz?"; assert(c.value == "foo&bar=baz?"); assert(c.rawValue == "foo%26bar%3Dbaz%3F", c.rawValue); c.setValue("foo%", Cookie.Encoding.raw); assert(c.rawValue == "foo%"); assertThrown(c.value); assertThrown(c.setValue("foo;bar", Cookie.Encoding.raw)); auto tup = parseHTTPCookie("foo=bar; HttpOnly; Secure; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Max-Age=60000; Domain=foo.com; Path=/users"); assert(tup[0] == "foo"); assert(tup[1].value == "bar"); assert(tup[1].httpOnly == true); assert(tup[1].secure == true); assert(tup[1].expires == "Wed, 09 Jun 2021 10:18:14 GMT"); assert(tup[1].maxAge == 60000L); assert(tup[1].domain == "foo.com"); assert(tup[1].path == "/users"); tup = parseHTTPCookie("SESSIONID=0123456789ABCDEF0123456789ABCDEF; Path=/site; HttpOnly"); assert(tup[0] == "SESSIONID"); assert(tup[1].value == "0123456789ABCDEF0123456789ABCDEF"); assert(tup[1].httpOnly == true); assert(tup[1].secure == false); assert(tup[1].expires == ""); assert(tup[1].maxAge == 0); assert(tup[1].domain == ""); assert(tup[1].path == "/site"); tup = parseHTTPCookie("invalid"); assert(!tup[0].length); tup = parseHTTPCookie("valid="); assert(tup[0] == "valid"); assert(tup[1].value == ""); tup = parseHTTPCookie("valid=;Path=/bar;Path=foo;Expires=14 ; Something ; Domain=..example.org"); assert(tup[0] == "valid"); assert(tup[1].value == ""); assert(tup[1].httpOnly == false); assert(tup[1].secure == false); assert(tup[1].expires == ""); assert(tup[1].maxAge == 0); assert(tup[1].domain == ".example.org"); // spec says you must strip only the first leading dot assert(tup[1].path == ""); } /** */ struct CookieValueMap { @safe: struct Cookie { /// Name of the cookie string name; /// The raw cookie value as transferred over the wire string rawValue; this(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url) { this.name = name; this.setValue(value, encoding); } /// Treats the value as URL encoded string value() const { return urlDecode(rawValue); } /// ditto void value(string val) { rawValue = urlEncode(val); } /// Sets the cookie value, applying the specified encoding. void setValue(string value, .Cookie.Encoding encoding = .Cookie.Encoding.url) { final switch (encoding) { case .Cookie.Encoding.none: this.rawValue = value; break; case .Cookie.Encoding.url: this.rawValue = urlEncode(value); break; } } } private { Cookie[] m_entries; } auto length(){ return m_entries.length; } string get(string name, string def_value = null) const { foreach (ref c; m_entries) if (c.name == name) return c.value; return def_value; } string[] getAll(string name) const { string[] ret; foreach(c; m_entries) if( c.name == name ) ret ~= c.value; return ret; } void add(string name, string value, .Cookie.Encoding encoding = .Cookie.Encoding.url){ m_entries ~= Cookie(name, value, encoding); } void opIndexAssign(string value, string name) { m_entries ~= Cookie(name, value); } string opIndex(string name) const { import core.exception : RangeError; foreach (ref c; m_entries) if (c.name == name) return c.value; throw new RangeError("Non-existent cookie: "~name); } int opApply(scope int delegate(ref Cookie) @safe del) { foreach(ref c; m_entries) if( auto ret = del(c) ) return ret; return 0; } int opApply(scope int delegate(ref Cookie) @safe del) const { foreach(Cookie c; m_entries) if( auto ret = del(c) ) return ret; return 0; } int opApply(scope int delegate(string name, string value) @safe del) { foreach(ref c; m_entries) if( auto ret = del(c.name, c.value) ) return ret; return 0; } int opApply(scope int delegate(string name, string value) @safe del) const { foreach(Cookie c; m_entries) if( auto ret = del(c.name, c.value) ) return ret; return 0; } auto opBinaryRight(string op)(string name) if(op == "in") { return Ptr(&this, name); } auto opBinaryRight(string op)(string name) const if(op == "in") { return const(Ptr)(&this, name); } private static struct Ref { private { CookieValueMap* map; string name; } @property string get() const { return (*map)[name]; } void opAssign(string newval) { foreach (ref c; *map) if (c.name == name) { c.value = newval; return; } assert(false); } alias get this; } private static struct Ptr { private { CookieValueMap* map; string name; } bool opCast() const { foreach (ref c; map.m_entries) if (c.name == name) return true; return false; } inout(Ref) opUnary(string op : "*")() inout { return inout(Ref)(map, name); } } } unittest { CookieValueMap m; m["foo"] = "bar;baz%1"; assert(m["foo"] == "bar;baz%1"); m["foo"] = "bar"; assert(m.getAll("foo") == ["bar;baz%1", "bar"]); assert("foo" in m); if (auto val = "foo" in m) { assert(*val == "bar;baz%1"); } else assert(false); *("foo" in m) = "baz"; assert(m["foo"] == "baz"); } package auto createRequestAllocator() { import vibe.container.internal.utilallocator: RegionListAllocator; static if (is(RegionListAllocator!(shared(GCAllocator), true) == struct)) { version (VibeManualMemoryManagement) return allocatorObject(RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance)); else return allocatorObject(RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance)); } else { version (VibeManualMemoryManagement) return new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance); else return new RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance); } } package void freeRequestAllocator(Allocator)(ref Allocator alloc) { destroy(alloc); }