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 }