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