123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107 |
- /**
- 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);
- }
|