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