common.d 29 KB

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