1 /**
2 A simple HTTP/1.1 client implementation.
3
4 Copyright: © 2012-2014 Sönke Ludwig
5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 Authors: Sönke Ludwig, Jan Krüger
7 */
8 module vibe.http.client;
9
10 public import vibe.core.net;
11 public import vibe.http.common;
12 public import vibe.inet.url;
13
14 import vibe.core.connectionpool;
15 import vibe.core.core;
16 import vibe.core.log;
17 import vibe.data.json;
18 import vibe.inet.message;
19 import vibe.inet.url;
20 import vibe.stream.counting;
21 import vibe.stream.tls;
22 import vibe.stream.operations;
23 import vibe.stream.wrapper : createConnectionProxyStream;
24 import vibe.stream.zlib;
25 import vibe.utils.array;
26 import vibe.utils.dictionarylist;
27 import vibe.internal.allocator;
28 import vibe.internal.freelistref;
29 import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy;
30
31 import core.exception : AssertError;
32 import std.algorithm : splitter;
33 import std.array;
34 import std.conv;
35 import std.encoding : sanitize;
36 import std.exception;
37 import std.format;
38 import std.string;
39 import std.typecons;
40 import std.datetime;
41 import std.socket : AddressFamily;
42
43 version(Posix)
44 {
45 version = UnixSocket;
46 }
47
48
49 /**************************************************************************************************/
50 /* Public functions */
51 /**************************************************************************************************/
52 @safe:
53
54 /**
55 Performs a synchronous HTTP request on the specified URL.
56
57 The requester parameter allows to customize the request and to specify the request body for
58 non-GET requests before it is sent. A response object is then returned or passed to the
59 responder callback synchronously.
60
61 This function is a low-level HTTP client facility. It will not perform automatic redirect,
62 caching or similar tasks. For a high-level download facility (similar to cURL), see the
63 `vibe.inet.urltransfer` module.
64
65 Note that it is highly recommended to use one of the overloads that take a responder callback,
66 as they can avoid some memory allocations and are safe against accidentally leaving stale
67 response objects (objects whose response body wasn't fully read). For the returning overloads
68 of the function it is recommended to put a `scope(exit)` right after the call in which
69 `HTTPClientResponse.dropBody` is called to avoid this.
70
71 See_also: `vibe.inet.urltransfer.download`
72 */
73 HTTPClientResponse requestHTTP(string url, scope void delegate(scope HTTPClientRequest req) requester = null, const(HTTPClientSettings) settings = defaultSettings)
74 {
75 return requestHTTP(URL.parse(url), requester, settings);
76 }
77 /// ditto
78 HTTPClientResponse requestHTTP(URL url, scope void delegate(scope HTTPClientRequest req) requester = null, const(HTTPClientSettings) settings = defaultSettings)
79 {
80 auto cli = connectHTTP(url, settings);
81 auto res = cli.request(
82 (scope req){ httpRequesterDg(req, url, settings, requester); },
83 );
84
85 // make sure the connection stays locked if the body still needs to be read
86 if( res.m_client ) res.lockedConnection = cli;
87
88 logTrace("Returning HTTPClientResponse for conn %s", () @trusted { return cast(void*)res.lockedConnection.__conn; } ());
89 return res;
90 }
91 /// ditto
92 void requestHTTP(string url, scope void delegate(scope HTTPClientRequest req) requester, scope void delegate(scope HTTPClientResponse req) responder, const(HTTPClientSettings) settings = defaultSettings)
93 {
94 requestHTTP(URL(url), requester, responder, settings);
95 }
96 /// ditto
97 void requestHTTP(URL url, scope void delegate(scope HTTPClientRequest req) requester, scope void delegate(scope HTTPClientResponse req) responder, const(HTTPClientSettings) settings = defaultSettings)
98 {
99 auto cli = connectHTTP(url, settings);
100 cli.request(
101 (scope req){ httpRequesterDg(req, url, settings, requester); },
102 responder
103 );
104 assert(!cli.m_requesting, "HTTP client still requesting after return!?");
105 assert(!cli.m_responding, "HTTP client still responding after return!?");
106 }
107
108 private bool isTLSRequired(in URL url, in HTTPClientSettings settings)
109 {
110 version(UnixSocket) {
111 enforce(url.schema == "http" || url.schema == "https" || url.schema == "http+unix" || url.schema == "https+unix", "URL schema must be http(s) or http(s)+unix.");
112 } else {
113 enforce(url.schema == "http" || url.schema == "https", "URL schema must be http(s).");
114 }
115 enforce(url.host.length > 0, "URL must contain a host name.");
116 bool use_tls;
117
118 if (settings.proxyURL.schema !is null)
119 use_tls = settings.proxyURL.schema == "https";
120 else
121 {
122 version(UnixSocket)
123 use_tls = url.schema == "https" || url.schema == "https+unix";
124 else
125 use_tls = url.schema == "https";
126 }
127
128 return use_tls;
129 }
130
131 private void httpRequesterDg(scope HTTPClientRequest req, in URL url, in HTTPClientSettings settings, scope void delegate(scope HTTPClientRequest req) requester)
132 {
133 import std.algorithm.searching : canFind;
134 import vibe.http.internal.basic_auth_client: addBasicAuth;
135
136 if (url.localURI.length) {
137 assert(url.path.absolute, "Request URL path must be absolute.");
138 req.requestURL = url.localURI;
139 }
140
141 if (settings.proxyURL.schema !is null)
142 req.requestURL = url.toString(); // proxy exception to the URL representation
143
144 // IPv6 addresses need to be put into brackets
145 auto hoststr = url.host.canFind(':') ? "["~url.host~"]" : url.host;
146
147 // Provide port number when it is not the default one (RFC2616 section 14.23)
148 if (url.port && url.port != url.defaultPort)
149 req.headers["Host"] = format("%s:%d", hoststr, url.port);
150 else
151 req.headers["Host"] = hoststr;
152
153 if ("authorization" !in req.headers && url.username != "")
154 req.addBasicAuth(url.username, url.password);
155
156 if (requester) () @trusted { requester(req); } ();
157 }
158
159 /** Posts a simple JSON request. Note that the server www.example.org does not
160 exists, so there will be no meaningful result.
161 */
162 unittest {
163 import vibe.core.log;
164 import vibe.http.client;
165 import vibe.stream.operations;
166
167 void test()
168 {
169 requestHTTP("http://www.example.org/",
170 (scope req) {
171 req.method = HTTPMethod.POST;
172 //req.writeJsonBody(["name": "My Name"]);
173 },
174 (scope res) {
175 logInfo("Response: %s", res.bodyReader.readAllUTF8());
176 }
177 );
178 }
179 }
180
181
182 /**
183 Returns a HTTPClient proxy object that is connected to the specified host.
184
185 Internally, a connection pool is used to reuse already existing connections. Note that
186 usually requestHTTP should be used for making requests instead of manually using a
187 HTTPClient to do so.
188 */
189 auto connectHTTP(string host, ushort port = 0, bool use_tls = false, const(HTTPClientSettings) settings = null)
190 {
191 auto sttngs = settings ? settings : defaultSettings;
192
193 if (port == 0) port = use_tls ? 443 : 80;
194 auto ckey = ConnInfo(host, sttngs.tlsPeerName, port, use_tls, sttngs.proxyURL.host, sttngs.proxyURL.port, sttngs.networkInterface);
195
196 ConnectionPool!HTTPClient pool;
197 s_connections.opApply((ref c) @safe {
198 if (c[0] == ckey)
199 pool = c[1];
200 return 0;
201 });
202
203 if (!pool) {
204 logDebug("Create HTTP client pool %s(%s):%s %s proxy %s:%d", host, sttngs.tlsPeerName, port, use_tls, sttngs.proxyURL.host, sttngs.proxyURL.port);
205 pool = new ConnectionPool!HTTPClient({
206 auto ret = new HTTPClient;
207 ret.connect(host, port, use_tls, sttngs);
208 return ret;
209 });
210 if (s_connections.full) s_connections.popFront();
211 s_connections.put(tuple(ckey, pool));
212 }
213
214 return pool.lockConnection();
215 }
216
217 /// Ditto
218 auto connectHTTP(URL url, const(HTTPClientSettings) settings = null)
219 {
220 const use_tls = isTLSRequired(url, settings);
221 return connectHTTP(url.getFilteredHost, url.port, use_tls, settings);
222 }
223
224 static ~this()
225 {
226 foreach (ci; s_connections) {
227 ci[1].removeUnused((conn) {
228 conn.disconnect();
229 });
230 }
231 }
232
233 private struct ConnInfo { string host; string tlsPeerName; ushort port; bool useTLS; string proxyIP; ushort proxyPort; NetworkAddress bind_addr; }
234 private static vibe.utils.array.FixedRingBuffer!(Tuple!(ConnInfo, ConnectionPool!HTTPClient), 16) s_connections;
235
236
237 /**************************************************************************************************/
238 /* Public types */
239 /**************************************************************************************************/
240
241 /**
242 Defines an HTTP/HTTPS proxy request or a connection timeout for an HTTPClient.
243 */
244 class HTTPClientSettings {
245 URL proxyURL;
246 Duration defaultKeepAliveTimeout = 10.seconds;
247
248 /** Timeout for establishing a connection to the server
249
250 Note that this setting is only supported when using the vibe-core
251 module. If using one of the legacy drivers, any value other than
252 `Duration.max` will emit a runtime warning and connects without a
253 specific timeout.
254 */
255 Duration connectTimeout = Duration.max;
256
257 /// Timeout during read operations on the underyling transport
258 Duration readTimeout = Duration.max;
259
260 /// Forces a specific network interface to use for outgoing connections.
261 NetworkAddress networkInterface = anyAddress;
262
263 /// Can be used to force looking up IPv4/IPv6 addresses for host names.
264 AddressFamily dnsAddressFamily = AddressFamily.UNSPEC;
265
266 /** Allows to customize the TLS context before connecting to a server.
267
268 Note that this overrides a callback set with `HTTPClient.setTLSContextSetup`.
269 */
270 void delegate(TLSContext ctx) @safe nothrow tlsContextSetup;
271
272 /**
273 TLS Peer name override.
274
275 Allows to customize the tls peer name sent to server during the TLS connection setup (SNI)
276 */
277 string tlsPeerName;
278
279 @property HTTPClientSettings dup()
280 const @safe {
281 auto ret = new HTTPClientSettings;
282 ret.proxyURL = this.proxyURL;
283 ret.connectTimeout = this.connectTimeout;
284 ret.readTimeout = this.readTimeout;
285 ret.networkInterface = this.networkInterface;
286 ret.dnsAddressFamily = this.dnsAddressFamily;
287 ret.tlsContextSetup = this.tlsContextSetup;
288 ret.tlsPeerName = this.tlsPeerName;
289 return ret;
290 }
291 }
292
293 ///
294 unittest {
295 void test() {
296
297 HTTPClientSettings settings = new HTTPClientSettings;
298 settings.proxyURL = URL.parse("http://proxyuser:proxypass@192.168.2.50:3128");
299 settings.defaultKeepAliveTimeout = 0.seconds; // closes connection immediately after receiving the data.
300 requestHTTP("http://www.example.org",
301 (scope req){
302 req.method = HTTPMethod.GET;
303 },
304 (scope res){
305 logInfo("Headers:");
306 foreach (key, ref value; res.headers.byKeyValue) {
307 logInfo("%s: %s", key, value);
308 }
309 logInfo("Response: %s", res.bodyReader.readAllUTF8());
310 }, settings);
311
312 }
313 }
314
315 version (Have_vibe_core)
316 unittest { // test connect timeout
317 import std.conv : to;
318 import vibe.core.stream : pipe, nullSink;
319
320 HTTPClientSettings settings = new HTTPClientSettings;
321 settings.connectTimeout = 50.msecs;
322
323 // Use an IP address that is guaranteed to be unassigned globally to force
324 // a timeout (see RFC 3330)
325 auto cli = connectHTTP("192.0.2.0", 80, false, settings);
326 auto timer = setTimer(500.msecs, { assert(false, "Connect timeout occurred too late"); });
327 scope (exit) timer.stop();
328
329 try {
330 cli.request(
331 (scope req) { assert(false, "Expected no connection"); },
332 (scope res) { assert(false, "Expected no response"); }
333 );
334 assert(false, "Response read expected to fail due to timeout");
335 } catch(Exception e) {}
336 }
337
338 unittest { // test read timeout
339 import std.conv : to;
340 import vibe.core.stream : pipe, nullSink;
341
342 version (VibeLibasyncDriver) {
343 logInfo("Skipping HTTP client read timeout test due to buggy libasync driver.");
344 } else {
345 HTTPClientSettings settings = new HTTPClientSettings;
346 settings.readTimeout = 50.msecs;
347
348 auto l = listenTCP(0, (conn) {
349 try conn.pipe(nullSink);
350 catch (Exception e) assert(false, e.msg);
351 conn.close();
352 }, "127.0.0.1");
353
354 auto cli = connectHTTP("127.0.0.1", l.bindAddress.port, false, settings);
355 auto timer = setTimer(500.msecs, { assert(false, "Read timeout occurred too late"); });
356 scope (exit) {
357 timer.stop();
358 l.stopListening();
359 cli.disconnect();
360 sleep(10.msecs); // allow the read connection end to fully close
361 }
362
363 try {
364 cli.request(
365 (scope req) { req.method = HTTPMethod.GET; },
366 (scope res) { assert(false, "Expected no response"); }
367 );
368 assert(false, "Response read expected to fail due to timeout");
369 } catch(Exception e) {}
370 }
371 }
372
373
374 /**
375 Implementation of a HTTP 1.0/1.1 client with keep-alive support.
376
377 Note that it is usually recommended to use requestHTTP for making requests as that will use a
378 pool of HTTPClient instances to keep the number of connection establishments low while not
379 blocking requests from different tasks.
380 */
381 final class HTTPClient {
382 @safe:
383
384 enum maxHeaderLineLength = 4096;
385
386 private {
387 Rebindable!(const(HTTPClientSettings)) m_settings;
388 string m_server;
389 string m_tlsPeerName;
390 ushort m_port;
391 bool m_useTLS;
392 TCPConnection m_conn;
393 InterfaceProxy!Stream m_stream;
394 TLSStream m_tlsStream;
395 TLSContext m_tls;
396 static __gshared m_userAgent = "vibe.d/"~vibeVersionString~" (HTTPClient, +http://vibed.org/)";
397 static __gshared void function(TLSContext) ms_tlsSetup;
398 bool m_requesting = false, m_responding = false;
399 SysTime m_keepAliveLimit;
400 Duration m_keepAliveTimeout;
401 }
402
403 /** Get the current settings for the HTTP client. **/
404 @property const(HTTPClientSettings) settings() const {
405 return m_settings;
406 }
407
408 /**
409 Sets the default user agent string for new HTTP requests.
410 */
411 static void setUserAgentString(string str) @trusted { m_userAgent = str; }
412
413 /**
414 Sets a callback that will be called for every TLS context that is created.
415
416 Setting such a callback is useful for adjusting the validation parameters
417 of the TLS context.
418 */
419 static void setTLSSetupCallback(void function(TLSContext) @safe func) @trusted { ms_tlsSetup = func; }
420
421 /**
422 Sets up this HTTPClient to connect to a specific server.
423
424 This method may only be called if any previous connection has been closed.
425
426 The actual connection is deferred until a request is initiated (using `HTTPClient.request`).
427 */
428 void connect(string server, ushort port = 80, bool use_tls = false, const(HTTPClientSettings) settings = defaultSettings)
429 {
430 assert(!m_conn);
431 assert(port != 0);
432 disconnect();
433 m_conn = TCPConnection.init;
434 m_settings = settings;
435 m_keepAliveTimeout = settings.defaultKeepAliveTimeout;
436 m_keepAliveLimit = Clock.currTime(UTC()) + m_keepAliveTimeout;
437 m_server = server;
438 m_tlsPeerName = settings.tlsPeerName.length ? settings.tlsPeerName : server;
439 m_port = port;
440 m_useTLS = use_tls;
441 if (use_tls) {
442 m_tls = createTLSContext(TLSContextKind.client);
443 // this will be changed to trustedCert once a proper root CA store is available by default
444 m_tls.peerValidationMode = TLSPeerValidationMode.none;
445 if (settings.tlsContextSetup) settings.tlsContextSetup(m_tls);
446 else () @trusted { if (ms_tlsSetup) ms_tlsSetup(m_tls); } ();
447 }
448 }
449
450 /**
451 Forcefully closes the TCP connection.
452
453 Before calling this method, be sure that no request is currently being processed.
454 */
455 void disconnect()
456 nothrow {
457 if (m_conn) {
458 version (Have_vibe_core) {}
459 else scope(failure) assert(false);
460
461 if (m_conn.connected) {
462 try m_stream.finalize();
463 catch (Exception e) logDebug("Failed to finalize connection stream when closing HTTP client connection: %s", e.msg);
464 m_conn.close();
465 }
466
467 if (m_useTLS) () @trusted { return destroy(m_stream); } ();
468 m_stream = InterfaceProxy!Stream.init;
469 () @trusted { return destroy(m_conn); } ();
470 m_conn = TCPConnection.init;
471 }
472 }
473
474 private void doProxyRequest(T, U)(ref T res, U requester, ref bool close_conn, ref bool has_body)
475 @trusted { // scope new
476 import std.conv : to;
477 import vibe.internal.utilallocator: RegionListAllocator;
478 version (VibeManualMemoryManagement)
479 scope request_allocator = new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance);
480 else
481 scope request_allocator = new RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance);
482
483 res.dropBody();
484 scope(failure)
485 res.disconnect();
486 if (res.statusCode != 407) {
487 throw new HTTPStatusException(HTTPStatus.internalServerError, "Proxy returned Proxy-Authenticate without a 407 status code.");
488 }
489
490 // send the request again with the proxy authentication information if available
491 if (m_settings.proxyURL.username is null) {
492 throw new HTTPStatusException(HTTPStatus.proxyAuthenticationRequired, "Proxy Authentication Required.");
493 }
494
495 m_responding = false;
496 close_conn = false;
497 bool found_proxy_auth;
498
499 foreach (string proxyAuth; res.headers.getAll("Proxy-Authenticate"))
500 {
501 if (proxyAuth.length >= "Basic".length && proxyAuth[0.."Basic".length] == "Basic")
502 {
503 found_proxy_auth = true;
504 break;
505 }
506 }
507
508 if (!found_proxy_auth)
509 {
510 throw new HTTPStatusException(HTTPStatus.notAcceptable, "The Proxy Server didn't allow Basic Authentication");
511 }
512
513 SysTime connected_time;
514 has_body = doRequestWithRetry(requester, true, close_conn, connected_time);
515 m_responding = true;
516
517 static if (is(T == HTTPClientResponse))
518 res = new HTTPClientResponse(this, has_body, close_conn, request_allocator, connected_time);
519 else
520 res = scoped!HTTPClientResponse(this, has_body, close_conn, request_allocator, connected_time);
521
522 if (res.headers.get("Proxy-Authenticate", null) !is null){
523 res.dropBody();
524 throw new HTTPStatusException(HTTPStatus.proxyAuthenticationRequired, "Proxy Authentication Failed.");
525 }
526
527 }
528
529 /**
530 Performs a HTTP request.
531
532 `requester` is called first to populate the request with headers and the desired
533 HTTP method and version. After a response has been received it is then passed
534 to the caller which can in turn read the reponse body. Any part of the body
535 that has not been processed will automatically be consumed and dropped.
536
537 Note that the `requester` callback might be invoked multiple times in the event
538 that a request has to be resent due to a connection failure.
539
540 Also note that the second form of this method (returning a `HTTPClientResponse`) is
541 not recommended to use as it may accidentially block a HTTP connection when
542 only part of the response body was read and also requires a heap allocation
543 for the response object. The callback based version on the other hand uses
544 a stack allocation and guarantees that the request has been fully processed
545 once it has returned.
546 */
547 void request(scope void delegate(scope HTTPClientRequest req) requester, scope void delegate(scope HTTPClientResponse) responder)
548 @trusted { // scope new
549 import vibe.internal.utilallocator: RegionListAllocator;
550 version (VibeManualMemoryManagement)
551 scope request_allocator = new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance);
552 else
553 scope request_allocator = new RegionListAllocator!(shared(GCAllocator), true)(1024, GCAllocator.instance);
554
555 scope (failure) {
556 m_responding = false;
557 disconnect();
558 }
559
560 bool close_conn;
561 SysTime connected_time;
562 bool has_body = doRequestWithRetry(requester, false, close_conn, connected_time);
563
564 m_responding = true;
565 auto res = scoped!HTTPClientResponse(this, has_body, close_conn, request_allocator, connected_time);
566
567 // proxy implementation
568 if (res.headers.get("Proxy-Authenticate", null) !is null) {
569 doProxyRequest(res, requester, close_conn, has_body);
570 }
571
572 Exception user_exception;
573 while (true)
574 {
575 try responder(res);
576 catch (Exception e) {
577 logDebug("Error while handling response: %s", e.toString().sanitize());
578 user_exception = e;
579 }
580 if (res.statusCode < 200) {
581 // just an informational status -> read and handle next response
582 if (m_responding) res.dropBody();
583 if (m_conn) {
584 res = scoped!HTTPClientResponse(this, has_body, close_conn, request_allocator, connected_time);
585 continue;
586 }
587 }
588 if (m_responding) {
589 logDebug("Failed to handle the complete response of the server - disconnecting.");
590 res.disconnect();
591 }
592 assert(!m_responding, "Still in responding state after finalizing the response!?");
593
594 if (user_exception || res.headers.get("Connection") == "close")
595 disconnect();
596 break;
597 }
598 if (user_exception) throw user_exception;
599 }
600
601 /// ditto
602 HTTPClientResponse request(scope void delegate(HTTPClientRequest) requester)
603 {
604 bool close_conn;
605 SysTime connected_time;
606 scope (failure) {
607 m_responding = false;
608 disconnect();
609 }
610 bool has_body = doRequestWithRetry(requester, false, close_conn, connected_time);
611 m_responding = true;
612 auto res = new HTTPClientResponse(this, has_body, close_conn, () @trusted { return vibeThreadAllocator(); } (), connected_time);
613
614 // proxy implementation
615 if (res.headers.get("Proxy-Authenticate", null) !is null) {
616 doProxyRequest(res, requester, close_conn, has_body);
617 }
618
619 return res;
620 }
621
622 private bool doRequestWithRetry(scope void delegate(HTTPClientRequest req) requester, bool confirmed_proxy_auth /* basic only */, out bool close_conn, out SysTime connected_time)
623 {
624 if (m_conn && m_conn.connected && Clock.currTime(UTC()) > m_keepAliveLimit){
625 logDebug("Disconnected to avoid timeout");
626 disconnect();
627 }
628
629 // check if this isn't the first request on a connection
630 bool is_persistent_request = m_conn && m_conn.connected;
631
632 // retry the request if the connection gets closed prematurely and this is a persistent request
633 bool has_body;
634 foreach (i; 0 .. is_persistent_request ? 2 : 1) {
635 connected_time = Clock.currTime(UTC());
636
637 close_conn = false;
638 has_body = doRequest(requester, close_conn, false, connected_time);
639
640 logTrace("HTTP client waiting for response");
641 if (!m_stream.empty) break;
642 }
643 return has_body;
644 }
645
646 private bool doRequest(scope void delegate(HTTPClientRequest req) requester, ref bool close_conn, bool confirmed_proxy_auth = false /* basic only */, SysTime connected_time = Clock.currTime(UTC()))
647 {
648 assert(!m_requesting, "Interleaved HTTP client requests detected!");
649 assert(!m_responding, "Interleaved HTTP client request/response detected!");
650
651 m_requesting = true;
652 scope(exit) m_requesting = false;
653
654 if (!m_conn || !m_conn.connected || m_conn.waitForDataEx(0.seconds) == WaitForDataStatus.noMoreData) {
655 if (m_conn)
656 disconnect(); // make sure all resources are freed
657
658 if (m_settings.proxyURL.host !is null){
659
660 enum AddressType {
661 IPv4,
662 IPv6,
663 Host
664 }
665
666 static AddressType getAddressType(string host){
667 import std.regex : regex, Captures, Regex, matchFirst;
668
669 static IPv4Regex = regex(`^\s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\s*$`, ``);
670 static IPv6Regex = regex(`^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$`, ``);
671
672 if (!matchFirst(host, IPv4Regex).empty)
673 {
674 return AddressType.IPv4;
675 }
676 else if (!matchFirst(host, IPv6Regex).empty)
677 {
678 return AddressType.IPv6;
679 }
680 else
681 {
682 return AddressType.Host;
683 }
684 }
685
686 import std.functional : memoize;
687 alias findAddressType = memoize!getAddressType;
688
689 bool use_dns;
690 if (() @trusted { return findAddressType(m_settings.proxyURL.host); } () == AddressType.Host)
691 {
692 use_dns = true;
693 }
694
695 NetworkAddress proxyAddr = resolveHost(m_settings.proxyURL.host, m_settings.dnsAddressFamily, use_dns,
696 m_settings.connectTimeout);
697 proxyAddr.port = m_settings.proxyURL.port;
698 m_conn = connectTCPWithTimeout(proxyAddr, m_settings.networkInterface, m_settings.connectTimeout);
699 }
700 else {
701 version(UnixSocket)
702 {
703 import core.sys.posix.sys.un;
704 import core.sys.posix.sys.socket;
705 import std.regex : regex, Captures, Regex, matchFirst, ctRegex;
706 import core.stdc.string : strcpy;
707
708 NetworkAddress addr;
709 if (m_server[0] == '/')
710 {
711 addr.family = AF_UNIX;
712 sockaddr_un* s = addr.sockAddrUnix();
713 enforce(s.sun_path.length > m_server.length, "Unix sockets cannot have that long a name.");
714 s.sun_family = AF_UNIX;
715 () @trusted { strcpy(cast(char*)s.sun_path.ptr,m_server.toStringz()); } ();
716 } else
717 {
718 addr = resolveHost(m_server, m_settings.dnsAddressFamily, true, m_settings.connectTimeout);
719 addr.port = m_port;
720 }
721 m_conn = connectTCPWithTimeout(addr, m_settings.networkInterface, m_settings.connectTimeout);
722 } else
723 {
724 auto addr = resolveHost(m_server, m_settings.dnsAddressFamily, true, m_settings.connectTimeout);
725 addr.port = m_port;
726 m_conn = connectTCPWithTimeout(addr, m_settings.networkInterface, m_settings.connectTimeout);
727 }
728 }
729
730 if (m_settings.readTimeout != Duration.max)
731 m_conn.readTimeout = m_settings.readTimeout;
732
733 m_stream = m_conn;
734 if (m_useTLS) {
735 try m_tlsStream = createTLSStream(m_conn, m_tls, TLSStreamState.connecting, m_tlsPeerName, m_conn.remoteAddress);
736 catch (Exception e) {
737 m_conn.close();
738 m_conn = TCPConnection.init;
739 throw e;
740 }
741 m_stream = m_tlsStream;
742 }
743 }
744
745 return () @trusted { // scoped
746 auto req = scoped!HTTPClientRequest(m_stream, m_conn);
747 if (m_useTLS)
748 req.m_peerCertificate = m_tlsStream.peerCertificate;
749
750 req.headers["User-Agent"] = m_userAgent;
751 if (m_settings.proxyURL.host !is null){
752 req.headers["Proxy-Connection"] = "keep-alive";
753 if (confirmed_proxy_auth)
754 {
755 import std.base64;
756 ubyte[] user_pass = cast(ubyte[])(m_settings.proxyURL.username ~ ":" ~ m_settings.proxyURL.password);
757
758 req.headers["Proxy-Authorization"] = "Basic " ~ cast(string) Base64.encode(user_pass);
759 }
760 }
761 else {
762 req.headers["Connection"] = "keep-alive";
763 }
764 req.headers["Accept-Encoding"] = "gzip, deflate";
765 req.headers["Host"] = m_server;
766 requester(req);
767
768 if (req.httpVersion == HTTPVersion.HTTP_1_0)
769 close_conn = true;
770 else if (m_settings.proxyURL.host !is null)
771 close_conn = req.headers.get("Proxy-Connection", "keep-alive") != "keep-alive";
772 else
773 close_conn = req.headers.get("Connection", "keep-alive") != "keep-alive";
774
775 req.finalize();
776
777 return req.method != HTTPMethod.HEAD;
778 } ();
779 }
780 }
781
782 private auto connectTCPWithTimeout(NetworkAddress addr, NetworkAddress bind_address, Duration timeout)
783 {
784 return connectTCP(addr, bind_address, timeout);
785 }
786
787 /**
788 Represents a HTTP client request (as sent to the server).
789 */
790 final class HTTPClientRequest : HTTPRequest {
791 private {
792 InterfaceProxy!OutputStream m_bodyWriter;
793 FreeListRef!ChunkedOutputStream m_chunkedStream;
794 bool m_headerWritten = false;
795 FixedAppender!(string, 22) m_contentLengthBuffer;
796 TCPConnection m_rawConn;
797 TLSCertificateInformation m_peerCertificate;
798 }
799
800
801 /// private
802 this(InterfaceProxy!Stream conn, TCPConnection raw_conn)
803 {
804 super(conn);
805 m_rawConn = raw_conn;
806 }
807
808 @property NetworkAddress localAddress() const { return m_rawConn.localAddress; }
809 @property NetworkAddress remoteAddress() const { return m_rawConn.remoteAddress; }
810
811 @property ref inout(TLSCertificateInformation) peerCertificate() inout { return m_peerCertificate; }
812
813 /**
814 Accesses the Content-Length header of the request.
815
816 Negative values correspond to an unset Content-Length header.
817 */
818 @property long contentLength() const { return headers.get("Content-Length", "-1").to!long(); }
819 /// ditto
820 @property void contentLength(long value)
821 {
822 if (value >= 0) headers["Content-Length"] = clengthString(value);
823 else if ("Content-Length" in headers) headers.remove("Content-Length");
824 }
825
826 /**
827 Writes the whole request body at once using raw bytes.
828 */
829 void writeBody(RandomAccessStream data)
830 {
831 writeBody(data, data.size - data.tell());
832 }
833 /// ditto
834 void writeBody(InputStream data)
835 {
836 data.pipe(bodyWriter);
837 finalize();
838 }
839 /// ditto
840 void writeBody(InputStream data, ulong length)
841 {
842 headers["Content-Length"] = clengthString(length);
843 data.pipe(bodyWriter, length);
844 finalize();
845 }
846 /// ditto
847 void writeBody(in ubyte[] data, string content_type = null)
848 {
849 if( content_type != "" ) headers["Content-Type"] = content_type;
850 headers["Content-Length"] = clengthString(data.length);
851 bodyWriter.write(data);
852 finalize();
853 }
854
855 /**
856 Writes the request body as JSON data.
857 */
858 void writeJsonBody(T)(T data, bool allow_chunked = false)
859 {
860 import vibe.stream.wrapper : streamOutputRange;
861
862 headers["Content-Type"] = "application/json; charset=UTF-8";
863
864 // set an explicit content-length field if chunked encoding is not allowed
865 if (!allow_chunked) {
866 import vibe.internal.rangeutil;
867 long length = 0;
868 auto counter = () @trusted { return RangeCounter(&length); } ();
869 () @trusted { serializeToJson(counter, data); } ();
870 headers["Content-Length"] = clengthString(length);
871 }
872
873 auto rng = streamOutputRange!1024(bodyWriter);
874 () @trusted { serializeToJson(&rng, data); } ();
875 rng.flush();
876 finalize();
877 }
878
879 /** Writes the request body as form data.
880 */
881 void writeFormBody(T)(T key_value_map)
882 {
883 import vibe.inet.webform : formEncode;
884 import vibe.stream.wrapper : streamOutputRange;
885
886 import vibe.internal.rangeutil;
887 long length = 0;
888 auto counter = () @trusted { return RangeCounter(&length); } ();
889 counter.formEncode(key_value_map);
890 headers["Content-Length"] = clengthString(length);
891 headers["Content-Type"] = "application/x-www-form-urlencoded";
892 auto dst = streamOutputRange!1024(bodyWriter);
893 () @trusted { return &dst; } ().formEncode(key_value_map);
894 }
895
896 ///
897 unittest {
898 void test(HTTPClientRequest req) {
899 req.writeFormBody(["foo": "bar"]);
900 }
901 }
902
903 void writePart(MultiPart part)
904 {
905 assert(false, "TODO");
906 }
907
908 /**
909 An output stream suitable for writing the request body.
910
911 The first retrieval will cause the request header to be written, make sure
912 that all headers are set up in advance.s
913 */
914 @property InterfaceProxy!OutputStream bodyWriter()
915 {
916 if (m_bodyWriter) return m_bodyWriter;
917
918 assert(!m_headerWritten, "Trying to write request body after body was already written.");
919
920 if (httpVersion != HTTPVersion.HTTP_1_0
921 && "Content-Length" !in headers && "Transfer-Encoding" !in headers
922 && headers.get("Connection", "") != "close")
923 {
924 headers["Transfer-Encoding"] = "chunked";
925 }
926
927 writeHeader();
928 m_bodyWriter = m_conn;
929
930 if (headers.get("Transfer-Encoding", null) == "chunked") {
931 m_chunkedStream = createChunkedOutputStreamFL(m_bodyWriter);
932 m_bodyWriter = m_chunkedStream;
933 }
934
935 return m_bodyWriter;
936 }
937
938 private void writeHeader()
939 {
940 import vibe.stream.wrapper;
941
942 assert(!m_headerWritten, "HTTPClient tried to write headers twice.");
943 m_headerWritten = true;
944
945 auto output = streamOutputRange!1024(m_conn);
946
947 formattedWrite(() @trusted { return &output; } (), "%s %s %s\r\n", httpMethodString(method), requestURL, getHTTPVersionString(httpVersion));
948 logTrace("--------------------");
949 logTrace("HTTP client request:");
950 logTrace("--------------------");
951 logTrace("%s", this);
952 foreach (k, v; headers.byKeyValue) {
953 () @trusted { formattedWrite(&output, "%s: %s\r\n", k, v); } ();
954 logTrace("%s: %s", k, v);
955 }
956 output.put("\r\n");
957 logTrace("--------------------");
958 }
959
960 private void finalize()
961 {
962 // test if already finalized
963 if (m_headerWritten && !m_bodyWriter)
964 return;
965
966 // force the request to be sent
967 if (!m_headerWritten) writeHeader();
968 else {
969 bodyWriter.flush();
970 if (m_chunkedStream) {
971 m_bodyWriter.finalize();
972 m_conn.flush();
973 }
974 m_bodyWriter = typeof(m_bodyWriter).init;
975 m_conn = typeof(m_conn).init;
976 }
977 }
978
979 private string clengthString(ulong len)
980 {
981 m_contentLengthBuffer.clear();
982 () @trusted { formattedWrite(&m_contentLengthBuffer, "%s", len); } ();
983 return () @trusted { return m_contentLengthBuffer.data; } ();
984 }
985 }
986
987
988 /**
989 Represents a HTTP client response (as received from the server).
990 */
991 final class HTTPClientResponse : HTTPResponse {
992 @safe:
993
994 private {
995 HTTPClient m_client;
996 LockedConnection!HTTPClient lockedConnection;
997 FreeListRef!LimitedInputStream m_limitedInputStream;
998 FreeListRef!ChunkedInputStream m_chunkedInputStream;
999 FreeListRef!ZlibInputStream m_zlibInputStream;
1000 FreeListRef!EndCallbackInputStream m_endCallback;
1001 InterfaceProxy!InputStream m_bodyReader;
1002 bool m_closeConn;
1003 int m_maxRequests;
1004 }
1005
1006 /// Contains the keep-alive 'max' parameter, indicates how many requests a client can
1007 /// make before the server closes the connection.
1008 @property int maxRequests() const {
1009 return m_maxRequests;
1010 }
1011
1012
1013 /// All cookies that shall be set on the client for this request
1014 override @property ref DictionaryList!Cookie cookies() {
1015 if ("Set-Cookie" in this.headers && m_cookies.length == 0) {
1016 foreach (cookieString; this.headers.getAll("Set-Cookie")) {
1017 auto cookie = parseHTTPCookie(cookieString);
1018 if (cookie[0].length)
1019 m_cookies[cookie[0]] = cookie[1];
1020 }
1021 }
1022 return m_cookies;
1023 }
1024
1025 /// private
1026 this(HTTPClient client, bool has_body, bool close_conn, IAllocator alloc, SysTime connected_time = Clock.currTime(UTC()))
1027 {
1028 m_client = client;
1029 m_closeConn = close_conn;
1030
1031 scope(failure) finalize(true);
1032
1033 // read and parse status line ("HTTP/#.# #[ $]\r\n")
1034 logTrace("HTTP client reading status line");
1035 string stln = () @trusted { return cast(string)client.m_stream.readLine(HTTPClient.maxHeaderLineLength, "\r\n", alloc); } ();
1036 logTrace("stln: %s", stln);
1037 this.httpVersion = parseHTTPVersion(stln);
1038
1039 enforce(stln.startsWith(" "));
1040 stln = stln[1 .. $];
1041 this.statusCode = parse!int(stln);
1042 if( stln.length > 0 ){
1043 enforce(stln.startsWith(" "));
1044 stln = stln[1 .. $];
1045 this.statusPhrase = stln;
1046 }
1047
1048 // read headers until an empty line is hit
1049 parseRFC5322Header(client.m_stream, this.headers, HTTPClient.maxHeaderLineLength, alloc, false);
1050
1051 logTrace("---------------------");
1052 logTrace("HTTP client response:");
1053 logTrace("---------------------");
1054 logTrace("%s", this);
1055 foreach (k, v; this.headers.byKeyValue)
1056 logTrace("%s: %s", k, v);
1057 logTrace("---------------------");
1058 Duration server_timeout;
1059 bool has_server_timeout;
1060 if (auto pka = "Keep-Alive" in this.headers) {
1061 foreach(s; splitter(*pka, ',')){
1062 auto pair = s.splitter('=');
1063 auto name = pair.front.strip();
1064 pair.popFront();
1065 if (icmp(name, "timeout") == 0) {
1066 has_server_timeout = true;
1067 server_timeout = pair.front.to!int().seconds;
1068 } else if (icmp(name, "max") == 0) {
1069 m_maxRequests = pair.front.to!int();
1070 }
1071 }
1072 }
1073 Duration elapsed = Clock.currTime(UTC()) - connected_time;
1074 if (this.headers.get("Connection") == "close") {
1075 // this header will trigger m_client.disconnect() in m_client.doRequest() when it goes out of scope
1076 } else if (has_server_timeout && m_client.m_keepAliveTimeout > server_timeout) {
1077 m_client.m_keepAliveLimit = Clock.currTime(UTC()) + server_timeout - elapsed;
1078 } else if (this.httpVersion == HTTPVersion.HTTP_1_1) {
1079 m_client.m_keepAliveLimit = Clock.currTime(UTC()) + m_client.m_keepAliveTimeout;
1080 }
1081
1082 if (!has_body) finalize();
1083 }
1084
1085 ~this()
1086 {
1087 debug if (m_client) {
1088 import core.stdc.stdio;
1089 printf("WARNING: HTTPClientResponse not fully processed before being finalized\n");
1090 }
1091 }
1092
1093 /**
1094 An input stream suitable for reading the response body.
1095 */
1096 @property InterfaceProxy!InputStream bodyReader()
1097 {
1098 if( m_bodyReader ) return m_bodyReader;
1099
1100 assert (m_client, "Response was already read or no response body, may not use bodyReader.");
1101
1102 // prepare body the reader
1103 if (auto pte = "Transfer-Encoding" in this.headers) {
1104 enforce(*pte == "chunked");
1105 m_chunkedInputStream = createChunkedInputStreamFL(m_client.m_stream);
1106 m_bodyReader = this.m_chunkedInputStream;
1107 } else if (auto pcl = "Content-Length" in this.headers) {
1108 m_limitedInputStream = createLimitedInputStreamFL(m_client.m_stream, to!ulong(*pcl));
1109 m_bodyReader = m_limitedInputStream;
1110 } else if (isKeepAliveResponse) {
1111 m_limitedInputStream = createLimitedInputStreamFL(m_client.m_stream, 0);
1112 m_bodyReader = m_limitedInputStream;
1113 } else {
1114 m_bodyReader = m_client.m_stream;
1115 }
1116
1117 if( auto pce = "Content-Encoding" in this.headers ){
1118 if( *pce == "deflate" ){
1119 m_zlibInputStream = createDeflateInputStreamFL(m_bodyReader);
1120 m_bodyReader = m_zlibInputStream;
1121 } else if( *pce == "gzip" || *pce == "x-gzip"){
1122 m_zlibInputStream = createGzipInputStreamFL(m_bodyReader);
1123 m_bodyReader = m_zlibInputStream;
1124 }
1125 else enforce(*pce == "identity" || *pce == "", "Unsuported content encoding: "~*pce);
1126 }
1127
1128 // be sure to free resouces as soon as the response has been read
1129 m_endCallback = createEndCallbackInputStreamFL(m_bodyReader, &this.finalize);
1130 m_bodyReader = m_endCallback;
1131
1132 return m_bodyReader;
1133 }
1134
1135 /**
1136 Provides unsafe means to read raw data from the connection.
1137
1138 No transfer decoding and no content decoding is done on the data.
1139
1140 Not that the provided delegate must read the whole stream,
1141 as the state of the response is unknown after raw bytes have been
1142 taken. Failure to read the right amount of data will lead to
1143 protocol corruption in later requests.
1144 */
1145 void readRawBody(scope void delegate(scope InterfaceProxy!InputStream stream) @safe del)
1146 {
1147 assert(!m_bodyReader, "May not mix use of readRawBody and bodyReader.");
1148 del(interfaceProxy!InputStream(m_client.m_stream));
1149 finalize();
1150 }
1151 /// ditto
1152 static if (!is(InputStream == InterfaceProxy!InputStream))
1153 void readRawBody(scope void delegate(scope InputStream stream) @safe del)
1154 {
1155 import vibe.internal.interfaceproxy : asInterface;
1156
1157 assert(!m_bodyReader, "May not mix use of readRawBody and bodyReader.");
1158 del(m_client.m_stream.asInterface!(.InputStream));
1159 finalize();
1160 }
1161
1162 /**
1163 Reads the whole response body and tries to parse it as JSON.
1164 */
1165 Json readJson(){
1166 auto bdy = bodyReader.readAllUTF8();
1167 return () @trusted { return parseJson(bdy); } ();
1168 }
1169
1170 /**
1171 Reads and discards the response body.
1172 */
1173 void dropBody()
1174 {
1175 if (m_client) {
1176 if( bodyReader.empty ){
1177 finalize();
1178 } else {
1179 bodyReader.pipe(nullSink);
1180 assert(!lockedConnection.__conn);
1181 }
1182 }
1183 }
1184
1185 /**
1186 Forcefully terminates the connection regardless of the current state.
1187
1188 Note that this will only actually disconnect if the request has not yet
1189 been fully processed. If the whole body was already read, the
1190 connection is not owned by the current request operation anymore and
1191 cannot be accessed. Use a "Connection: close" header instead in this
1192 case to let the server close the connection.
1193 */
1194 void disconnect()
1195 {
1196 finalize(true);
1197 }
1198
1199 /**
1200 Switches the connection to a new protocol and returns the resulting ConnectionStream.
1201
1202 The caller caller gets ownership of the ConnectionStream and is responsible
1203 for closing it.
1204
1205 Notice:
1206 When using the overload that returns a `ConnectionStream`, the caller
1207 must make sure that the stream is not used after the
1208 `HTTPClientRequest` has been destroyed.
1209
1210 Params:
1211 new_protocol = The protocol to which the connection is expected to
1212 upgrade. Should match the Upgrade header of the request. If an
1213 empty string is passed, the "Upgrade" header will be ignored and
1214 should be checked by other means.
1215 */
1216 ConnectionStream switchProtocol(string new_protocol)
1217 {
1218 enforce(statusCode == HTTPStatus.switchingProtocols, "Server did not send a 101 - Switching Protocols response");
1219 string *resNewProto = "Upgrade" in headers;
1220 enforce(resNewProto, "Server did not send an Upgrade header");
1221 enforce(!new_protocol.length || !icmp(*resNewProto, new_protocol),
1222 "Expected Upgrade: " ~ new_protocol ~", received Upgrade: " ~ *resNewProto);
1223 auto stream = createConnectionProxyStream!(typeof(m_client.m_stream), typeof(m_client.m_conn))(m_client.m_stream, m_client.m_conn);
1224 m_closeConn = true; // cannot reuse connection for further requests!
1225 return stream;
1226 }
1227 /// ditto
1228 void switchProtocol(string new_protocol, scope void delegate(ConnectionStream str) @safe del)
1229 {
1230 enforce(statusCode == HTTPStatus.switchingProtocols, "Server did not send a 101 - Switching Protocols response");
1231 string *resNewProto = "Upgrade" in headers;
1232 enforce(resNewProto, "Server did not send an Upgrade header");
1233 enforce(!new_protocol.length || !icmp(*resNewProto, new_protocol),
1234 "Expected Upgrade: " ~ new_protocol ~", received Upgrade: " ~ *resNewProto);
1235 auto stream = createConnectionProxyStream(m_client.m_stream, m_client.m_conn);
1236 scope (exit) () @trusted { destroy(stream); } ();
1237 m_closeConn = true;
1238 del(stream);
1239 }
1240
1241 private @property isKeepAliveResponse()
1242 const {
1243 string conn;
1244 if (this.httpVersion == HTTPVersion.HTTP_1_0) {
1245 // Workaround for non-standard-conformant servers - for example see #1780
1246 auto pcl = "Content-Length" in this.headers;
1247 if (pcl) conn = this.headers.get("Connection", "close");
1248 else return false; // can't use keepalive when no content length is set
1249 }
1250 else conn = this.headers.get("Connection", "keep-alive");
1251 return icmp(conn, "close") != 0;
1252 }
1253
1254 private void finalize()
1255 {
1256 finalize(m_closeConn);
1257 }
1258
1259 private void finalize(bool disconnect)
1260 {
1261 // ignore duplicate and too early calls to finalize
1262 // (too early happesn for empty response bodies)
1263 if (!m_client) return;
1264
1265 auto cli = m_client;
1266 m_client = null;
1267 cli.m_responding = false;
1268 destroy(m_zlibInputStream);
1269 destroy(m_chunkedInputStream);
1270 destroy(m_limitedInputStream);
1271 if (disconnect) cli.disconnect();
1272 destroy(lockedConnection);
1273 }
1274 }
1275
1276 /** Returns clean host string. In case of unix socket it performs urlDecode on host. */
1277 package auto getFilteredHost(URL url)
1278 {
1279 version(UnixSocket)
1280 {
1281 import vibe.textfilter.urlencode : urlDecode;
1282 if (url.schema == "https+unix" || url.schema == "http+unix")
1283 return urlDecode(url.host);
1284 else
1285 return url.host;
1286 } else
1287 return url.host;
1288 }
1289
1290 // This object is a placeholder and should to never be modified.
1291 package @property const(HTTPClientSettings) defaultSettings()
1292 @trusted nothrow {
1293 __gshared HTTPClientSettings ret = new HTTPClientSettings;
1294 return ret;
1295 }