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