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 version (Have_vibe_core) { 785 return connectTCP(addr, bind_address, timeout); 786 } else { 787 if (timeout != Duration.max) 788 logWarn("HTTP client connect timeout is set, but not supported by the legacy vibe-d:core module."); 789 return connectTCP(addr, bind_address); 790 } 791 } 792 793 /** 794 Represents a HTTP client request (as sent to the server). 795 */ 796 final class HTTPClientRequest : HTTPRequest { 797 private { 798 InterfaceProxy!OutputStream m_bodyWriter; 799 FreeListRef!ChunkedOutputStream m_chunkedStream; 800 bool m_headerWritten = false; 801 FixedAppender!(string, 22) m_contentLengthBuffer; 802 TCPConnection m_rawConn; 803 TLSCertificateInformation m_peerCertificate; 804 } 805 806 807 /// private 808 this(InterfaceProxy!Stream conn, TCPConnection raw_conn) 809 { 810 super(conn); 811 m_rawConn = raw_conn; 812 } 813 814 @property NetworkAddress localAddress() const { return m_rawConn.localAddress; } 815 @property NetworkAddress remoteAddress() const { return m_rawConn.remoteAddress; } 816 817 @property ref inout(TLSCertificateInformation) peerCertificate() inout { return m_peerCertificate; } 818 819 /** 820 Accesses the Content-Length header of the request. 821 822 Negative values correspond to an unset Content-Length header. 823 */ 824 @property long contentLength() const { return headers.get("Content-Length", "-1").to!long(); } 825 /// ditto 826 @property void contentLength(long value) 827 { 828 if (value >= 0) headers["Content-Length"] = clengthString(value); 829 else if ("Content-Length" in headers) headers.remove("Content-Length"); 830 } 831 832 /** 833 Writes the whole request body at once using raw bytes. 834 */ 835 void writeBody(RandomAccessStream data) 836 { 837 writeBody(data, data.size - data.tell()); 838 } 839 /// ditto 840 void writeBody(InputStream data) 841 { 842 data.pipe(bodyWriter); 843 finalize(); 844 } 845 /// ditto 846 void writeBody(InputStream data, ulong length) 847 { 848 headers["Content-Length"] = clengthString(length); 849 data.pipe(bodyWriter, length); 850 finalize(); 851 } 852 /// ditto 853 void writeBody(in ubyte[] data, string content_type = null) 854 { 855 if( content_type != "" ) headers["Content-Type"] = content_type; 856 headers["Content-Length"] = clengthString(data.length); 857 bodyWriter.write(data); 858 finalize(); 859 } 860 861 /** 862 Writes the request body as JSON data. 863 */ 864 void writeJsonBody(T)(T data, bool allow_chunked = false) 865 { 866 import vibe.stream.wrapper : streamOutputRange; 867 868 headers["Content-Type"] = "application/json; charset=UTF-8"; 869 870 // set an explicit content-length field if chunked encoding is not allowed 871 if (!allow_chunked) { 872 import vibe.internal.rangeutil; 873 long length = 0; 874 auto counter = () @trusted { return RangeCounter(&length); } (); 875 () @trusted { serializeToJson(counter, data); } (); 876 headers["Content-Length"] = clengthString(length); 877 } 878 879 auto rng = streamOutputRange!1024(bodyWriter); 880 () @trusted { serializeToJson(&rng, data); } (); 881 rng.flush(); 882 finalize(); 883 } 884 885 /** Writes the request body as form data. 886 */ 887 void writeFormBody(T)(T key_value_map) 888 { 889 import vibe.inet.webform : formEncode; 890 import vibe.stream.wrapper : streamOutputRange; 891 892 import vibe.internal.rangeutil; 893 long length = 0; 894 auto counter = () @trusted { return RangeCounter(&length); } (); 895 counter.formEncode(key_value_map); 896 headers["Content-Length"] = clengthString(length); 897 headers["Content-Type"] = "application/x-www-form-urlencoded"; 898 auto dst = streamOutputRange!1024(bodyWriter); 899 () @trusted { return &dst; } ().formEncode(key_value_map); 900 } 901 902 /// 903 unittest { 904 void test(HTTPClientRequest req) { 905 req.writeFormBody(["foo": "bar"]); 906 } 907 } 908 909 void writePart(MultiPart part) 910 { 911 assert(false, "TODO"); 912 } 913 914 /** 915 An output stream suitable for writing the request body. 916 917 The first retrieval will cause the request header to be written, make sure 918 that all headers are set up in advance.s 919 */ 920 @property InterfaceProxy!OutputStream bodyWriter() 921 { 922 if (m_bodyWriter) return m_bodyWriter; 923 924 assert(!m_headerWritten, "Trying to write request body after body was already written."); 925 926 if (httpVersion != HTTPVersion.HTTP_1_0 927 && "Content-Length" !in headers && "Transfer-Encoding" !in headers 928 && headers.get("Connection", "") != "close") 929 { 930 headers["Transfer-Encoding"] = "chunked"; 931 } 932 933 writeHeader(); 934 m_bodyWriter = m_conn; 935 936 if (headers.get("Transfer-Encoding", null) == "chunked") { 937 m_chunkedStream = createChunkedOutputStreamFL(m_bodyWriter); 938 m_bodyWriter = m_chunkedStream; 939 } 940 941 return m_bodyWriter; 942 } 943 944 private void writeHeader() 945 { 946 import vibe.stream.wrapper; 947 948 assert(!m_headerWritten, "HTTPClient tried to write headers twice."); 949 m_headerWritten = true; 950 951 auto output = streamOutputRange!1024(m_conn); 952 953 formattedWrite(() @trusted { return &output; } (), "%s %s %s\r\n", httpMethodString(method), requestURL, getHTTPVersionString(httpVersion)); 954 logTrace("--------------------"); 955 logTrace("HTTP client request:"); 956 logTrace("--------------------"); 957 logTrace("%s", this); 958 foreach (k, v; headers.byKeyValue) { 959 () @trusted { formattedWrite(&output, "%s: %s\r\n", k, v); } (); 960 logTrace("%s: %s", k, v); 961 } 962 output.put("\r\n"); 963 logTrace("--------------------"); 964 } 965 966 private void finalize() 967 { 968 // test if already finalized 969 if (m_headerWritten && !m_bodyWriter) 970 return; 971 972 // force the request to be sent 973 if (!m_headerWritten) writeHeader(); 974 else { 975 bodyWriter.flush(); 976 if (m_chunkedStream) { 977 m_bodyWriter.finalize(); 978 m_conn.flush(); 979 } 980 m_bodyWriter = typeof(m_bodyWriter).init; 981 m_conn = typeof(m_conn).init; 982 } 983 } 984 985 private string clengthString(ulong len) 986 { 987 m_contentLengthBuffer.clear(); 988 () @trusted { formattedWrite(&m_contentLengthBuffer, "%s", len); } (); 989 return () @trusted { return m_contentLengthBuffer.data; } (); 990 } 991 } 992 993 994 /** 995 Represents a HTTP client response (as received from the server). 996 */ 997 final class HTTPClientResponse : HTTPResponse { 998 @safe: 999 1000 private { 1001 HTTPClient m_client; 1002 LockedConnection!HTTPClient lockedConnection; 1003 FreeListRef!LimitedInputStream m_limitedInputStream; 1004 FreeListRef!ChunkedInputStream m_chunkedInputStream; 1005 FreeListRef!ZlibInputStream m_zlibInputStream; 1006 FreeListRef!EndCallbackInputStream m_endCallback; 1007 InterfaceProxy!InputStream m_bodyReader; 1008 bool m_closeConn; 1009 int m_maxRequests; 1010 } 1011 1012 /// Contains the keep-alive 'max' parameter, indicates how many requests a client can 1013 /// make before the server closes the connection. 1014 @property int maxRequests() const { 1015 return m_maxRequests; 1016 } 1017 1018 1019 /// All cookies that shall be set on the client for this request 1020 override @property ref DictionaryList!Cookie cookies() { 1021 if ("Set-Cookie" in this.headers && m_cookies.length == 0) { 1022 foreach (cookieString; this.headers.getAll("Set-Cookie")) { 1023 auto cookie = parseHTTPCookie(cookieString); 1024 if (cookie[0].length) 1025 m_cookies[cookie[0]] = cookie[1]; 1026 } 1027 } 1028 return m_cookies; 1029 } 1030 1031 /// private 1032 this(HTTPClient client, bool has_body, bool close_conn, IAllocator alloc, SysTime connected_time = Clock.currTime(UTC())) 1033 { 1034 m_client = client; 1035 m_closeConn = close_conn; 1036 1037 scope(failure) finalize(true); 1038 1039 // read and parse status line ("HTTP/#.# #[ $]\r\n") 1040 logTrace("HTTP client reading status line"); 1041 string stln = () @trusted { return cast(string)client.m_stream.readLine(HTTPClient.maxHeaderLineLength, "\r\n", alloc); } (); 1042 logTrace("stln: %s", stln); 1043 this.httpVersion = parseHTTPVersion(stln); 1044 1045 enforce(stln.startsWith(" ")); 1046 stln = stln[1 .. $]; 1047 this.statusCode = parse!int(stln); 1048 if( stln.length > 0 ){ 1049 enforce(stln.startsWith(" ")); 1050 stln = stln[1 .. $]; 1051 this.statusPhrase = stln; 1052 } 1053 1054 // read headers until an empty line is hit 1055 parseRFC5322Header(client.m_stream, this.headers, HTTPClient.maxHeaderLineLength, alloc, false); 1056 1057 logTrace("---------------------"); 1058 logTrace("HTTP client response:"); 1059 logTrace("---------------------"); 1060 logTrace("%s", this); 1061 foreach (k, v; this.headers.byKeyValue) 1062 logTrace("%s: %s", k, v); 1063 logTrace("---------------------"); 1064 Duration server_timeout; 1065 bool has_server_timeout; 1066 if (auto pka = "Keep-Alive" in this.headers) { 1067 foreach(s; splitter(*pka, ',')){ 1068 auto pair = s.splitter('='); 1069 auto name = pair.front.strip(); 1070 pair.popFront(); 1071 if (icmp(name, "timeout") == 0) { 1072 has_server_timeout = true; 1073 server_timeout = pair.front.to!int().seconds; 1074 } else if (icmp(name, "max") == 0) { 1075 m_maxRequests = pair.front.to!int(); 1076 } 1077 } 1078 } 1079 Duration elapsed = Clock.currTime(UTC()) - connected_time; 1080 if (this.headers.get("Connection") == "close") { 1081 // this header will trigger m_client.disconnect() in m_client.doRequest() when it goes out of scope 1082 } else if (has_server_timeout && m_client.m_keepAliveTimeout > server_timeout) { 1083 m_client.m_keepAliveLimit = Clock.currTime(UTC()) + server_timeout - elapsed; 1084 } else if (this.httpVersion == HTTPVersion.HTTP_1_1) { 1085 m_client.m_keepAliveLimit = Clock.currTime(UTC()) + m_client.m_keepAliveTimeout; 1086 } 1087 1088 if (!has_body) finalize(); 1089 } 1090 1091 ~this() 1092 { 1093 debug if (m_client) { 1094 import core.stdc.stdio; 1095 printf("WARNING: HTTPClientResponse not fully processed before being finalized\n"); 1096 } 1097 } 1098 1099 /** 1100 An input stream suitable for reading the response body. 1101 */ 1102 @property InterfaceProxy!InputStream bodyReader() 1103 { 1104 if( m_bodyReader ) return m_bodyReader; 1105 1106 assert (m_client, "Response was already read or no response body, may not use bodyReader."); 1107 1108 // prepare body the reader 1109 if (auto pte = "Transfer-Encoding" in this.headers) { 1110 enforce(*pte == "chunked"); 1111 m_chunkedInputStream = createChunkedInputStreamFL(m_client.m_stream); 1112 m_bodyReader = this.m_chunkedInputStream; 1113 } else if (auto pcl = "Content-Length" in this.headers) { 1114 m_limitedInputStream = createLimitedInputStreamFL(m_client.m_stream, to!ulong(*pcl)); 1115 m_bodyReader = m_limitedInputStream; 1116 } else if (isKeepAliveResponse) { 1117 m_limitedInputStream = createLimitedInputStreamFL(m_client.m_stream, 0); 1118 m_bodyReader = m_limitedInputStream; 1119 } else { 1120 m_bodyReader = m_client.m_stream; 1121 } 1122 1123 if( auto pce = "Content-Encoding" in this.headers ){ 1124 if( *pce == "deflate" ){ 1125 m_zlibInputStream = createDeflateInputStreamFL(m_bodyReader); 1126 m_bodyReader = m_zlibInputStream; 1127 } else if( *pce == "gzip" || *pce == "x-gzip"){ 1128 m_zlibInputStream = createGzipInputStreamFL(m_bodyReader); 1129 m_bodyReader = m_zlibInputStream; 1130 } 1131 else enforce(*pce == "identity" || *pce == "", "Unsuported content encoding: "~*pce); 1132 } 1133 1134 // be sure to free resouces as soon as the response has been read 1135 m_endCallback = createEndCallbackInputStreamFL(m_bodyReader, &this.finalize); 1136 m_bodyReader = m_endCallback; 1137 1138 return m_bodyReader; 1139 } 1140 1141 /** 1142 Provides unsafe means to read raw data from the connection. 1143 1144 No transfer decoding and no content decoding is done on the data. 1145 1146 Not that the provided delegate must read the whole stream, 1147 as the state of the response is unknown after raw bytes have been 1148 taken. Failure to read the right amount of data will lead to 1149 protocol corruption in later requests. 1150 */ 1151 void readRawBody(scope void delegate(scope InterfaceProxy!InputStream stream) @safe del) 1152 { 1153 assert(!m_bodyReader, "May not mix use of readRawBody and bodyReader."); 1154 del(interfaceProxy!InputStream(m_client.m_stream)); 1155 finalize(); 1156 } 1157 /// ditto 1158 static if (!is(InputStream == InterfaceProxy!InputStream)) 1159 void readRawBody(scope void delegate(scope InputStream stream) @safe del) 1160 { 1161 import vibe.internal.interfaceproxy : asInterface; 1162 1163 assert(!m_bodyReader, "May not mix use of readRawBody and bodyReader."); 1164 del(m_client.m_stream.asInterface!(.InputStream)); 1165 finalize(); 1166 } 1167 1168 /** 1169 Reads the whole response body and tries to parse it as JSON. 1170 */ 1171 Json readJson(){ 1172 auto bdy = bodyReader.readAllUTF8(); 1173 return () @trusted { return parseJson(bdy); } (); 1174 } 1175 1176 /** 1177 Reads and discards the response body. 1178 */ 1179 void dropBody() 1180 { 1181 if (m_client) { 1182 if( bodyReader.empty ){ 1183 finalize(); 1184 } else { 1185 bodyReader.pipe(nullSink); 1186 assert(!lockedConnection.__conn); 1187 } 1188 } 1189 } 1190 1191 /** 1192 Forcefully terminates the connection regardless of the current state. 1193 1194 Note that this will only actually disconnect if the request has not yet 1195 been fully processed. If the whole body was already read, the 1196 connection is not owned by the current request operation anymore and 1197 cannot be accessed. Use a "Connection: close" header instead in this 1198 case to let the server close the connection. 1199 */ 1200 void disconnect() 1201 { 1202 finalize(true); 1203 } 1204 1205 /** 1206 Switches the connection to a new protocol and returns the resulting ConnectionStream. 1207 1208 The caller caller gets ownership of the ConnectionStream and is responsible 1209 for closing it. 1210 1211 Notice: 1212 When using the overload that returns a `ConnectionStream`, the caller 1213 must make sure that the stream is not used after the 1214 `HTTPClientRequest` has been destroyed. 1215 1216 Params: 1217 new_protocol = The protocol to which the connection is expected to 1218 upgrade. Should match the Upgrade header of the request. If an 1219 empty string is passed, the "Upgrade" header will be ignored and 1220 should be checked by other means. 1221 */ 1222 ConnectionStream switchProtocol(string new_protocol) 1223 { 1224 enforce(statusCode == HTTPStatus.switchingProtocols, "Server did not send a 101 - Switching Protocols response"); 1225 string *resNewProto = "Upgrade" in headers; 1226 enforce(resNewProto, "Server did not send an Upgrade header"); 1227 enforce(!new_protocol.length || !icmp(*resNewProto, new_protocol), 1228 "Expected Upgrade: " ~ new_protocol ~", received Upgrade: " ~ *resNewProto); 1229 auto stream = createConnectionProxyStream!(typeof(m_client.m_stream), typeof(m_client.m_conn))(m_client.m_stream, m_client.m_conn); 1230 m_closeConn = true; // cannot reuse connection for further requests! 1231 return stream; 1232 } 1233 /// ditto 1234 void switchProtocol(string new_protocol, scope void delegate(ConnectionStream str) @safe del) 1235 { 1236 enforce(statusCode == HTTPStatus.switchingProtocols, "Server did not send a 101 - Switching Protocols response"); 1237 string *resNewProto = "Upgrade" in headers; 1238 enforce(resNewProto, "Server did not send an Upgrade header"); 1239 enforce(!new_protocol.length || !icmp(*resNewProto, new_protocol), 1240 "Expected Upgrade: " ~ new_protocol ~", received Upgrade: " ~ *resNewProto); 1241 auto stream = createConnectionProxyStream(m_client.m_stream, m_client.m_conn); 1242 scope (exit) () @trusted { destroy(stream); } (); 1243 m_closeConn = true; 1244 del(stream); 1245 } 1246 1247 private @property isKeepAliveResponse() 1248 const { 1249 string conn; 1250 if (this.httpVersion == HTTPVersion.HTTP_1_0) { 1251 // Workaround for non-standard-conformant servers - for example see #1780 1252 auto pcl = "Content-Length" in this.headers; 1253 if (pcl) conn = this.headers.get("Connection", "close"); 1254 else return false; // can't use keepalive when no content length is set 1255 } 1256 else conn = this.headers.get("Connection", "keep-alive"); 1257 return icmp(conn, "close") != 0; 1258 } 1259 1260 private void finalize() 1261 { 1262 finalize(m_closeConn); 1263 } 1264 1265 private void finalize(bool disconnect) 1266 { 1267 // ignore duplicate and too early calls to finalize 1268 // (too early happesn for empty response bodies) 1269 if (!m_client) return; 1270 1271 auto cli = m_client; 1272 m_client = null; 1273 cli.m_responding = false; 1274 destroy(m_zlibInputStream); 1275 destroy(m_chunkedInputStream); 1276 destroy(m_limitedInputStream); 1277 if (disconnect) cli.disconnect(); 1278 destroy(lockedConnection); 1279 } 1280 } 1281 1282 /** Returns clean host string. In case of unix socket it performs urlDecode on host. */ 1283 package auto getFilteredHost(URL url) 1284 { 1285 version(UnixSocket) 1286 { 1287 import vibe.textfilter.urlencode : urlDecode; 1288 if (url.schema == "https+unix" || url.schema == "http+unix") 1289 return urlDecode(url.host); 1290 else 1291 return url.host; 1292 } else 1293 return url.host; 1294 } 1295 1296 // This object is a placeholder and should to never be modified. 1297 package @property const(HTTPClientSettings) defaultSettings() 1298 @trusted nothrow { 1299 __gshared HTTPClientSettings ret = new HTTPClientSettings; 1300 return ret; 1301 }