1 // Copyright 2013-2022, Adam D. Ruppe. 2 3 // FIXME: websocket proxy support 4 // FIXME: ipv6 support 5 6 // FIXME: headers are supposed to be case insensitive. ugh. 7 8 /++ 9 This is version 2 of my http/1.1 client implementation. 10 11 12 It has no runtime dependencies for basic operation, but does require OpenSSL 13 libraries (or compatible) to support HTTPS. This dynamically loaded 14 on-demand (meaning it won't be loaded if you don't use it, but if you do 15 use it, the openssl dynamic libraries must be found in the system search path). 16 17 On Windows, you can bundle the openssl dlls with your exe and they will be picked 18 up when distributed. 19 20 You can compile with `-version=without_openssl` to entirely disable ssl support. 21 22 http2.d, despite its name, does NOT implement HTTP/2.0, but this 23 shouldn't matter for 99.9% of usage, since all servers will continue 24 to support HTTP/1.1 for a very long time. 25 26 History: 27 Automatic `100 Continue` handling was added on September 28, 2021. It doesn't 28 set the Expect header, so it isn't supposed to happen, but plenty of web servers 29 don't follow the standard anyway. 30 31 A dependency on [arsd.core] was added on March 19, 2023 (dub v11.0). Previously, 32 module was stand-alone. You will have add the `core.d` file from the arsd repo 33 to your build now if you are managing the files and builds yourself. 34 35 The benefits of this dependency include some simplified implementation code which 36 makes it easier for me to add more api conveniences, better exceptions with more 37 information, and better event loop integration with other arsd modules beyond 38 just the simpledisplay adapters available previously. The new integration can 39 also make things like heartbeat timers easier for you to code. 40 41 A dependency on [arsd.uri], and through it, [arsd.string], and [arsd.conv] was added 42 on November 2, 2025 43 +/ 44 module arsd.http2; 45 46 /// 47 unittest { 48 import arsd.http2; 49 50 void main() { 51 auto client = new HttpClient(); 52 53 auto request = client.request(Uri("http://dlang.org/")); 54 auto response = request.waitForCompletion(); 55 56 import std.stdio; 57 writeln(response.contentText); 58 writeln(response.code, " ", response.codeText); 59 writeln(response.contentType); 60 } 61 62 version(arsd_http2_integration_test) main(); // exclude from docs 63 } 64 65 /+ 66 // arsd core is now default but you can opt out for a lil while 67 version(no_arsd_core) { 68 69 } else { 70 version=use_arsd_core; 71 } 72 +/ 73 74 static import arsd.core; 75 import arsd.uri; 76 public import arsd.uri : Uri; 77 78 // FIXME: I think I want to disable sigpipe here too. 79 80 import arsd.core : encodeUriComponent, decodeUriComponent; 81 82 debug(arsd_http2_verbose) debug=arsd_http2; 83 84 debug(arsd_http2) import std.stdio : writeln; 85 86 version=arsd_http_internal_implementation; 87 88 version(without_openssl) {} 89 else { 90 version=use_openssl; 91 version=with_openssl; 92 version(older_openssl) {} else 93 version=newer_openssl; 94 } 95 96 version(arsd_http_winhttp_implementation) { 97 pragma(lib, "winhttp") 98 import core.sys.windows.winhttp; 99 // FIXME: alter the dub package file too 100 101 // https://github.com/curl/curl/blob/master/lib/vtls/schannel.c 102 // https://docs.microsoft.com/en-us/windows/win32/secauthn/creating-an-schannel-security-context 103 104 105 // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpreaddata 106 // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpsendrequest 107 // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpopenrequest 108 // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpconnect 109 } 110 111 112 113 /++ 114 Demonstrates core functionality, using the [HttpClient], 115 [HttpRequest] (returned by [HttpClient.navigateTo|client.navigateTo]), 116 and [HttpResponse] (returned by [HttpRequest.waitForCompletion|request.waitForCompletion]). 117 118 +/ 119 unittest { 120 import arsd.http2; 121 122 void main() { 123 auto client = new HttpClient(); 124 auto request = client.navigateTo(Uri("http://dlang.org/")); 125 auto response = request.waitForCompletion(); 126 127 string returnedHtml = response.contentText; 128 } 129 } 130 131 private __gshared bool defaultVerifyPeer_ = true; 132 133 void defaultVerifyPeer(bool v) { 134 defaultVerifyPeer_ = v; 135 } 136 137 debug import std.stdio; 138 139 import std.socket; 140 import core.time; 141 142 // FIXME: check Transfer-Encoding: gzip always 143 144 version(with_openssl) { 145 //pragma(lib, "crypto"); 146 //pragma(lib, "ssl"); 147 } 148 149 /+ 150 HttpRequest httpRequest(string method, string url, ubyte[] content, string[string] content) { 151 return null; 152 } 153 +/ 154 155 /** 156 auto request = get("http://arsdnet.net/"); 157 request.send(); 158 159 auto response = get("http://arsdnet.net/").waitForCompletion(); 160 */ 161 HttpRequest get(string url) { 162 auto client = new HttpClient(); 163 auto request = client.navigateTo(Uri(url)); 164 return request; 165 } 166 167 /** 168 Do not forget to call `waitForCompletion()` on the returned object! 169 */ 170 HttpRequest post(string url, string[string] req) { 171 auto client = new HttpClient(); 172 ubyte[] bdata; 173 foreach(k, v; req) { 174 if(bdata.length) 175 bdata ~= cast(ubyte[]) "&"; 176 bdata ~= cast(ubyte[]) encodeUriComponent(k); 177 bdata ~= cast(ubyte[]) "="; 178 bdata ~= cast(ubyte[]) encodeUriComponent(v); 179 } 180 auto request = client.request(Uri(url), HttpVerb.POST, bdata, "application/x-www-form-urlencoded"); 181 return request; 182 } 183 184 /// gets the text off a url. basic operation only. 185 string getText(string url) { 186 auto request = get(url); 187 auto response = request.waitForCompletion(); 188 return cast(string) response.content; 189 } 190 191 /+ 192 ubyte[] getBinary(string url, string[string] cookies = null) { 193 auto hr = httpRequest("GET", url, null, cookies); 194 if(hr.code != 200) 195 throw new Exception(format("HTTP answered %d instead of 200 on %s", hr.code, url)); 196 return hr.content; 197 } 198 199 /** 200 Gets a textual document, ignoring headers. Throws on non-text or error. 201 */ 202 string get(string url, string[string] cookies = null) { 203 auto hr = httpRequest("GET", url, null, cookies); 204 if(hr.code != 200) 205 throw new Exception(format("HTTP answered %d instead of 200 on %s", hr.code, url)); 206 if(hr.contentType.indexOf("text/") == -1) 207 throw new Exception(hr.contentType ~ " is bad content for conversion to string"); 208 return cast(string) hr.content; 209 210 } 211 212 string post(string url, string[string] args, string[string] cookies = null) { 213 string content; 214 215 foreach(name, arg; args) { 216 if(content.length) 217 content ~= "&"; 218 content ~= encodeUriComponent(name) ~ "=" ~ encodeUriComponent(arg); 219 } 220 221 auto hr = httpRequest("POST", url, cast(ubyte[]) content, cookies, ["Content-Type: application/x-www-form-urlencoded"]); 222 if(hr.code != 200) 223 throw new Exception(format("HTTP answered %d instead of 200", hr.code)); 224 if(hr.contentType.indexOf("text/") == -1) 225 throw new Exception(hr.contentType ~ " is bad content for conversion to string"); 226 227 return cast(string) hr.content; 228 } 229 230 +/ 231 232 /// 233 struct HttpResponse { 234 /++ 235 The HTTP response code, if the response was completed, or some value < 100 if it was aborted or failed. 236 237 Code 0 - initial value, nothing happened 238 Code 1 - you called request.abort 239 Code 2 - connection refused 240 Code 3 - connection succeeded, but server disconnected early 241 Code 4 - server sent corrupted response (or this code has a bug and processed it wrong) 242 Code 5 - request timed out 243 244 Code >= 100 - a HTTP response 245 +/ 246 int code; 247 string codeText; /// 248 249 string httpVersion; /// 250 251 string statusLine; /// 252 253 string contentType; /// The *full* content type header. See also [contentTypeMimeType] and [contentTypeCharset]. 254 string location; /// The location header 255 256 /++ 257 258 History: 259 Added December 5, 2020 (version 9.1) 260 +/ 261 bool wasSuccessful() { 262 return code >= 200 && code < 400; 263 } 264 265 /++ 266 Returns the mime type part of the [contentType] header. 267 268 History: 269 Added July 25, 2022 (version 10.9) 270 +/ 271 string contentTypeMimeType() { 272 auto idx = contentType.indexOf(";"); 273 if(idx == -1) 274 return contentType; 275 276 return contentType[0 .. idx].strip; 277 } 278 279 /// the charset out of content type, if present. `null` if not. 280 string contentTypeCharset() { 281 auto idx = contentType.indexOf("charset="); 282 if(idx == -1) 283 return null; 284 auto c = contentType[idx + "charset=".length .. $].strip; 285 if(c.length) 286 return c; 287 return null; 288 } 289 290 /++ 291 Names and values of cookies set in the response. 292 293 History: 294 Prior to July 5, 2021 (dub v10.2), this was a public field instead of a property. I did 295 not consider this a breaking change since the intended use is completely compatible with the 296 property, and it was not actually implemented properly before anyway. 297 +/ 298 @property string[string] cookies() const { 299 string[string] ret; 300 foreach(cookie; cookiesDetails) 301 ret[cookie.name] = cookie.value; 302 return ret; 303 } 304 /++ 305 The full parsed-out information of cookies set in the response. 306 307 History: 308 Added July 5, 2021 (dub v10.2). 309 +/ 310 @property CookieHeader[] cookiesDetails() inout { 311 CookieHeader[] ret; 312 foreach(header; headers) { 313 if(auto content = header.isHttpHeader("set-cookie")) { 314 // format: name=value, value might be double quoted. it MIGHT be url encoded, but im not going to attempt that since the RFC is silent. 315 // then there's optionally ; attr=value after that. attributes need not have a value 316 317 CookieHeader cookie; 318 319 auto remaining = content; 320 321 cookie_name: 322 foreach(idx, ch; remaining) { 323 if(ch == '=') { 324 cookie.name = remaining[0 .. idx].idup_if_needed; 325 remaining = remaining[idx + 1 .. $]; 326 break; 327 } 328 } 329 330 cookie_value: 331 332 { 333 auto idx = remaining.indexOf(";"); 334 if(idx == -1) { 335 cookie.value = remaining.idup_if_needed; 336 remaining = remaining[$..$]; 337 } else { 338 cookie.value = remaining[0 .. idx].idup_if_needed; 339 remaining = remaining[idx + 1 .. $].stripLeft; 340 } 341 342 if(cookie.value.length > 2 && cookie.value[0] == '"' && cookie.value[$-1] == '"') 343 cookie.value = cookie.value[1 .. $ - 1]; 344 } 345 346 cookie_attributes: 347 348 while(remaining.length) { 349 string name; 350 foreach(idx, ch; remaining) { 351 if(ch == '=') { 352 name = remaining[0 .. idx].idup_if_needed; 353 remaining = remaining[idx + 1 .. $]; 354 355 string value; 356 357 foreach(idx2, ch2; remaining) { 358 if(ch2 == ';') { 359 value = remaining[0 .. idx2].idup_if_needed; 360 remaining = remaining[idx2 + 1 .. $].stripLeft; 361 break; 362 } 363 } 364 365 if(value is null) { 366 value = remaining.idup_if_needed; 367 remaining = remaining[$ .. $]; 368 } 369 370 cookie.attributes[name] = value; 371 continue cookie_attributes; 372 } else if(ch == ';') { 373 name = remaining[0 .. idx].idup_if_needed; 374 remaining = remaining[idx + 1 .. $].stripLeft; 375 cookie.attributes[name] = ""; 376 continue cookie_attributes; 377 } 378 } 379 380 if(remaining.length) { 381 cookie.attributes[remaining.idup_if_needed] = ""; 382 remaining = remaining[$..$]; 383 384 } 385 } 386 387 ret ~= cookie; 388 } 389 } 390 return ret; 391 } 392 393 string[] headers; /// Array of all headers returned. 394 string[string] headersHash; /// 395 396 ubyte[] content; /// The raw content returned in the response body. 397 string contentText; /// [content], but casted to string (for convenience) 398 399 alias responseText = contentText; // just cuz I do this so often. 400 //alias body = content; 401 402 /++ 403 returns `new Document(this.contentText)`. Requires [arsd.dom]. 404 +/ 405 auto contentDom()() { 406 import arsd.dom; 407 return new Document(this.contentText); 408 409 } 410 411 /++ 412 returns `var.fromJson(this.contentText)`. Requires [arsd.jsvar]. 413 +/ 414 auto contentJson()() { 415 import arsd.jsvar; 416 return var.fromJson(this.contentText); 417 } 418 419 HttpRequestParameters requestParameters; /// 420 421 LinkHeader[] linksStored; 422 bool linksLazilyParsed; 423 424 HttpResponse deepCopy() const { 425 HttpResponse h = cast(HttpResponse) this; 426 h.headers = h.headers.dup; 427 h.headersHash = h.headersHash.dup; 428 h.content = h.content.dup; 429 h.linksStored = h.linksStored.dup; 430 return h; 431 } 432 433 /// Returns links header sorted by "rel" attribute. 434 /// It returns a new array on each call. 435 LinkHeader[string] linksHash() { 436 auto links = this.links(); 437 LinkHeader[string] ret; 438 foreach(link; links) 439 ret[link.rel] = link; 440 return ret; 441 } 442 443 /// Returns the Link header, parsed. 444 LinkHeader[] links() { 445 if(linksLazilyParsed) 446 return linksStored; 447 linksLazilyParsed = true; 448 LinkHeader[] ret; 449 450 auto hdrPtr = "link" in headersHash; 451 if(hdrPtr is null) 452 return ret; 453 454 auto header = *hdrPtr; 455 456 LinkHeader current; 457 458 while(header.length) { 459 char ch = header[0]; 460 461 if(ch == '<') { 462 // read url 463 header = header[1 .. $]; 464 size_t idx; 465 while(idx < header.length && header[idx] != '>') 466 idx++; 467 current.url = header[0 .. idx]; 468 header = header[idx .. $]; 469 } else if(ch == ';') { 470 // read attribute 471 header = header[1 .. $]; 472 header = header.stripLeft; 473 474 size_t idx; 475 while(idx < header.length && header[idx] != '=') 476 idx++; 477 478 string name = header[0 .. idx]; 479 if(idx + 1 < header.length) 480 header = header[idx + 1 .. $]; 481 else 482 header = header[$ .. $]; 483 484 string value; 485 486 if(header.length && header[0] == '"') { 487 // quoted value 488 header = header[1 .. $]; 489 idx = 0; 490 while(idx < header.length && header[idx] != '\"') 491 idx++; 492 value = header[0 .. idx]; 493 header = header[idx .. $]; 494 495 } else if(header.length) { 496 // unquoted value 497 idx = 0; 498 while(idx < header.length && header[idx] != ',' && header[idx] != ' ' && header[idx] != ';') 499 idx++; 500 501 value = header[0 .. idx]; 502 header = header[idx .. $].stripLeft; 503 } 504 505 name = name.toLower; 506 if(name == "rel") 507 current.rel = value; 508 else 509 current.attributes[name] = value; 510 511 } else if(ch == ',') { 512 // start another 513 ret ~= current; 514 current = LinkHeader.init; 515 } else if(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') { 516 // ignore 517 } 518 519 if(header.length) 520 header = header[1 .. $]; 521 } 522 523 ret ~= current; 524 525 linksStored = ret; 526 527 return ret; 528 } 529 } 530 531 /+ 532 headerName MUST be all lower case and NOT have the colon on it 533 534 returns slice of the input thing after the header name 535 +/ 536 private inout(char)[] isHttpHeader(inout(char)[] thing, const(char)[] headerName) { 537 foreach(idx, ch; thing) { 538 if(idx < headerName.length) { 539 if(headerName[idx] == '-' && ch != '-') 540 return null; 541 if((ch | ' ') != headerName[idx]) 542 return null; 543 } else if(idx == headerName.length) { 544 if(ch != ':') 545 return null; 546 } else { 547 return thing[idx .. $].strip; 548 } 549 } 550 return null; 551 } 552 553 private string idup_if_needed(string s) { return s; } 554 private string idup_if_needed(const(char)[] s) { return s.idup; } 555 556 unittest { 557 assert("Cookie: foo=bar".isHttpHeader("cookie") == "foo=bar"); 558 assert("cookie: foo=bar".isHttpHeader("cookie") == "foo=bar"); 559 assert("cOOkie: foo=bar".isHttpHeader("cookie") == "foo=bar"); 560 assert("Set-Cookie: foo=bar".isHttpHeader("set-cookie") == "foo=bar"); 561 assert(!"".isHttpHeader("cookie")); 562 } 563 564 /// 565 struct LinkHeader { 566 string url; /// 567 string rel; /// 568 string[string] attributes; /// like title, rev, media, whatever attributes 569 } 570 571 /++ 572 History: 573 Added July 5, 2021 574 +/ 575 struct CookieHeader { 576 string name; 577 string value; 578 string[string] attributes; 579 580 // max-age 581 // expires 582 // httponly 583 // secure 584 // samesite 585 // path 586 // domain 587 // partitioned ? 588 589 // also want cookiejar features here with settings to save session cookies or not 590 591 // storing in file: http://kb.mozillazine.org/Cookies.txt (second arg in practice true if first arg starts with . it seems) 592 // or better yet sqlite: http://kb.mozillazine.org/Cookies.sqlite 593 // should be able to import/export from either upon request 594 } 595 596 import std.string; 597 static import std.algorithm; 598 import std.conv; 599 import std.range; 600 601 602 private AddressFamily family(string unixSocketPath) { 603 if(unixSocketPath.length) 604 return AddressFamily.UNIX; 605 else // FIXME: what about ipv6? 606 return AddressFamily.INET; 607 } 608 609 version(Windows) 610 private class UnixAddress : Address { 611 this(string) { 612 throw new Exception("No unix address support on this system in lib yet :("); 613 } 614 override sockaddr* name() { assert(0); } 615 override const(sockaddr)* name() const { assert(0); } 616 override int nameLen() const { assert(0); } 617 } 618 619 /* 620 void main(string args[]) { 621 write(post("http://arsdnet.net/bugs.php", ["test" : "hey", "again" : "what"])); 622 } 623 */ 624 625 /// 626 struct BasicAuth { 627 string username; /// 628 string password; /// 629 } 630 631 class ProxyException : Exception { 632 this(string msg) {super(msg); } 633 } 634 635 /** 636 Represents a HTTP request. You usually create these through a [HttpClient]. 637 638 639 --- 640 auto request = new HttpRequest(); // note that when there's no associated client, some features may not work 641 // normally you'd instead do `new HttpClient(); client.request(...)` 642 // set any properties here 643 644 // synchronous usage 645 auto reply = request.perform(); 646 647 // async usage, type 1: 648 request.send(); 649 request2.send(); 650 651 // wait until the first one is done, with the second one still in-flight 652 auto response = request.waitForCompletion(); 653 654 // async usage, type 2: 655 request.onDataReceived = (HttpRequest hr) { 656 if(hr.state == HttpRequest.State.complete) { 657 // use hr.responseData 658 } 659 }; 660 request.send(); // send, using the callback 661 662 // before terminating, be sure you wait for your requests to finish! 663 664 request.waitForCompletion(); 665 --- 666 */ 667 class HttpRequest { 668 669 /// Automatically follow a redirection? 670 bool followLocation = false; 671 672 /++ 673 Maximum number of redirections to follow (used only if [followLocation] is set to true). Will resolve with an error if a single request has more than this number of redirections. The default value is currently 10, but may change without notice. If you need a specific value, be sure to call this function. 674 675 If you want unlimited redirects, call it with `int.max`. If you set it to 0 but set [followLocation] to `true`, any attempt at redirection will abort the request. To disable automatically following redirection, set [followLocation] to `false` so you can process the 30x code yourself as a completed request. 676 677 History: 678 Added July 27, 2022 (dub v10.9) 679 +/ 680 void setMaximumNumberOfRedirects(int max = 10) { 681 maximumNumberOfRedirectsRemaining = max; 682 } 683 684 private int maximumNumberOfRedirectsRemaining; 685 686 /++ 687 Set to `true` to automatically retain cookies in the associated [HttpClient] from this request. 688 Note that you must have constructed the request from a `HttpClient` or at least passed one into the 689 constructor for this to have any effect. 690 691 Bugs: 692 See [HttpClient.retainCookies] for important caveats. 693 694 History: 695 Added July 5, 2021 (dub v10.2) 696 +/ 697 bool retainCookies = false; 698 699 private HttpClient client; 700 701 this() { 702 } 703 704 /// 705 this(HttpClient client, Uri where, HttpVerb method, ICache cache = null, Duration timeout = 10.seconds, string proxy = null) { 706 this.client = client; 707 populateFromInfo(where, method); 708 setTimeout(timeout); 709 this.cache = cache; 710 this.proxy = proxy; 711 712 setMaximumNumberOfRedirects(); 713 } 714 715 716 /// ditto 717 this(Uri where, HttpVerb method, ICache cache = null, Duration timeout = 10.seconds, string proxy = null) { 718 this(null, where, method, cache, timeout, proxy); 719 } 720 721 /++ 722 Adds the given header to the request, without checking for duplicates or similar. 723 724 History: 725 Added October 8, 2025. Previously, you'd have to do `request.requestParameters.headers ~= "Name: Value"` (which is exactly what this does, but less conveniently). 726 +/ 727 void addHeader(string name, string value) { 728 this.requestParameters.headers ~= name ~ ": " ~ value; 729 } 730 731 /++ 732 Sets the timeout from inactivity on the request. This is the amount of time that passes with no send or receive activity on the request before it fails with "request timed out" error. 733 734 History: 735 Added March 31, 2021 736 +/ 737 void setTimeout(Duration timeout) { 738 this.requestParameters.timeoutFromInactivity = timeout; 739 this.timeoutFromInactivity = MonoTime.currTime + this.requestParameters.timeoutFromInactivity; 740 } 741 742 /++ 743 Set to `true` to gzip the request body when sending to the server. This is often not supported, and thus turned off 744 by default. 745 746 747 If a server doesn't support this, you MAY get an http error or it might just do the wrong thing. 748 By spec, it is supposed to be code "415 Unsupported Media Type", but there's no guarantee they 749 will do that correctly since many servers will simply have never considered this possibility. Request 750 compression is quite rare, so before using this, ensure your server supports it by checking its documentation 751 or asking its administrator. (Or running a test, but remember, it might just do the wrong thing and not issue 752 an appropriate error, or the config may change in the future.) 753 754 History: 755 Added August 6, 2024 (dub v11.5) 756 +/ 757 void gzipBody(bool want) { 758 this.requestParameters.gzipBody = want; 759 } 760 761 private MonoTime timeoutFromInactivity; 762 763 private Uri where; 764 765 private ICache cache; 766 767 /++ 768 Proxy to use for this request. It should be a URL or `null`. 769 770 This must be sent before you call [send]. 771 772 History: 773 Added April 12, 2021 (dub v9.5) 774 +/ 775 string proxy; 776 777 /++ 778 For https connections, if this is `true`, it will fail to connect if the TLS certificate can not be 779 verified. Setting this to `false` will skip this check and allow the connection to continue anyway. 780 781 When the [HttpRequest] is constructed from a [HttpClient], it will inherit the value from the client 782 instead of using the `= true` here. You can change this value any time before you call [send] (which 783 is done implicitly if you call [waitForCompletion]). 784 785 History: 786 Added April 5, 2022 (dub v10.8) 787 788 Prior to this, it always used the global (but undocumented) `defaultVerifyPeer` setting, and sometimes 789 even if it was true, it would skip the verification. Now, it always respects this local setting. 790 +/ 791 bool verifyPeer = true; 792 793 794 /// Final url after any redirections 795 string finalUrl; 796 797 void populateFromInfo(Uri where, HttpVerb method) { 798 auto parts = where.basedOn(this.where); 799 this.where = parts; 800 finalUrl = where.toString(); 801 requestParameters.method = method; 802 requestParameters.unixSocketPath = where.unixSocketPath; 803 requestParameters.host = parts.host; 804 requestParameters.port = cast(ushort) parts.effectivePort; 805 requestParameters.ssl = parts.scheme == "https"; 806 requestParameters.uri = parts.path.length ? parts.path : "/"; 807 if(parts.query.length) { 808 requestParameters.uri ~= "?"; 809 requestParameters.uri ~= parts.query; 810 } 811 } 812 813 ~this() { 814 } 815 816 ubyte[] sendBuffer; 817 818 HttpResponse responseData; 819 private HttpClient parentClient; 820 821 size_t bodyBytesSent; 822 size_t bodyBytesReceived; 823 824 State state_; 825 final State state() { return state_; } 826 final State state(State s) { 827 assert(state_ != State.complete); 828 return state_ = s; 829 } 830 /// Called when data is received. Check the state to see what data is available. 831 void delegate(HttpRequest) onDataReceived; 832 833 enum State { 834 /// The request has not yet been sent 835 unsent, 836 837 /// The send() method has been called, but no data is 838 /// sent on the socket yet because the connection is busy. 839 pendingAvailableConnection, 840 841 /// connect has been called, but we're waiting on word of success 842 connecting, 843 844 /// connecting a ssl, needing this 845 sslConnectPendingRead, 846 /// ditto 847 sslConnectPendingWrite, 848 849 /// The headers are being sent now 850 sendingHeaders, 851 852 // FIXME: allow Expect: 100-continue and separate the body send 853 854 /// The body is being sent now 855 sendingBody, 856 857 /// The request has been sent but we haven't received any response yet 858 waitingForResponse, 859 860 /// We have received some data and are currently receiving headers 861 readingHeaders, 862 863 /// All headers are available but we're still waiting on the body 864 readingBody, 865 866 /// The request is complete. 867 complete, 868 869 /// The request is aborted, either by the abort() method, or as a result of the server disconnecting 870 aborted 871 } 872 873 /// Sends now and waits for the request to finish, returning the response. 874 HttpResponse perform() { 875 send(); 876 return waitForCompletion(); 877 } 878 879 /// Sends the request asynchronously. 880 void send() { 881 sendPrivate(true); 882 } 883 884 private void sendPrivate(bool advance) { 885 if(state != State.unsent && state != State.aborted) 886 return; // already sent 887 888 if(cache !is null) { 889 auto res = cache.getCachedResponse(this.requestParameters); 890 if(res !is null) { 891 state = State.complete; 892 responseData = (*res).deepCopy(); 893 return; 894 } 895 } 896 897 if(this.where.scheme == "data") { 898 void error(string content) { 899 responseData.code = 400; 900 responseData.codeText = "Bad Request"; 901 responseData.contentType = "text/plain"; 902 responseData.content = cast(ubyte[]) content; 903 responseData.contentText = content; 904 state = State.complete; 905 return; 906 } 907 908 auto thing = this.where.path; 909 // format is: type,data 910 // type can have ;base64 911 auto comma = thing.indexOf(","); 912 if(comma == -1) 913 return error("Invalid data uri, no comma found"); 914 915 auto type = thing[0 .. comma]; 916 auto data = thing[comma + 1 .. $]; 917 if(type.length == 0) 918 type = "text/plain"; 919 920 auto bdata = cast(ubyte[]) decodeUriComponent(data); 921 922 if(type.indexOf(";base64") != -1) { 923 import std.base64; 924 try { 925 bdata = Base64.decode(bdata); 926 } catch(Exception e) { 927 return error(e.msg); 928 } 929 } 930 931 responseData.code = 200; 932 responseData.codeText = "OK"; 933 responseData.contentType = type; 934 responseData.content = bdata; 935 responseData.contentText = cast(string) responseData.content; 936 state = State.complete; 937 return; 938 } 939 940 string headers; 941 942 headers ~= to!string(requestParameters.method); 943 headers ~= " "; 944 if(proxy.length && !requestParameters.ssl) { 945 // if we're doing a http proxy, we need to send a complete, absolute uri 946 // so reconstruct it 947 headers ~= "http://"; 948 headers ~= requestParameters.host; 949 if(requestParameters.port != 80) { 950 headers ~= ":"; 951 headers ~= to!string(requestParameters.port); 952 } 953 } 954 955 headers ~= requestParameters.uri; 956 957 if(requestParameters.useHttp11) 958 headers ~= " HTTP/1.1\r\n"; 959 else 960 headers ~= " HTTP/1.0\r\n"; 961 962 // the whole authority section is supposed to be there, but curl doesn't send if default port 963 // so I'll copy what they do 964 headers ~= "Host: "; 965 headers ~= requestParameters.host; 966 if(requestParameters.port != 80 && requestParameters.port != 443) { 967 headers ~= ":"; 968 headers ~= to!string(requestParameters.port); 969 } 970 headers ~= "\r\n"; 971 972 bool specSaysRequestAlwaysHasBody = 973 requestParameters.method == HttpVerb.POST || 974 requestParameters.method == HttpVerb.PUT || 975 requestParameters.method == HttpVerb.PATCH; 976 977 if(requestParameters.userAgent.length) 978 headers ~= "User-Agent: "~requestParameters.userAgent~"\r\n"; 979 if(requestParameters.contentType.length) 980 headers ~= "Content-Type: "~requestParameters.contentType~"\r\n"; 981 if(requestParameters.authorization.length) 982 headers ~= "Authorization: "~requestParameters.authorization~"\r\n"; 983 if(requestParameters.bodyData.length || specSaysRequestAlwaysHasBody) 984 headers ~= "Content-Length: "~to!string(requestParameters.bodyData.length)~"\r\n"; 985 if(requestParameters.acceptGzip) 986 headers ~= "Accept-Encoding: gzip\r\n"; 987 if(requestParameters.keepAlive) 988 headers ~= "Connection: keep-alive\r\n"; 989 990 string cookieHeader; 991 foreach(name, value; requestParameters.cookies) { 992 if(cookieHeader is null) 993 cookieHeader = "Cookie: "; 994 else 995 cookieHeader ~= "; "; 996 cookieHeader ~= name; 997 cookieHeader ~= "="; 998 cookieHeader ~= value; 999 } 1000 1001 if(cookieHeader !is null) { 1002 cookieHeader ~= "\r\n"; 1003 headers ~= cookieHeader; 1004 } 1005 1006 foreach(header; requestParameters.headers) 1007 headers ~= header ~ "\r\n"; 1008 1009 const(ubyte)[] bodyToSend = requestParameters.bodyData; 1010 if(requestParameters.gzipBody) { 1011 headers ~= "Content-Encoding: gzip\r\n"; 1012 auto c = new Compress(HeaderFormat.gzip); 1013 1014 auto data = c.compress(bodyToSend); 1015 data ~= c.flush(); 1016 bodyToSend = cast(ubyte[]) data; 1017 } 1018 1019 headers ~= "\r\n"; 1020 1021 // FIXME: separate this for 100 continue 1022 sendBuffer = cast(ubyte[]) headers ~ bodyToSend; 1023 1024 // import std.stdio; writeln("******* ", cast(string) sendBuffer); 1025 1026 responseData = HttpResponse.init; 1027 responseData.requestParameters = requestParameters; 1028 bodyBytesSent = 0; 1029 bodyBytesReceived = 0; 1030 state = State.pendingAvailableConnection; 1031 1032 bool alreadyPending = false; 1033 foreach(req; pending) 1034 if(req is this) { 1035 alreadyPending = true; 1036 break; 1037 } 1038 if(!alreadyPending) { 1039 pending ~= this; 1040 } 1041 1042 if(advance) 1043 HttpRequest.advanceConnections(0.seconds);//requestParameters.timeoutFromInactivity); // doing async so no block here 1044 } 1045 1046 1047 /// Waits for the request to finish or timeout, whichever comes first. 1048 HttpResponse waitForCompletion() { 1049 while(state != State.aborted && state != State.complete) { 1050 if(state == State.unsent) { 1051 send(); 1052 continue; 1053 } 1054 if(auto err = HttpRequest.advanceConnections(requestParameters.timeoutFromInactivity)) { 1055 switch(err) { 1056 case 1: throw new Exception("HttpRequest.advanceConnections returned 1: all connections timed out"); 1057 case 2: throw new Exception("HttpRequest.advanceConnections returned 2: nothing to do"); 1058 case 3: continue; // EINTR 1059 default: throw new Exception("HttpRequest.advanceConnections got err " ~ to!string(err)); 1060 } 1061 } 1062 } 1063 1064 if(state == State.complete && responseData.code >= 200) 1065 if(cache !is null) 1066 cache.cacheResponse(this.requestParameters, this.responseData); 1067 1068 return responseData; 1069 } 1070 1071 /// Aborts this request. 1072 void abort() { 1073 this.state = State.aborted; 1074 this.responseData.code = 1; 1075 this.responseData.codeText = "request.abort called"; 1076 // the actual cancellation happens in the event loop 1077 } 1078 1079 HttpRequestParameters requestParameters; /// 1080 1081 version(arsd_http_winhttp_implementation) { 1082 public static void resetInternals() { 1083 1084 } 1085 1086 static assert(0, "implementation not finished"); 1087 } 1088 1089 1090 version(arsd_http_internal_implementation) { 1091 1092 /++ 1093 Changes the limit of number of open, inactive sockets. Reusing connections can provide a significant 1094 performance improvement, but the operating system can also impose a global limit on the number of open 1095 sockets and/or files that you don't want to run into. This lets you choose a balance right for you. 1096 1097 1098 When the total number of cached, inactive sockets approaches this maximum, it will check for ones closed by the 1099 server first. If there are none already closed by the server, it will select sockets at random from its connection 1100 cache and close them to make room for the new ones. 1101 1102 Please note: 1103 1104 $(LIST 1105 * there is always a limit of six open sockets per domain, per the common practice suggested by the http standard 1106 * the limit given here is thread-local. If you run multiple http clients/requests from multiple threads, don't set this too high or you might bump into the global limit from the OS. 1107 * setting this too low can waste connections because the server might close them, but they will never be garbage collected since my current implementation won't check for dead connections except when it thinks it is running close to the limit. 1108 ) 1109 1110 Setting it just right for your use case may provide an up to 10x performance boost. 1111 1112 This implementation is subject to change. If it does, I'll document it, but may not bump the version number. 1113 1114 History: 1115 Added August 10, 2022 (dub v10.9) 1116 +/ 1117 static void setConnectionCacheSize(int max = 32) { 1118 connectionCacheSize = max; 1119 } 1120 1121 private static { 1122 // we manage the actual connections. When a request is made on a particular 1123 // host, we try to reuse connections. We may open more than one connection per 1124 // host to do parallel requests. 1125 // 1126 // The key is the *domain name* and the port. Multiple domains on the same address will have separate connections. 1127 Socket[][string] socketsPerHost; 1128 1129 // only one request can be active on a given socket (at least HTTP < 2.0) so this is that 1130 HttpRequest[Socket] activeRequestOnSocket; 1131 HttpRequest[] pending; // and these are the requests that are waiting 1132 1133 int cachedSockets; 1134 int connectionCacheSize = 32; 1135 1136 /+ 1137 This is a somewhat expensive, but essential operation. If it isn't used in a heavy 1138 application, you'll risk running out of file descriptors. 1139 +/ 1140 void cleanOldSockets() { 1141 static struct CloseCandidate { 1142 string key; 1143 Socket socket; 1144 } 1145 1146 CloseCandidate[36] closeCandidates; 1147 int closeCandidatesPosition; 1148 1149 outer: foreach(key, sockets; socketsPerHost) { 1150 foreach(socket; sockets) { 1151 if(socket in activeRequestOnSocket) 1152 continue; // it is still in use; we can't close it 1153 1154 closeCandidates[closeCandidatesPosition++] = CloseCandidate(key, socket); 1155 if(closeCandidatesPosition == closeCandidates.length) 1156 break outer; 1157 } 1158 } 1159 1160 auto cc = closeCandidates[0 .. closeCandidatesPosition]; 1161 1162 if(cc.length == 0) 1163 return; // no candidates to even examine 1164 1165 // has the server closed any of these? if so, we also close and drop them 1166 static SocketSet readSet = null; 1167 if(readSet is null) 1168 readSet = new SocketSet(); 1169 readSet.reset(); 1170 1171 foreach(candidate; cc) { 1172 readSet.add(candidate.socket); 1173 } 1174 1175 int closeCount; 1176 1177 auto got = Socket.select(readSet, null, null, 0.msecs /* timeout, want it small since we just checking for eof */); 1178 if(got > 0) { 1179 foreach(ref candidate; cc) { 1180 if(readSet.isSet(candidate.socket)) { 1181 // if we can read when it isn't in use, that means eof; the 1182 // server closed it. 1183 candidate.socket.close(); 1184 loseSocketByKey(candidate.key, candidate.socket); 1185 closeCount++; 1186 } 1187 } 1188 debug(arsd_http2) writeln(closeCount, " from inactivity"); 1189 } else { 1190 // and if not, of the remaining ones, close a few just at random to bring us back beneath the arbitrary limit. 1191 1192 while(cc.length > 0 && (cachedSockets - closeCount) > connectionCacheSize) { 1193 import std.random; 1194 auto idx = uniform(0, cc.length); 1195 1196 cc[idx].socket.close(); 1197 loseSocketByKey(cc[idx].key, cc[idx].socket); 1198 1199 cc[idx] = cc[$ - 1]; 1200 cc = cc[0 .. $-1]; 1201 closeCount++; 1202 } 1203 debug(arsd_http2) writeln(closeCount, " from randomness"); 1204 } 1205 1206 cachedSockets -= closeCount; 1207 } 1208 1209 void loseSocketByKey(string key, Socket s) { 1210 if(auto list = key in socketsPerHost) { 1211 for(int a = 0; a < (*list).length; a++) { 1212 if((*list)[a] is s) { 1213 1214 for(int b = a; b < (*list).length - 1; b++) 1215 (*list)[b] = (*list)[b+1]; 1216 (*list) = (*list)[0 .. $-1]; 1217 break; 1218 } 1219 } 1220 } 1221 } 1222 1223 void loseSocket(string host, ushort port, bool ssl, Socket s) { 1224 import std.string; 1225 auto key = format("http%s://%s:%s", ssl ? "s" : "", host, port); 1226 1227 loseSocketByKey(key, s); 1228 } 1229 1230 Socket getOpenSocketOnHost(string proxy, string host, ushort port, bool ssl, string unixSocketPath, bool verifyPeer) { 1231 Socket openNewConnection() { 1232 Socket socket; 1233 if(ssl) { 1234 version(with_openssl) { 1235 loadOpenSsl(); 1236 socket = new SslClientSocket(family(unixSocketPath), SocketType.STREAM, host, verifyPeer); 1237 socket.blocking = false; 1238 } else 1239 throw new Exception("SSL not compiled in"); 1240 } else { 1241 socket = new Socket(family(unixSocketPath), SocketType.STREAM); 1242 socket.blocking = false; 1243 } 1244 1245 socket.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 1246 1247 // FIXME: connect timeout? 1248 if(unixSocketPath) { 1249 import std.stdio; writeln(cast(ubyte[]) unixSocketPath); 1250 socket.connect(new UnixAddress(unixSocketPath)); 1251 } else { 1252 // FIXME: i should prolly do ipv6 if available too. 1253 if(host.length == 0) // this could arguably also be an in contract since it is user error, but the exception is good enough 1254 throw new Exception("No host given for request"); 1255 if(proxy.length) { 1256 if(proxy.indexOf("//") == -1) 1257 proxy = "http://" ~ proxy; 1258 auto proxyurl = Uri(proxy); 1259 1260 //auto proxyhttps = proxyurl.scheme == "https"; 1261 enum proxyhttps = false; // this isn't properly implemented and might never be necessary anyway so meh 1262 1263 // the precise types here are important to help with overload 1264 // resolution of the devirtualized call! 1265 Address pa = new InternetAddress(proxyurl.host, proxyurl.port ? cast(ushort) proxyurl.port : 80); 1266 1267 debug(arsd_http2) writeln("using proxy ", pa.toString()); 1268 1269 if(proxyhttps) { 1270 socket.connect(pa); 1271 } else { 1272 // the proxy never actually starts TLS, but if the request is tls then we need to CONNECT then upgrade the connection 1273 // using the parent class functions let us bypass the encryption 1274 socket.Socket.connect(pa); 1275 } 1276 1277 socket.blocking = true; // FIXME total hack to simplify the code here since it isn't really using the event loop yet 1278 1279 string message; 1280 if(ssl) { 1281 auto hostName = host ~ ":" ~ to!string(port); 1282 message = "CONNECT " ~ hostName ~ " HTTP/1.1\r\n"; 1283 message ~= "Host: " ~ hostName ~ "\r\n"; 1284 if(proxyurl.userinfo.length) { 1285 import std.base64; 1286 message ~= "Proxy-Authorization: Basic " ~ Base64.encode(cast(ubyte[]) proxyurl.userinfo) ~ "\r\n"; 1287 } 1288 message ~= "\r\n"; 1289 1290 // FIXME: what if proxy times out? should be reasonably fast too. 1291 if(proxyhttps) { 1292 socket.send(message, SocketFlags.NONE); 1293 } else { 1294 socket.Socket.send(message, SocketFlags.NONE); 1295 } 1296 1297 ubyte[1024] recvBuffer; 1298 // and last time 1299 ptrdiff_t rcvGot; 1300 if(proxyhttps) { 1301 rcvGot = socket.receive(recvBuffer[], SocketFlags.NONE); 1302 // bool verifyPeer = true; 1303 //(cast(OpenSslSocket)socket).freeSsl(); 1304 //(cast(OpenSslSocket)socket).initSsl(verifyPeer, host); 1305 } else { 1306 rcvGot = socket.Socket.receive(recvBuffer[], SocketFlags.NONE); 1307 } 1308 1309 if(rcvGot == -1) 1310 throw new ProxyException("proxy receive error"); 1311 auto got = cast(string) recvBuffer[0 .. rcvGot]; 1312 auto expect = "HTTP/1.1 200"; 1313 if(got.length < expect.length || (got[0 .. expect.length] != expect && got[0 .. expect.length] != "HTTP/1.0 200")) 1314 throw new ProxyException("Proxy rejected request: " ~ got[0 .. expect.length <= got.length ? expect.length : got.length]); 1315 1316 if(proxyhttps) { 1317 //(cast(OpenSslSocket)socket).do_ssl_connect(); 1318 } else { 1319 (cast(OpenSslSocket)socket).do_ssl_connect(); 1320 } 1321 } else { 1322 } 1323 } else { 1324 socket.connect(new InternetAddress(host, port)); 1325 } 1326 } 1327 1328 debug(arsd_http2) writeln("opening to ", host, ":", port, " ", cast(void*) socket, " ssl=", ssl); 1329 assert(socket.handle() !is socket_t.init); 1330 return socket; 1331 } 1332 1333 // import std.stdio; writeln(cachedSockets); 1334 if(cachedSockets > connectionCacheSize) 1335 cleanOldSockets(); 1336 1337 import std.string; 1338 auto key = format("http%s://%s:%s", ssl ? "s" : "", host, port); 1339 1340 if(auto hostListing = key in socketsPerHost) { 1341 // try to find an available socket that is already open 1342 foreach(socket; *hostListing) { 1343 if(socket !in activeRequestOnSocket) { 1344 // let's see if it has closed since we last tried 1345 // e.g. a server timeout or something. If so, we need 1346 // to lose this one and immediately open a new one. 1347 static SocketSet readSet = null; 1348 if(readSet is null) 1349 readSet = new SocketSet(); 1350 readSet.reset(); 1351 assert(socket !is null); 1352 assert(socket.handle() !is socket_t.init, socket is null ? "null" : socket.toString()); 1353 readSet.add(socket); 1354 auto got = Socket.select(readSet, null, null, 0.msecs /* timeout, want it small since we just checking for eof */); 1355 if(got > 0) { 1356 // we can read something off this... but there aren't 1357 // any active requests. Assume it is EOF and open a new one 1358 1359 socket.close(); 1360 loseSocket(host, port, ssl, socket); 1361 goto openNew; 1362 } 1363 cachedSockets--; 1364 return socket; 1365 } 1366 } 1367 1368 // if not too many already open, go ahead and do a new one 1369 if((*hostListing).length < 6) { 1370 auto socket = openNewConnection(); 1371 (*hostListing) ~= socket; 1372 return socket; 1373 } else 1374 return null; // too many, you'll have to wait 1375 } 1376 1377 openNew: 1378 1379 auto socket = openNewConnection(); 1380 socketsPerHost[key] ~= socket; 1381 return socket; 1382 } 1383 1384 // stuff used by advanceConnections 1385 SocketSet readSet; 1386 SocketSet writeSet; 1387 private ubyte[] reusableBuffer; 1388 1389 /+ 1390 Generic event loop registration: 1391 1392 handle, operation (read/write), buffer (on posix it *might* be stack if a select loop), timeout (in real time), callback when op completed. 1393 1394 ....basically Windows style. Then it translates internally. 1395 1396 It should tell the thing if the buffer is reused or not 1397 +/ 1398 1399 /++ 1400 This is made public for rudimentary event loop integration, but is still 1401 basically an internal detail. Try not to use it if you have another way. 1402 1403 This does a single iteration of the internal select()-based processing loop. 1404 1405 1406 Future directions: 1407 I want to merge the internal use of [WebSocket.eventLoop] with this; 1408 [advanceConnections] does just one run on the loop, whereas eventLoop 1409 runs it until all connections are closed. But they'd both process both 1410 pending http requests and active websockets. 1411 1412 After that, I want to be able to integrate in other event loops too. 1413 One might be to simply to reactor callbacks, then perhaps Windows overlapped 1414 i/o (that's just going to be tricky to retrofit into the existing select()-based 1415 code). It could then go fiber just by calling the resume function too. 1416 1417 The hard part is ensuring I keep this file stand-alone while offering these 1418 things. 1419 1420 This `advanceConnections` call will probably continue to work now that it is 1421 public, but it may not be wholly compatible with all the future features; you'd 1422 have to pick either the internal event loop or an external one you integrate, but not 1423 mix them. 1424 1425 History: 1426 This has been included in the library since almost day one, but 1427 it was private until April 13, 2021 (dub v9.5). 1428 1429 Params: 1430 maximumTimeout = the maximum time it will wait in select(). It may return much sooner than this if a connection timed out in the mean time. 1431 automaticallyRetryOnInterruption = internally loop on EINTR. 1432 1433 Returns: 1434 1435 0 = no error, work may remain so you should call `advanceConnections` again when you can 1436 1437 1 = passed `maximumTimeout` reached with no work done, yet requests are still in the queue. You may call `advanceConnections` again. 1438 1439 2 = no work to do, no point calling it again unless you've added new requests. Your program may exit if you have nothing to add since it means everything requested is now done. 1440 1441 3 = EINTR occurred on select(), you should check your interrupt flags if you set a signal handler, then call `advanceConnections` again if you aren't exiting. Only occurs if `automaticallyRetryOnInterruption` is set to `false` (the default when it is called externally). 1442 1443 any other value should be considered a non-recoverable error if you want to be forward compatible as I reserve the right to add more values later. 1444 +/ 1445 public int advanceConnections(Duration maximumTimeout = 10.seconds, bool automaticallyRetryOnInterruption = false) { 1446 debug(arsd_http2_verbose) writeln("advancing"); 1447 if(readSet is null) 1448 readSet = new SocketSet(); 1449 if(writeSet is null) 1450 writeSet = new SocketSet(); 1451 1452 if(reusableBuffer is null) 1453 reusableBuffer = new ubyte[](32 * 1024); 1454 ubyte[] buffer = reusableBuffer; 1455 1456 HttpRequest[16] removeFromPending; 1457 size_t removeFromPendingCount = 0; 1458 1459 bool hadAbortedRequest; 1460 1461 // are there pending requests? let's try to send them 1462 foreach(idx, pc; pending) { 1463 if(removeFromPendingCount == removeFromPending.length) 1464 break; 1465 1466 if(pc.state == HttpRequest.State.aborted) { 1467 removeFromPending[removeFromPendingCount++] = pc; 1468 hadAbortedRequest = true; 1469 continue; 1470 } 1471 1472 Socket socket; 1473 1474 try { 1475 socket = getOpenSocketOnHost(pc.proxy, pc.requestParameters.host, pc.requestParameters.port, pc.requestParameters.ssl, pc.requestParameters.unixSocketPath, pc.verifyPeer); 1476 } catch(ProxyException e) { 1477 // connection refused or timed out (I should disambiguate somehow)... 1478 pc.state = HttpRequest.State.aborted; 1479 1480 pc.responseData.code = 2; 1481 pc.responseData.codeText = e.msg ~ " from " ~ pc.proxy; 1482 1483 hadAbortedRequest = true; 1484 1485 removeFromPending[removeFromPendingCount++] = pc; 1486 continue; 1487 1488 } catch(SocketException e) { 1489 // connection refused or timed out (I should disambiguate somehow)... 1490 pc.state = HttpRequest.State.aborted; 1491 1492 pc.responseData.code = 2; 1493 pc.responseData.codeText = pc.proxy.length ? ("connection failed to proxy " ~ pc.proxy) : "connection failed"; 1494 1495 hadAbortedRequest = true; 1496 1497 removeFromPending[removeFromPendingCount++] = pc; 1498 continue; 1499 } catch(Exception e) { 1500 // connection failed due to other user error or SSL (i should disambiguate somehow)... 1501 pc.state = HttpRequest.State.aborted; 1502 1503 pc.responseData.code = 2; 1504 pc.responseData.codeText = e.msg; 1505 1506 hadAbortedRequest = true; 1507 1508 removeFromPending[removeFromPendingCount++] = pc; 1509 continue; 1510 1511 } 1512 1513 if(socket !is null) { 1514 activeRequestOnSocket[socket] = pc; 1515 assert(pc.sendBuffer.length); 1516 pc.state = State.connecting; 1517 1518 removeFromPending[removeFromPendingCount++] = pc; 1519 } 1520 } 1521 1522 import std.algorithm : remove; 1523 foreach(rp; removeFromPending[0 .. removeFromPendingCount]) { 1524 if(rp.onDataReceived) rp.onDataReceived(rp); 1525 pending = pending.remove!((a) => a is rp)(); 1526 } 1527 1528 tryAgain: 1529 1530 Socket[16] inactive; 1531 int inactiveCount = 0; 1532 void killInactives() { 1533 foreach(s; inactive[0 .. inactiveCount]) { 1534 debug(arsd_http2) writeln("removing socket from active list ", cast(void*) s); 1535 activeRequestOnSocket.remove(s); 1536 cachedSockets++; 1537 } 1538 } 1539 1540 1541 readSet.reset(); 1542 writeSet.reset(); 1543 1544 bool hadOne = false; 1545 1546 auto minTimeout = maximumTimeout; 1547 auto now = MonoTime.currTime; 1548 1549 // active requests need to be read or written to 1550 foreach(sock, request; activeRequestOnSocket) { 1551 1552 if(request.state == State.aborted) { 1553 inactive[inactiveCount++] = sock; 1554 sock.close(); 1555 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1556 hadAbortedRequest = true; 1557 if(request.onDataReceived) request.onDataReceived(request); 1558 continue; 1559 } 1560 1561 // check the other sockets just for EOF, if they close, take them out of our list, 1562 // we'll reopen if needed upon request. 1563 readSet.add(sock); 1564 hadOne = true; 1565 1566 Duration timeo; 1567 if(request.timeoutFromInactivity <= now) 1568 timeo = 0.seconds; 1569 else 1570 timeo = request.timeoutFromInactivity - now; 1571 1572 if(timeo < minTimeout) 1573 minTimeout = timeo; 1574 1575 if(request.state == State.connecting || request.state == State.sslConnectPendingWrite || request.state == State.sendingHeaders || request.state == State.sendingBody) { 1576 writeSet.add(sock); 1577 hadOne = true; 1578 } 1579 } 1580 1581 if(!hadOne) { 1582 if(hadAbortedRequest) { 1583 killInactives(); 1584 return 0; // something got aborted, that's progress 1585 } 1586 return 2; // automatic timeout, nothing to do 1587 } 1588 1589 auto selectGot = Socket.select(readSet, writeSet, null, minTimeout); 1590 if(selectGot == 0) { /* timeout */ 1591 now = MonoTime.currTime; 1592 bool anyWorkDone = false; 1593 foreach(sock, request; activeRequestOnSocket) { 1594 1595 if(request.timeoutFromInactivity <= now) { 1596 request.state = HttpRequest.State.aborted; 1597 request.responseData.code = 5; 1598 if(request.state == State.connecting) 1599 request.responseData.codeText = "Connect timed out"; 1600 else 1601 request.responseData.codeText = "Request timed out"; 1602 1603 inactive[inactiveCount++] = sock; 1604 sock.close(); 1605 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1606 anyWorkDone = true; 1607 1608 if(request.onDataReceived) request.onDataReceived(request); 1609 } 1610 } 1611 killInactives(); 1612 return anyWorkDone ? 0 : 1; 1613 // return 1; was an error to time out but now im making it on the individual request 1614 } else if(selectGot == -1) { /* interrupted */ 1615 /* 1616 version(Posix) { 1617 import core.stdc.errno; 1618 if(errno != EINTR) 1619 throw new Exception("select error: " ~ to!string(errno)); 1620 } 1621 */ 1622 if(automaticallyRetryOnInterruption) 1623 goto tryAgain; 1624 else 1625 return 3; 1626 } else { /* ready */ 1627 1628 void sslProceed(HttpRequest request, SslClientSocket s) { 1629 try { 1630 auto code = s.do_ssl_connect(); 1631 switch(code) { 1632 case 0: 1633 request.state = State.sendingHeaders; 1634 break; 1635 case SSL_ERROR_WANT_READ: 1636 request.state = State.sslConnectPendingRead; 1637 break; 1638 case SSL_ERROR_WANT_WRITE: 1639 request.state = State.sslConnectPendingWrite; 1640 break; 1641 default: 1642 assert(0); 1643 } 1644 } catch(Exception e) { 1645 request.state = State.aborted; 1646 1647 request.responseData.code = 2; 1648 request.responseData.codeText = e.msg; 1649 inactive[inactiveCount++] = s; 1650 s.close(); 1651 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, s); 1652 if(request.onDataReceived) request.onDataReceived(request); 1653 } 1654 } 1655 1656 1657 foreach(sock, request; activeRequestOnSocket) { 1658 // always need to try to send first in part because http works that way but 1659 // also because openssl will sometimes leave something ready to read even if we haven't 1660 // sent yet (probably leftover data from the crypto negotiation) and if that happens ssl 1661 // is liable to block forever hogging the connection and not letting it send... 1662 if(request.state == State.connecting) 1663 if(writeSet.isSet(sock) || readSet.isSet(sock)) { 1664 import core.stdc.stdint; 1665 int32_t error; 1666 int retopt = sock.getOption(SocketOptionLevel.SOCKET, SocketOption.ERROR, error); 1667 if(retopt < 0 || error != 0) { 1668 request.state = State.aborted; 1669 1670 request.responseData.code = 2; 1671 try { 1672 request.responseData.codeText = "connection failed - " ~ formatSocketError(error); 1673 } catch(Exception e) { 1674 request.responseData.codeText = "connection failed"; 1675 } 1676 inactive[inactiveCount++] = sock; 1677 sock.close(); 1678 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1679 if(request.onDataReceived) request.onDataReceived(request); 1680 continue; 1681 } else { 1682 if(auto s = cast(SslClientSocket) sock) { 1683 sslProceed(request, s); 1684 continue; 1685 } else { 1686 request.state = State.sendingHeaders; 1687 } 1688 } 1689 } 1690 1691 if(request.state == State.sslConnectPendingRead) 1692 if(readSet.isSet(sock)) { 1693 sslProceed(request, cast(SslClientSocket) sock); 1694 continue; 1695 } 1696 if(request.state == State.sslConnectPendingWrite) 1697 if(writeSet.isSet(sock)) { 1698 sslProceed(request, cast(SslClientSocket) sock); 1699 continue; 1700 } 1701 1702 if(request.state == State.sendingHeaders || request.state == State.sendingBody) 1703 if(writeSet.isSet(sock)) { 1704 request.timeoutFromInactivity = MonoTime.currTime + request.requestParameters.timeoutFromInactivity; 1705 assert(request.sendBuffer.length); 1706 auto sent = sock.send(request.sendBuffer); 1707 debug(arsd_http2_verbose) writeln(cast(void*) sock, "<send>", cast(string) request.sendBuffer, "</send>"); 1708 if(sent <= 0) { 1709 if(wouldHaveBlocked()) 1710 continue; 1711 1712 request.state = State.aborted; 1713 1714 request.responseData.code = 3; 1715 request.responseData.codeText = "send failed to server: " ~ lastSocketError(sock); 1716 inactive[inactiveCount++] = sock; 1717 sock.close(); 1718 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1719 if(request.onDataReceived) request.onDataReceived(request); 1720 continue; 1721 1722 } 1723 request.sendBuffer = request.sendBuffer[sent .. $]; 1724 if(request.sendBuffer.length == 0) { 1725 request.state = State.waitingForResponse; 1726 1727 debug(arsd_http2_verbose) writeln("all sent"); 1728 } 1729 if(request.onDataReceived) request.onDataReceived(request); 1730 } 1731 1732 1733 if(readSet.isSet(sock)) { 1734 keep_going: 1735 request.timeoutFromInactivity = MonoTime.currTime + request.requestParameters.timeoutFromInactivity; 1736 auto got = sock.receive(buffer); 1737 debug(arsd_http2_verbose) { if(got < 0) writeln(lastSocketError(sock)); else writeln("====PACKET ",got,"=====",cast(string)buffer[0 .. got],"===/PACKET==="); } 1738 if(got < 0) { 1739 if(wouldHaveBlocked()) 1740 continue; 1741 debug(arsd_http2) writeln("receive error"); 1742 if(request.state != State.complete) { 1743 request.state = State.aborted; 1744 1745 request.responseData.code = 3; 1746 request.responseData.codeText = "receive error from server: " ~ lastSocketError(sock); 1747 } 1748 inactive[inactiveCount++] = sock; 1749 sock.close(); 1750 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1751 } else if(got == 0) { 1752 // remote side disconnected 1753 debug(arsd_http2) writeln("remote disconnect"); 1754 if(request.state != State.complete) { 1755 request.state = State.aborted; 1756 1757 request.responseData.code = 3; 1758 request.responseData.codeText = "server disconnected"; 1759 } 1760 inactive[inactiveCount++] = sock; 1761 sock.close(); 1762 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1763 } else { 1764 // data available 1765 bool stillAlive; 1766 1767 try { 1768 stillAlive = request.handleIncomingData(buffer[0 .. got]); 1769 /+ 1770 state needs to be set and public 1771 requestData.content/contentText needs to be around 1772 you need to be able to clear the content and keep processing for things like event sources. 1773 also need to be able to abort it. 1774 1775 and btw it should prolly just have evnet source as a pre-packaged thing. 1776 +/ 1777 } catch (Exception e) { 1778 debug(arsd_http2_verbose) { import std.stdio; writeln(e); } 1779 request.state = HttpRequest.State.aborted; 1780 request.responseData.code = 4; 1781 request.responseData.codeText = e.msg; 1782 1783 inactive[inactiveCount++] = sock; 1784 sock.close(); 1785 loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock); 1786 } 1787 1788 if(!stillAlive || request.state == HttpRequest.State.complete || request.state == HttpRequest.State.aborted) { 1789 //import std.stdio; writeln(cast(void*) sock, " ", stillAlive, " ", request.state); 1790 inactive[inactiveCount++] = sock; 1791 // reuse the socket for another pending request, if we can 1792 } 1793 } 1794 1795 if(request.onDataReceived) 1796 request.onDataReceived(request); 1797 1798 version(with_openssl) 1799 if(auto s = cast(SslClientSocket) sock) { 1800 // select doesn't handle the case with stuff 1801 // left in the ssl buffer so i'm checking it separately 1802 if(s.dataPending()) { 1803 goto keep_going; 1804 } 1805 } 1806 } 1807 } 1808 } 1809 1810 killInactives(); 1811 1812 // we've completed a request, are there any more pending connection? if so, send them now 1813 1814 return 0; 1815 } 1816 } 1817 1818 public static void resetInternals() { 1819 socketsPerHost = null; 1820 activeRequestOnSocket = null; 1821 pending = null; 1822 1823 } 1824 1825 struct HeaderReadingState { 1826 bool justSawLf; 1827 bool justSawCr; 1828 bool atStartOfLine = true; 1829 bool readingLineContinuation; 1830 } 1831 HeaderReadingState headerReadingState; 1832 1833 struct BodyReadingState { 1834 bool isGzipped; 1835 bool isDeflated; 1836 1837 bool isChunked; 1838 int chunkedState; 1839 1840 // used for the chunk size if it is chunked 1841 int contentLengthRemaining; 1842 } 1843 BodyReadingState bodyReadingState; 1844 1845 bool closeSocketWhenComplete; 1846 1847 import std.zlib; 1848 UnCompress uncompress; 1849 1850 const(ubyte)[] leftoverDataFromLastTime; 1851 1852 bool handleIncomingData(scope const ubyte[] dataIn) { 1853 bool stillAlive = true; 1854 debug(arsd_http2) writeln("handleIncomingData, state: ", state); 1855 if(state == State.waitingForResponse) { 1856 state = State.readingHeaders; 1857 headerReadingState = HeaderReadingState.init; 1858 bodyReadingState = BodyReadingState.init; 1859 } 1860 1861 const(ubyte)[] data; 1862 if(leftoverDataFromLastTime.length) 1863 data = leftoverDataFromLastTime ~ dataIn[]; 1864 else 1865 data = dataIn[]; 1866 1867 if(state == State.readingHeaders) { 1868 void parseLastHeader() { 1869 assert(responseData.headers.length); 1870 if(responseData.headers.length == 1) { 1871 responseData.statusLine = responseData.headers[0]; 1872 import std.algorithm; 1873 auto parts = responseData.statusLine.splitter(" "); 1874 responseData.httpVersion = parts.front; 1875 parts.popFront(); 1876 if(parts.empty) 1877 throw new Exception("Corrupted response, bad status line"); 1878 responseData.code = to!int(parts.front()); 1879 parts.popFront(); 1880 responseData.codeText = ""; 1881 while(!parts.empty) { 1882 // FIXME: this sucks! 1883 responseData.codeText ~= parts.front(); 1884 parts.popFront(); 1885 if(!parts.empty) 1886 responseData.codeText ~= " "; 1887 } 1888 } else { 1889 // parse the new header 1890 auto header = responseData.headers[$-1]; 1891 1892 auto colon = header.indexOf(":"); 1893 if(colon < 0 || colon >= header.length) 1894 return; 1895 auto name = toLower(header[0 .. colon]); 1896 auto value = header[colon + 1 .. $].strip; // skip colon and strip whitespace 1897 1898 switch(name) { 1899 case "connection": 1900 if(value == "close") 1901 closeSocketWhenComplete = true; 1902 break; 1903 case "content-type": 1904 responseData.contentType = value; 1905 break; 1906 case "location": 1907 responseData.location = value; 1908 break; 1909 case "content-length": 1910 bodyReadingState.contentLengthRemaining = to!int(value); 1911 // preallocate the buffer for a bit of a performance boost 1912 responseData.content.reserve(bodyReadingState.contentLengthRemaining); 1913 break; 1914 case "transfer-encoding": 1915 // note that if it is gzipped, it zips first, then chunks the compressed stream. 1916 // so we should always dechunk first, then feed into the decompressor 1917 if(value == "chunked") 1918 bodyReadingState.isChunked = true; 1919 else throw new Exception("Unknown Transfer-Encoding: " ~ value); 1920 break; 1921 case "content-encoding": 1922 if(value == "gzip") { 1923 bodyReadingState.isGzipped = true; 1924 uncompress = new UnCompress(); 1925 } else if(value == "deflate") { 1926 bodyReadingState.isDeflated = true; 1927 uncompress = new UnCompress(); 1928 } else throw new Exception("Unknown Content-Encoding: " ~ value); 1929 break; 1930 case "set-cookie": 1931 // handled elsewhere fyi 1932 break; 1933 default: 1934 // ignore 1935 } 1936 1937 responseData.headersHash[name] = value; 1938 } 1939 } 1940 1941 size_t position = 0; 1942 for(position = 0; position < data.length; position++) { 1943 if(headerReadingState.readingLineContinuation) { 1944 if(data[position] == ' ' || data[position] == '\t') 1945 continue; 1946 headerReadingState.readingLineContinuation = false; 1947 } 1948 1949 if(headerReadingState.atStartOfLine) { 1950 headerReadingState.atStartOfLine = false; 1951 // FIXME it being \r should never happen... and i don't think it does 1952 if(data[position] == '\r' || data[position] == '\n') { 1953 // done with headers 1954 1955 position++; // skip the \r 1956 1957 if(responseData.headers.length) 1958 parseLastHeader(); 1959 1960 if(responseData.code >= 100 && responseData.code < 200) { 1961 // "100 Continue" - we should continue uploading request data at this point 1962 // "101 Switching Protocols" - websocket, not expected here... 1963 // "102 Processing" - server still working, keep the connection alive 1964 // "103 Early Hints" - can have useful Link headers etc 1965 // 1966 // and other unrecognized ones can just safely be skipped 1967 1968 // FIXME: the headers shouldn't actually be reset; 103 Early Hints 1969 // can give useful headers we want to keep 1970 1971 responseData.headers = null; 1972 headerReadingState.atStartOfLine = true; 1973 1974 continue; // the \n will be skipped by the for loop advance 1975 } 1976 1977 if(this.requestParameters.method == HttpVerb.HEAD) 1978 state = State.complete; 1979 else 1980 state = State.readingBody; 1981 1982 // skip the \n before we break 1983 position++; 1984 1985 break; 1986 } else if(data[position] == ' ' || data[position] == '\t') { 1987 // line continuation, ignore all whitespace and collapse it into a space 1988 headerReadingState.readingLineContinuation = true; 1989 responseData.headers[$-1] ~= ' '; 1990 } else { 1991 // new header 1992 if(responseData.headers.length) 1993 parseLastHeader(); 1994 responseData.headers ~= ""; 1995 } 1996 } 1997 1998 if(data[position] == '\r') { 1999 headerReadingState.justSawCr = true; 2000 continue; 2001 } else 2002 headerReadingState.justSawCr = false; 2003 2004 if(data[position] == '\n') { 2005 headerReadingState.justSawLf = true; 2006 headerReadingState.atStartOfLine = true; 2007 continue; 2008 } else 2009 headerReadingState.justSawLf = false; 2010 2011 responseData.headers[$-1] ~= data[position]; 2012 } 2013 2014 data = data[position .. $]; 2015 } 2016 2017 if(state == State.readingBody) { 2018 if(bodyReadingState.isChunked) { 2019 // read the hex length, stopping at a \r\n, ignoring everything between the new line but after the first non-valid hex character 2020 // read binary data of that length. it is our content 2021 // repeat until a zero sized chunk 2022 // then read footers as headers. 2023 2024 start_over: 2025 for(int a = 0; a < data.length; a++) { 2026 final switch(bodyReadingState.chunkedState) { 2027 case 0: // reading hex 2028 char c = data[a]; 2029 if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { 2030 // just keep reading 2031 } else { 2032 int power = 1; 2033 bodyReadingState.contentLengthRemaining = 0; 2034 if(a == 0) 2035 break; // just wait for more data 2036 assert(a != 0, cast(string) data); 2037 for(int b = a-1; b >= 0; b--) { 2038 char cc = data[b]; 2039 if(cc >= 'a' && cc <= 'z') 2040 cc -= 0x20; 2041 int val = 0; 2042 if(cc >= '0' && cc <= '9') 2043 val = cc - '0'; 2044 else 2045 val = cc - 'A' + 10; 2046 2047 assert(val >= 0 && val <= 15, to!string(val)); 2048 bodyReadingState.contentLengthRemaining += power * val; 2049 power *= 16; 2050 } 2051 debug(arsd_http2_verbose) writeln("Chunk length: ", bodyReadingState.contentLengthRemaining); 2052 bodyReadingState.chunkedState = 1; 2053 data = data[a + 1 .. $]; 2054 goto start_over; 2055 } 2056 break; 2057 case 1: // reading until end of line 2058 char c = data[a]; 2059 if(c == '\n') { 2060 if(bodyReadingState.contentLengthRemaining == 0) 2061 bodyReadingState.chunkedState = 5; 2062 else 2063 bodyReadingState.chunkedState = 2; 2064 } 2065 data = data[a + 1 .. $]; 2066 goto start_over; 2067 case 2: // reading data 2068 auto can = a + bodyReadingState.contentLengthRemaining; 2069 if(can > data.length) 2070 can = cast(int) data.length; 2071 2072 auto newData = data[a .. can]; 2073 data = data[can .. $]; 2074 2075 //if(bodyReadingState.isGzipped || bodyReadingState.isDeflated) 2076 // responseData.content ~= cast(ubyte[]) uncompress.uncompress(data[a .. can]); 2077 //else 2078 responseData.content ~= newData; 2079 2080 bodyReadingState.contentLengthRemaining -= newData.length; 2081 debug(arsd_http2_verbose) writeln("clr: ", bodyReadingState.contentLengthRemaining, " " , a, " ", can); 2082 assert(bodyReadingState.contentLengthRemaining >= 0); 2083 if(bodyReadingState.contentLengthRemaining == 0) { 2084 bodyReadingState.chunkedState = 3; 2085 } else { 2086 // will continue grabbing more 2087 } 2088 goto start_over; 2089 case 3: // reading 13/10 2090 assert(data[a] == 13); 2091 bodyReadingState.chunkedState++; 2092 data = data[a + 1 .. $]; 2093 goto start_over; 2094 case 4: // reading 10 at end of packet 2095 assert(data[a] == 10); 2096 data = data[a + 1 .. $]; 2097 bodyReadingState.chunkedState = 0; 2098 goto start_over; 2099 case 5: // reading footers 2100 //goto done; // FIXME 2101 2102 int footerReadingState = 0; 2103 int footerSize; 2104 2105 while(footerReadingState != 2 && a < data.length) { 2106 // import std.stdio; writeln(footerReadingState, " ", footerSize, " ", data); 2107 switch(footerReadingState) { 2108 case 0: 2109 if(data[a] == 13) 2110 footerReadingState++; 2111 else 2112 footerSize++; 2113 break; 2114 case 1: 2115 if(data[a] == 10) { 2116 if(footerSize == 0) { 2117 // all done, time to break 2118 footerReadingState++; 2119 2120 } else { 2121 // actually had a footer, try to read another 2122 footerReadingState = 0; 2123 footerSize = 0; 2124 } 2125 } else { 2126 throw new Exception("bad footer thing"); 2127 } 2128 break; 2129 default: 2130 assert(0); 2131 } 2132 2133 a++; 2134 } 2135 2136 if(footerReadingState != 2) 2137 break start_over; // haven't hit the end of the thing yet 2138 2139 bodyReadingState.chunkedState = 0; 2140 data = data[a .. $]; 2141 2142 if(bodyReadingState.isGzipped || bodyReadingState.isDeflated) { 2143 auto n = uncompress.uncompress(responseData.content); 2144 n ~= uncompress.flush(); 2145 responseData.content = cast(ubyte[]) n; 2146 } 2147 2148 // responseData.content ~= cast(ubyte[]) uncompress.flush(); 2149 responseData.contentText = cast(string) responseData.content; 2150 2151 goto done; 2152 } 2153 } 2154 2155 } else { 2156 //if(bodyReadingState.isGzipped || bodyReadingState.isDeflated) 2157 // responseData.content ~= cast(ubyte[]) uncompress.uncompress(data); 2158 //else 2159 responseData.content ~= data; 2160 //assert(data.length <= bodyReadingState.contentLengthRemaining, format("%d <= %d\n%s", data.length, bodyReadingState.contentLengthRemaining, cast(string)data)); 2161 { 2162 int use = cast(int) data.length; 2163 if(use > bodyReadingState.contentLengthRemaining) 2164 use = bodyReadingState.contentLengthRemaining; 2165 bodyReadingState.contentLengthRemaining -= use; 2166 data = data[use .. $]; 2167 } 2168 if(bodyReadingState.contentLengthRemaining == 0) { 2169 if(bodyReadingState.isGzipped || bodyReadingState.isDeflated) { 2170 // import std.stdio; writeln(responseData.content.length, " ", responseData.content[0 .. 2], " .. ", responseData.content[$-2 .. $]); 2171 auto n = uncompress.uncompress(responseData.content); 2172 n ~= uncompress.flush(); 2173 responseData.content = cast(ubyte[]) n; 2174 responseData.contentText = cast(string) responseData.content; 2175 //responseData.content ~= cast(ubyte[]) uncompress.flush(); 2176 } else { 2177 responseData.contentText = cast(string) responseData.content; 2178 } 2179 2180 done: 2181 2182 if(retainCookies && client !is null) { 2183 client.retainCookies(responseData); 2184 } 2185 2186 if(followLocation && responseData.location.length) { 2187 if(maximumNumberOfRedirectsRemaining <= 0) { 2188 throw new Exception("Maximum number of redirects exceeded"); 2189 } else { 2190 maximumNumberOfRedirectsRemaining--; 2191 } 2192 2193 static bool first = true; 2194 //version(DigitalMars) if(!first) asm { int 3; } 2195 debug(arsd_http2) writeln("redirecting to ", responseData.location); 2196 populateFromInfo(Uri(responseData.location), HttpVerb.GET); 2197 //import std.stdio; writeln("redirected to ", responseData.location); 2198 first = false; 2199 responseData = HttpResponse.init; 2200 headerReadingState = HeaderReadingState.init; 2201 bodyReadingState = BodyReadingState.init; 2202 if(client !is null) { 2203 // FIXME: this won't clear cookies that were cleared in another request 2204 client.populateCookies(this); // they might have changed in the previous redirection cycle! 2205 } 2206 state = State.unsent; 2207 stillAlive = false; 2208 sendPrivate(false); 2209 } else { 2210 state = State.complete; 2211 // FIXME 2212 //if(closeSocketWhenComplete) 2213 //socket.close(); 2214 } 2215 } 2216 } 2217 } 2218 2219 if(data.length) 2220 leftoverDataFromLastTime = data.dup; 2221 else 2222 leftoverDataFromLastTime = null; 2223 2224 return stillAlive; 2225 } 2226 2227 } 2228 } 2229 2230 /++ 2231 Waits for the first of the given requests to be either aborted or completed. 2232 Returns the first one in that state, or `null` if the operation was interrupted 2233 or reached the given timeout before any completed. (If it returns null even before 2234 the timeout, it might be because the user pressed ctrl+c, so you should consider 2235 checking if you should cancel the operation. If not, you can simply call it again 2236 with the same arguments to start waiting again.) 2237 2238 You MUST check for null, even if you don't specify a timeout! 2239 2240 Note that if an individual request times out before any others request, it will 2241 return that timed out request, since that counts as completion. 2242 2243 If the return is not null, you should call `waitForCompletion` on the given request 2244 to get the response out. It will not have to wait since it is guaranteed to be 2245 finished when returned by this function; that will just give you the cached response. 2246 2247 (I thought about just having it return the response, but tying a response back to 2248 a request is harder than just getting the original request object back and taking 2249 the response out of it.) 2250 2251 Please note: if a request in the set has already completed or been aborted, it will 2252 always return the first one it sees upon calling the function. You may wish to remove 2253 them from the list before calling the function. 2254 2255 History: 2256 Added December 24, 2021 (dub v10.5) 2257 +/ 2258 HttpRequest waitForFirstToComplete(Duration timeout, HttpRequest[] requests...) { 2259 2260 foreach(request; requests) { 2261 if(request.state == HttpRequest.State.unsent) 2262 request.send(); 2263 else if(request.state == HttpRequest.State.complete) 2264 return request; 2265 else if(request.state == HttpRequest.State.aborted) 2266 return request; 2267 } 2268 2269 while(true) { 2270 if(auto err = HttpRequest.advanceConnections(timeout)) { 2271 switch(err) { 2272 case 1: return null; 2273 case 2: throw new Exception("HttpRequest.advanceConnections returned 2: nothing to do"); 2274 case 3: return null; 2275 default: throw new Exception("HttpRequest.advanceConnections got err " ~ to!string(err)); 2276 } 2277 } 2278 2279 foreach(request; requests) { 2280 if(request.state == HttpRequest.State.aborted || request.state == HttpRequest.State.complete) { 2281 request.waitForCompletion(); 2282 return request; 2283 } 2284 } 2285 2286 } 2287 } 2288 2289 /// ditto 2290 HttpRequest waitForFirstToComplete(HttpRequest[] requests...) { 2291 return waitForFirstToComplete(1.weeks, requests); 2292 } 2293 2294 /++ 2295 An input range that runs [waitForFirstToComplete] but only returning each request once. 2296 Before you loop over it, you can set some properties to customize behavior. 2297 2298 If it times out or is interrupted, it will prematurely run empty. You can set the delegate 2299 to process this. 2300 2301 Implementation note: each iteration through the loop does a O(n) check over each item remaining. 2302 This shouldn't matter, but if it does become an issue for you, let me know. 2303 2304 History: 2305 Added December 24, 2021 (dub v10.5) 2306 +/ 2307 struct HttpRequestsAsTheyComplete { 2308 /++ 2309 Seeds it with an overall timeout and the initial requests. 2310 It will send all the requests before returning, then will process 2311 the responses as they come. 2312 2313 Please note that it modifies the array of requests you pass in! It 2314 will keep a reference to it and reorder items on each call of popFront. 2315 You might want to pass a duplicate if you have another purpose for your 2316 array and don't want to see it shuffled. 2317 +/ 2318 this(Duration timeout, HttpRequest[] requests) { 2319 remainingRequests = requests; 2320 this.timeout = timeout; 2321 popFront(); 2322 } 2323 2324 /++ 2325 You can set this delegate to decide how to handle an interruption. Returning true 2326 from this will keep working. Returning false will terminate the loop. 2327 2328 If this is null, an interruption will always terminate the loop. 2329 2330 Note that interruptions can be caused by the garbage collector being triggered by 2331 another thread as well as by user action. If you don't set a SIGINT handler, it 2332 might be reasonable to always return true here. 2333 +/ 2334 bool delegate() onInterruption; 2335 2336 private HttpRequest[] remainingRequests; 2337 2338 /// The timeout you set in the constructor. You can change it if you want. 2339 Duration timeout; 2340 2341 /++ 2342 Adds another request to the work queue. It is safe to call this from inside the loop 2343 as you process other requests. 2344 +/ 2345 void appendRequest(HttpRequest request) { 2346 remainingRequests ~= request; 2347 } 2348 2349 /++ 2350 If the loop exited, it might be due to an interruption or a time out. If you like, you 2351 can call this to pick up the work again, 2352 2353 If it returns `false`, the work is indeed all finished and you should not re-enter the loop. 2354 2355 --- 2356 auto range = HttpRequestsAsTheyComplete(10.seconds, your_requests); 2357 process_loop: foreach(req; range) { 2358 // process req 2359 } 2360 // make sure we weren't interrupted because the user requested we cancel! 2361 // but then try to re-enter the range if possible 2362 if(!user_quit && range.reenter()) { 2363 // there's still something unprocessed in there 2364 // range.reenter returning true means it is no longer 2365 // empty, so we should try to loop over it again 2366 goto process_loop; // re-enter the loop 2367 } 2368 --- 2369 +/ 2370 bool reenter() { 2371 if(remainingRequests.length == 0) 2372 return false; 2373 empty = false; 2374 popFront(); 2375 return true; 2376 } 2377 2378 /// Standard range primitives. I reserve the right to change the variables to read-only properties in the future without notice. 2379 HttpRequest front; 2380 2381 /// ditto 2382 bool empty; 2383 2384 /// ditto 2385 void popFront() { 2386 resume: 2387 if(remainingRequests.length == 0) { 2388 empty = true; 2389 return; 2390 } 2391 2392 front = waitForFirstToComplete(timeout, remainingRequests); 2393 2394 if(front is null) { 2395 if(onInterruption) { 2396 if(onInterruption()) 2397 goto resume; 2398 } 2399 empty = true; 2400 return; 2401 } 2402 foreach(idx, req; remainingRequests) { 2403 if(req is front) { 2404 remainingRequests[idx] = remainingRequests[$ - 1]; 2405 remainingRequests = remainingRequests[0 .. $ - 1]; 2406 return; 2407 } 2408 } 2409 } 2410 } 2411 2412 // 2413 struct HttpRequestParameters { 2414 // FIXME: implement these 2415 //Duration timeoutTotal; // the whole request must finish in this time or else it fails,even if data is still trickling in 2416 Duration timeoutFromInactivity; // if there's no activity in this time it dies. basically the socket receive timeout 2417 2418 // debugging 2419 bool useHttp11 = true; /// 2420 bool acceptGzip = true; /// 2421 bool keepAlive = true; /// 2422 2423 // the request itself 2424 HttpVerb method; /// 2425 string host; /// 2426 ushort port; /// 2427 string uri; /// 2428 2429 bool ssl; /// 2430 2431 string userAgent; /// 2432 string authorization; /// 2433 2434 string[string] cookies; /// 2435 2436 string[] headers; /// do not duplicate host, content-length, content-type, or any others that have a specific property 2437 2438 string contentType; /// 2439 ubyte[] bodyData; /// 2440 2441 string unixSocketPath; /// 2442 2443 bool gzipBody; /// 2444 } 2445 2446 interface IHttpClient { 2447 2448 } 2449 2450 /// 2451 enum HttpVerb { 2452 /// 2453 GET, 2454 /// 2455 HEAD, 2456 /// 2457 POST, 2458 /// 2459 PUT, 2460 /// 2461 DELETE, 2462 /// 2463 OPTIONS, 2464 /// 2465 TRACE, 2466 /// 2467 CONNECT, 2468 /// 2469 PATCH, 2470 /// 2471 MERGE 2472 } 2473 2474 /++ 2475 Supported file formats for [HttpClient.setClientCert]. These are loaded by OpenSSL 2476 in the current implementation. 2477 2478 History: 2479 Added February 3, 2022 (dub v10.6) 2480 +/ 2481 enum CertificateFileFormat { 2482 guess, /// try to guess the format from the file name and/or contents 2483 pem, /// the files are specifically in PEM format 2484 der /// the files are specifically in DER format 2485 } 2486 2487 /++ 2488 HttpClient keeps cookies, location, and some other state to reuse connections, when possible, like a web browser. 2489 You can use it as your entry point to make http requests. 2490 2491 See the example on [arsd.http2#examples]. 2492 +/ 2493 class HttpClient { 2494 /* Protocol restrictions, useful to disable when debugging servers */ 2495 bool useHttp11 = true; /// 2496 bool acceptGzip = true; /// 2497 bool keepAlive = true; /// 2498 2499 /++ 2500 Sets the client certificate used as a log in identifier on https connections. 2501 The certificate and key must be unencrypted at this time and both must be in 2502 the same file format. 2503 2504 Bugs: 2505 The current implementation sets the filenames into a static variable, 2506 meaning it is shared across all clients and connections. 2507 2508 Errors in the cert or key are only reported if the server reports an 2509 authentication failure. Make sure you are passing correct filenames 2510 and formats of you do see a failure. 2511 2512 History: 2513 Added February 2, 2022 (dub v10.6) 2514 +/ 2515 void setClientCertificate(string certFilename, string keyFilename, CertificateFileFormat certFormat = CertificateFileFormat.guess) { 2516 this.certFilename = certFilename; 2517 this.keyFilename = keyFilename; 2518 this.certFormat = certFormat; 2519 } 2520 2521 /++ 2522 Sets whether [HttpRequest]s created through this object (with [navigateTo], [request], etc.), will have the 2523 value of [HttpRequest.verifyPeer] of true or false upon construction. 2524 2525 History: 2526 Added April 5, 2022 (dub v10.8). Previously, there was an undocumented global value used. 2527 +/ 2528 bool defaultVerifyPeer = true; 2529 2530 /++ 2531 Adds a header to be automatically appended to each request created through this client. 2532 2533 If you add duplicate headers, it will add multiple copies. 2534 2535 You should NOT use this to add headers that can be set through other properties like [userAgent], [authorization], or [setCookie]. 2536 2537 History: 2538 Added July 12, 2023 2539 +/ 2540 void addDefaultHeader(string key, string value) { 2541 defaultHeaders ~= key ~ ": " ~ value; 2542 } 2543 2544 private string[] defaultHeaders; 2545 2546 // FIXME: getCookies api 2547 // FIXME: an easy way to download files 2548 2549 // FIXME: try to not make these static 2550 private static string certFilename; 2551 private static string keyFilename; 2552 private static CertificateFileFormat certFormat; 2553 2554 /// 2555 @property Uri location() { 2556 return currentUrl; 2557 } 2558 2559 /++ 2560 Default timeout for requests created on this client. 2561 2562 History: 2563 Added March 31, 2021 2564 +/ 2565 Duration defaultTimeout = 10.seconds; 2566 2567 /++ 2568 High level function that works similarly to entering a url 2569 into a browser. 2570 2571 Follows locations, retain cookies, updates the current url, etc. 2572 +/ 2573 HttpRequest navigateTo(Uri where, HttpVerb method = HttpVerb.GET) { 2574 currentUrl = where.basedOn(currentUrl); 2575 currentDomain = where.host; 2576 2577 auto request = this.request(currentUrl, method); 2578 request.followLocation = true; 2579 request.retainCookies = true; 2580 2581 return request; 2582 } 2583 2584 /++ 2585 Creates a request without updating the current url state. If you want to save cookies, either call [retainCookies] with the response yourself 2586 or set [HttpRequest.retainCookies|request.retainCookies] to `true` on the returned object. But see important implementation shortcomings on [retainCookies]. 2587 2588 To upload files, you can use the [FormData] overload. 2589 +/ 2590 HttpRequest request(Uri uri, HttpVerb method = HttpVerb.GET, ubyte[] bodyData = null, string contentType = null) { 2591 string proxyToUse = getProxyFor(uri); 2592 2593 auto request = new HttpRequest(this, uri, method, cache, defaultTimeout, proxyToUse); 2594 2595 request.verifyPeer = this.defaultVerifyPeer; 2596 2597 request.requestParameters.userAgent = userAgent; 2598 request.requestParameters.authorization = authorization; 2599 2600 request.requestParameters.useHttp11 = this.useHttp11; 2601 request.requestParameters.acceptGzip = this.acceptGzip; 2602 request.requestParameters.keepAlive = this.keepAlive; 2603 2604 request.requestParameters.bodyData = bodyData; 2605 request.requestParameters.contentType = contentType; 2606 2607 request.requestParameters.headers = this.defaultHeaders; 2608 2609 populateCookies(request); 2610 2611 return request; 2612 } 2613 2614 /// ditto 2615 HttpRequest request(Uri uri, FormData fd, HttpVerb method = HttpVerb.POST) { 2616 return request(uri, method, fd.toBytes, fd.contentType); 2617 } 2618 2619 2620 private void populateCookies(HttpRequest request) { 2621 // FIXME: what about expiration and the like? or domain/path checks? or Secure checks? 2622 // FIXME: is uri.host correct? i think it should include port number too. what fun. 2623 if(auto cookies = ""/*uri.host*/ in this.cookies) { 2624 foreach(cookie; *cookies) 2625 request.requestParameters.cookies[cookie.name] = cookie.value; 2626 } 2627 } 2628 2629 private Uri currentUrl; 2630 private string currentDomain; 2631 private ICache cache; 2632 2633 /++ 2634 2635 +/ 2636 this(ICache cache = null) { 2637 this.defaultVerifyPeer = .defaultVerifyPeer_; 2638 this.cache = cache; 2639 loadDefaultProxy(); 2640 } 2641 2642 /++ 2643 Loads the system-default proxy. Note that the constructor does this automatically 2644 so you should rarely need to call this explicitly. 2645 2646 The environment variables are used, if present, on all operating systems. 2647 2648 History: 2649 no_proxy support added April 13, 2022 2650 2651 Added April 12, 2021 (included in dub v9.5) 2652 2653 Bugs: 2654 On Windows, it does NOT currently check the IE settings, but I do intend to 2655 implement that in the future. When I do, it will be classified as a bug fix, 2656 NOT a breaking change. 2657 +/ 2658 void loadDefaultProxy() { 2659 import std.process; 2660 httpProxy = environment.get("http_proxy", environment.get("HTTP_PROXY", null)); 2661 httpsProxy = environment.get("https_proxy", environment.get("HTTPS_PROXY", null)); 2662 auto noProxy = environment.get("no_proxy", environment.get("NO_PROXY", null)); 2663 if (noProxy.length) { 2664 proxyIgnore = noProxy.split(","); 2665 foreach (ref rule; proxyIgnore) 2666 rule = rule.strip; 2667 } 2668 2669 // FIXME: on Windows, I should use the Internet Explorer proxy settings 2670 } 2671 2672 /++ 2673 Checks if the given uri should be proxied according to the httpProxy, httpsProxy, proxyIgnore 2674 variables and returns either httpProxy, httpsProxy or null. 2675 2676 If neither `httpProxy` or `httpsProxy` are set this always returns `null`. Same if `proxyIgnore` 2677 contains `*`. 2678 2679 DNS is not resolved for proxyIgnore IPs, only IPs match IPs and hosts match hosts. 2680 +/ 2681 string getProxyFor(Uri uri) { 2682 string proxyToUse; 2683 switch(uri.scheme) { 2684 case "http": 2685 proxyToUse = httpProxy; 2686 break; 2687 case "https": 2688 proxyToUse = httpsProxy; 2689 break; 2690 default: 2691 proxyToUse = null; 2692 } 2693 2694 if (proxyToUse.length) { 2695 foreach (ignore; proxyIgnore) { 2696 if (matchProxyIgnore(ignore, uri)) { 2697 return null; 2698 } 2699 } 2700 } 2701 2702 return proxyToUse; 2703 } 2704 2705 /// Returns -1 on error, otherwise the IP as uint. Parsing is very strict. 2706 private static long tryParseIPv4(scope const(char)[] s) nothrow { 2707 import std.algorithm : findSplit, all; 2708 import std.ascii : isDigit; 2709 2710 static int parseNum(scope const(char)[] num) nothrow { 2711 if (num.length < 1 || num.length > 3 || !num.representation.all!isDigit) 2712 return -1; 2713 try { 2714 auto ret = num.to!int; 2715 return ret > 255 ? -1 : ret; 2716 } catch (Exception) { 2717 assert(false); 2718 } 2719 } 2720 2721 if (s.length < "0.0.0.0".length || s.length > "255.255.255.255".length) 2722 return -1; 2723 auto firstPair = s.findSplit("."); 2724 auto secondPair = firstPair[2].findSplit("."); 2725 auto thirdPair = secondPair[2].findSplit("."); 2726 auto a = parseNum(firstPair[0]); 2727 auto b = parseNum(secondPair[0]); 2728 auto c = parseNum(thirdPair[0]); 2729 auto d = parseNum(thirdPair[2]); 2730 if (a < 0 || b < 0 || c < 0 || d < 0) 2731 return -1; 2732 return (cast(uint)a << 24) | (b << 16) | (c << 8) | (d); 2733 } 2734 2735 unittest { 2736 assert(tryParseIPv4("0.0.0.0") == 0); 2737 assert(tryParseIPv4("127.0.0.1") == 0x7f000001); 2738 assert(tryParseIPv4("162.217.114.56") == 0xa2d97238); 2739 assert(tryParseIPv4("256.0.0.1") == -1); 2740 assert(tryParseIPv4("0.0.0.-2") == -1); 2741 assert(tryParseIPv4("0.0.0.a") == -1); 2742 assert(tryParseIPv4("0.0.0") == -1); 2743 assert(tryParseIPv4("0.0.0.0.0") == -1); 2744 } 2745 2746 /++ 2747 Returns true if the given no_proxy rule matches the uri. 2748 2749 Invalid IP ranges are silently ignored and return false. 2750 2751 See $(LREF proxyIgnore). 2752 +/ 2753 static bool matchProxyIgnore(scope const(char)[] rule, scope const Uri uri) nothrow { 2754 import std.algorithm; 2755 import std.ascii : isDigit; 2756 import std.uni : sicmp; 2757 2758 string uriHost = uri.host; 2759 if (uriHost.length && uriHost[$ - 1] == '.') 2760 uriHost = uriHost[0 .. $ - 1]; 2761 2762 if (rule == "*") 2763 return true; 2764 while (rule.length && rule[0] == '.') rule = rule[1 .. $]; 2765 2766 static int parsePort(scope const(char)[] portStr) nothrow { 2767 if (portStr.length < 1 || portStr.length > 5 || !portStr.representation.all!isDigit) 2768 return -1; 2769 try { 2770 return portStr.to!int; 2771 } catch (Exception) { 2772 assert(false, "to!int should succeed"); 2773 } 2774 } 2775 2776 if (sicmp(rule, uriHost) == 0 2777 || (uriHost.length > rule.length 2778 && sicmp(rule, uriHost[$ - rule.length .. $]) == 0 2779 && uriHost[$ - rule.length - 1] == '.')) 2780 return true; 2781 2782 if (rule.startsWith("[")) { // IPv6 2783 // below code is basically nothrow lastIndexOfAny("]:") 2784 ptrdiff_t lastColon = cast(ptrdiff_t) rule.length - 1; 2785 while (lastColon >= 0) { 2786 if (rule[lastColon] == ']' || rule[lastColon] == ':') 2787 break; 2788 lastColon--; 2789 } 2790 if (lastColon == -1) 2791 return false; // malformed 2792 2793 if (rule[lastColon] == ':') { // match with port 2794 auto port = parsePort(rule[lastColon + 1 .. $]); 2795 if (port != -1) { 2796 if (uri.effectivePort != port.to!int) 2797 return false; 2798 return uriHost == rule[0 .. lastColon]; 2799 } 2800 } 2801 // exact match of host already done above 2802 } else { 2803 auto slash = rule.lastIndexOfNothrow('/'); 2804 if (slash == -1) { // no IP range 2805 auto colon = rule.lastIndexOfNothrow(':'); 2806 auto host = colon == -1 ? rule : rule[0 .. colon]; 2807 auto port = colon != -1 ? parsePort(rule[colon + 1 .. $]) : -1; 2808 auto ip = tryParseIPv4(host); 2809 if (ip == -1) { // not an IPv4, test for host with port 2810 return port != -1 2811 && uri.effectivePort == port 2812 && uriHost == host; 2813 } else { 2814 // perform IPv4 equals 2815 auto other = tryParseIPv4(uriHost); 2816 if (other == -1) 2817 return false; // rule == IPv4, uri != IPv4 2818 if (port != -1) 2819 return uri.effectivePort == port 2820 && uriHost == host; 2821 else 2822 return uriHost == host; 2823 } 2824 } else { 2825 auto maskStr = rule[slash + 1 .. $]; 2826 auto ip = tryParseIPv4(rule[0 .. slash]); 2827 if (ip == -1) 2828 return false; 2829 if (maskStr.length && maskStr.length < 3 && maskStr.representation.all!isDigit) { 2830 // IPv4 range match 2831 int mask; 2832 try { 2833 mask = maskStr.to!int; 2834 } catch (Exception) { 2835 assert(false); 2836 } 2837 2838 auto other = tryParseIPv4(uriHost); 2839 if (other == -1) 2840 return false; // rule == IPv4, uri != IPv4 2841 2842 if (mask == 0) // matches all 2843 return true; 2844 if (mask > 32) // matches none 2845 return false; 2846 2847 auto shift = 32 - mask; 2848 return cast(uint)other >> shift 2849 == cast(uint)ip >> shift; 2850 } 2851 } 2852 } 2853 return false; 2854 } 2855 2856 unittest { 2857 assert(matchProxyIgnore("0.0.0.0/0", Uri("http://127.0.0.1:80/a"))); 2858 assert(matchProxyIgnore("0.0.0.0/0", Uri("http://127.0.0.1/a"))); 2859 assert(!matchProxyIgnore("0.0.0.0/0", Uri("https://dlang.org/a"))); 2860 assert(matchProxyIgnore("*", Uri("https://dlang.org/a"))); 2861 assert(matchProxyIgnore("127.0.0.0/8", Uri("http://127.0.0.1:80/a"))); 2862 assert(matchProxyIgnore("127.0.0.0/8", Uri("http://127.0.0.1/a"))); 2863 assert(matchProxyIgnore("127.0.0.1", Uri("http://127.0.0.1:1234/a"))); 2864 assert(!matchProxyIgnore("127.0.0.1:80", Uri("http://127.0.0.1:1234/a"))); 2865 assert(!matchProxyIgnore("127.0.0.1/8", Uri("http://localhost/a"))); // no DNS resolution / guessing 2866 assert(!matchProxyIgnore("0.0.0.0/1", Uri("http://localhost/a")) 2867 && !matchProxyIgnore("128.0.0.0/1", Uri("http://localhost/a"))); // no DNS resolution / guessing 2 2868 foreach (m; 1 .. 32) { 2869 assert(matchProxyIgnore(text("127.0.0.1/", m), Uri("http://127.0.0.1/a"))); 2870 assert(!matchProxyIgnore(text("127.0.0.1/", m), Uri("http://128.0.0.1/a"))); 2871 bool expectedMatch = m <= 24; 2872 assert(expectedMatch == matchProxyIgnore(text("127.0.1.0/", m), Uri("http://127.0.1.128/a")), m.to!string); 2873 } 2874 assert(matchProxyIgnore("localhost", Uri("http://localhost/a"))); 2875 assert(matchProxyIgnore("localhost", Uri("http://foo.localhost/a"))); 2876 assert(matchProxyIgnore("localhost", Uri("http://foo.localhost./a"))); 2877 assert(matchProxyIgnore(".localhost", Uri("http://localhost/a"))); 2878 assert(matchProxyIgnore(".localhost", Uri("http://foo.localhost/a"))); 2879 assert(matchProxyIgnore(".localhost", Uri("http://foo.localhost./a"))); 2880 assert(!matchProxyIgnore("foo.localhost", Uri("http://localhost/a"))); 2881 assert(matchProxyIgnore("foo.localhost", Uri("http://foo.localhost/a"))); 2882 assert(matchProxyIgnore("foo.localhost", Uri("http://foo.localhost./a"))); 2883 assert(!matchProxyIgnore("bar.localhost", Uri("http://localhost/a"))); 2884 assert(!matchProxyIgnore("bar.localhost", Uri("http://foo.localhost/a"))); 2885 assert(!matchProxyIgnore("bar.localhost", Uri("http://foo.localhost./a"))); 2886 assert(!matchProxyIgnore("bar.localhost", Uri("http://bbar.localhost./a"))); 2887 assert(matchProxyIgnore("[::1]", Uri("http://[::1]/a"))); 2888 assert(!matchProxyIgnore("[::1]", Uri("http://[::2]/a"))); 2889 assert(matchProxyIgnore("[::1]:80", Uri("http://[::1]/a"))); 2890 assert(!matchProxyIgnore("[::1]:443", Uri("http://[::1]/a"))); 2891 assert(!matchProxyIgnore("[::1]:80", Uri("https://[::1]/a"))); 2892 assert(matchProxyIgnore("[::1]:443", Uri("https://[::1]/a"))); 2893 assert(matchProxyIgnore("google.com", Uri("https://GOOGLE.COM/a"))); 2894 } 2895 2896 /++ 2897 Proxies to use for requests. The [HttpClient] constructor will set these to the system values, 2898 then you can reset it to `null` if you want to override and not use the proxy after all, or you 2899 can set it after construction to whatever. 2900 2901 The proxy from the client will be automatically set to the requests performed through it. You can 2902 also override on a per-request basis by creating the request and setting the `proxy` field there 2903 before sending it. 2904 2905 History: 2906 Added April 12, 2021 (included in dub v9.5) 2907 +/ 2908 string httpProxy; 2909 /// ditto 2910 string httpsProxy; 2911 /++ 2912 List of hosts or ips, optionally including a port, where not to proxy. 2913 2914 Each entry may be one of the following formats: 2915 - `127.0.0.1` (IPv4, any port) 2916 - `127.0.0.1:1234` (IPv4, specific port) 2917 - `127.0.0.1/8` (IPv4 range / CIDR block, any port) 2918 - `[::1]` (IPv6, any port) 2919 - `[::1]:1234` (IPv6, specific port) 2920 - `*` (all hosts and ports, basically don't proxy at all anymore) 2921 - `.domain.name`, `domain.name` (don't proxy the specified domain, 2922 leading dots are stripped and subdomains are also not proxied) 2923 - `.domain.name:1234`, `domain.name:1234` (same as above, with specific port) 2924 2925 No DNS resolution or regex is done in this list. 2926 2927 See https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ 2928 2929 History: 2930 Added April 13, 2022 2931 +/ 2932 string[] proxyIgnore; 2933 2934 /// See [retainCookies] for important caveats. 2935 void setCookie(string name, string value, string domain = null) { 2936 CookieHeader ch; 2937 2938 ch.name = name; 2939 ch.value = value; 2940 2941 setCookie(ch, domain); 2942 } 2943 2944 /// ditto 2945 void setCookie(CookieHeader ch, string domain = null) { 2946 if(domain is null) 2947 domain = currentDomain; 2948 2949 // FIXME: figure all this out or else cookies liable to get too long, in addition to the overwriting and oversharing issues in long scraping sessions 2950 cookies[""/*domain*/] ~= ch; 2951 } 2952 2953 /++ 2954 [HttpClient] does NOT automatically store cookies. You must explicitly retain them from a response by calling this method. 2955 2956 Examples: 2957 --- 2958 import arsd.http2; 2959 void main() { 2960 auto client = new HttpClient(); 2961 auto setRequest = client.request(Uri("http://arsdnet.net/cgi-bin/cookies/set")); 2962 auto setResponse = setRequest.waitForCompletion(); 2963 2964 auto request = client.request(Uri("http://arsdnet.net/cgi-bin/cookies/get")); 2965 auto response = request.waitForCompletion(); 2966 2967 // the cookie wasn't explicitly retained, so the server echos back nothing 2968 assert(response.responseText.length == 0); 2969 2970 // now keep the cookies from our original set 2971 client.retainCookies(setResponse); 2972 2973 request = client.request(Uri("http://arsdnet.net/cgi-bin/cookies/get")); 2974 response = request.waitForCompletion(); 2975 2976 // now it matches 2977 assert(response.responseText.length && response.responseText == setResponse.cookies["example-cookie"]); 2978 } 2979 --- 2980 2981 Bugs: 2982 It does NOT currently implement domain / path / secure separation nor cookie expiration. It assumes that if you call this function, you're ok with it. 2983 2984 You may want to use separate HttpClient instances if any sharing is unacceptable at this time. 2985 2986 History: 2987 Added July 5, 2021 (dub v10.2) 2988 +/ 2989 void retainCookies(HttpResponse fromResponse) { 2990 foreach(name, value; fromResponse.cookies) 2991 setCookie(name, value); 2992 } 2993 2994 /// 2995 void clearCookies(string domain = null) { 2996 if(domain is null) 2997 cookies = null; 2998 else 2999 cookies[domain] = null; 3000 } 3001 3002 // If you set these, they will be pre-filled on all requests made with this client 3003 string userAgent = "D arsd.html2"; /// 3004 string authorization; /// 3005 3006 /* inter-request state */ 3007 private CookieHeader[][string] cookies; 3008 } 3009 3010 private ptrdiff_t lastIndexOfNothrow(T)(scope T[] arr, T value) nothrow 3011 { 3012 ptrdiff_t ret = cast(ptrdiff_t)arr.length - 1; 3013 while (ret >= 0) { 3014 if (arr[ret] == value) 3015 return ret; 3016 ret--; 3017 } 3018 return ret; 3019 } 3020 3021 interface ICache { 3022 /++ 3023 The client is about to make the given `request`. It will ALWAYS pass it to the cache object first so you can decide if you want to and can provide a response. You should probably check the appropriate headers to see if you should even attempt to look up on the cache (HttpClient does NOT do this to give maximum flexibility to the cache implementor). 3024 3025 Return null if the cache does not provide. 3026 +/ 3027 const(HttpResponse)* getCachedResponse(HttpRequestParameters request); 3028 3029 /++ 3030 The given request has received the given response. The implementing class needs to decide if it wants to cache or not. Return true if it was added, false if you chose not to. 3031 3032 You may wish to examine headers, etc., in making the decision. The HttpClient will ALWAYS pass a request/response to this. 3033 +/ 3034 bool cacheResponse(HttpRequestParameters request, HttpResponse response); 3035 } 3036 3037 /+ 3038 // / Provides caching behavior similar to a real web browser 3039 class HttpCache : ICache { 3040 const(HttpResponse)* getCachedResponse(HttpRequestParameters request) { 3041 return null; 3042 } 3043 } 3044 3045 // / Gives simple maximum age caching, ignoring the actual http headers 3046 class SimpleCache : ICache { 3047 const(HttpResponse)* getCachedResponse(HttpRequestParameters request) { 3048 return null; 3049 } 3050 } 3051 +/ 3052 3053 /++ 3054 A pseudo-cache to provide a mock server. Construct one of these, 3055 populate it with test responses, and pass it to [HttpClient] to 3056 do a network-free test. 3057 3058 You should populate it with the [populate] method. Any request not 3059 pre-populated will return a "server refused connection" response. 3060 +/ 3061 class HttpMockProvider : ICache { 3062 /+ + 3063 3064 +/ 3065 version(none) 3066 this(Uri baseUrl, string defaultResponseContentType) { 3067 3068 } 3069 3070 this() {} 3071 3072 HttpResponse defaultResponse; 3073 3074 /// Implementation of the ICache interface. Hijacks all requests to return a pre-populated response or "server disconnected". 3075 const(HttpResponse)* getCachedResponse(HttpRequestParameters request) { 3076 import std.conv; 3077 auto defaultPort = request.ssl ? 443 : 80; 3078 string identifier = text( 3079 request.method, " ", 3080 request.ssl ? "https" : "http", "://", 3081 request.host, 3082 (request.port && request.port != defaultPort) ? (":" ~ to!string(request.port)) : "", 3083 request.uri 3084 ); 3085 3086 if(auto res = identifier in population) 3087 return res; 3088 return &defaultResponse; 3089 } 3090 3091 /// Implementation of the ICache interface. We never actually cache anything here since it is all about mock responses, not actually caching real data. 3092 bool cacheResponse(HttpRequestParameters request, HttpResponse response) { 3093 return false; 3094 } 3095 3096 /++ 3097 Convenience method to populate simple responses. For more complex 3098 work, use one of the other overloads where you build complete objects 3099 yourself. 3100 3101 Params: 3102 request = a verb and complete URL to mock as one string. 3103 For example "GET http://example.com/". If you provide only 3104 a partial URL, it will be based on the `baseUrl` you gave 3105 in the `HttpMockProvider` constructor. 3106 3107 responseCode = the HTTP response code, like 200 or 404. 3108 3109 response = the response body as a string. It is assumed 3110 to be of the `defaultResponseContentType` you passed in the 3111 `HttpMockProvider` constructor. 3112 +/ 3113 void populate(string request, int responseCode, string response) { 3114 3115 // FIXME: absolute-ize the URL in the request 3116 3117 HttpResponse r; 3118 r.code = responseCode; 3119 r.codeText = getHttpCodeText(r.code); 3120 3121 r.content = cast(ubyte[]) response; 3122 r.contentText = response; 3123 3124 population[request] = r; 3125 } 3126 3127 version(none) 3128 void populate(string method, string url, HttpResponse response) { 3129 // FIXME 3130 } 3131 3132 private HttpResponse[string] population; 3133 } 3134 3135 // modified from the one in cgi.d to just have the text 3136 private static string getHttpCodeText(int code) pure nothrow @nogc { 3137 switch(code) { 3138 // this module's proprietary extensions 3139 case 0: return null; 3140 case 1: return "request.abort called"; 3141 case 2: return "connection failed"; 3142 case 3: return "server disconnected"; 3143 case 4: return "exception thrown"; // actually should be some other thing 3144 case 5: return "Request timed out"; 3145 3146 // * * * standard ones * * * 3147 3148 // 1xx skipped since they shouldn't happen 3149 3150 // 3151 case 200: return "OK"; 3152 case 201: return "Created"; 3153 case 202: return "Accepted"; 3154 case 203: return "Non-Authoritative Information"; 3155 case 204: return "No Content"; 3156 case 205: return "Reset Content"; 3157 // 3158 case 300: return "Multiple Choices"; 3159 case 301: return "Moved Permanently"; 3160 case 302: return "Found"; 3161 case 303: return "See Other"; 3162 case 307: return "Temporary Redirect"; 3163 case 308: return "Permanent Redirect"; 3164 // 3165 case 400: return "Bad Request"; 3166 case 403: return "Forbidden"; 3167 case 404: return "Not Found"; 3168 case 405: return "Method Not Allowed"; 3169 case 406: return "Not Acceptable"; 3170 case 409: return "Conflict"; 3171 case 410: return "Gone"; 3172 // 3173 case 500: return "Internal Server Error"; 3174 case 501: return "Not Implemented"; 3175 case 502: return "Bad Gateway"; 3176 case 503: return "Service Unavailable"; 3177 // 3178 default: assert(0, "Unsupported http code"); 3179 } 3180 } 3181 3182 3183 /// 3184 struct HttpCookie { 3185 string name; /// 3186 string value; /// 3187 string domain; /// 3188 string path; /// 3189 //SysTime expirationDate; /// 3190 bool secure; /// 3191 bool httpOnly; /// 3192 } 3193 3194 // FIXME: websocket 3195 3196 version(testing) 3197 void main() { 3198 import std.stdio; 3199 auto client = new HttpClient(); 3200 auto request = client.navigateTo(Uri("http://localhost/chunked.php")); 3201 request.send(); 3202 auto request2 = client.navigateTo(Uri("http://dlang.org/")); 3203 request2.send(); 3204 3205 { 3206 auto response = request2.waitForCompletion(); 3207 //write(cast(string) response.content); 3208 } 3209 3210 auto response = request.waitForCompletion(); 3211 write(cast(string) response.content); 3212 3213 writeln(HttpRequest.socketsPerHost); 3214 } 3215 3216 string lastSocketError(Socket sock) { 3217 import std.socket; 3218 version(use_openssl) { 3219 if(auto s = cast(OpenSslSocket) sock) 3220 if(s.lastSocketError.length) 3221 return s.lastSocketError; 3222 } 3223 return std.socket.lastSocketError(); 3224 } 3225 3226 // From sslsocket.d, but this is the maintained version! 3227 version(use_openssl) { 3228 alias SslClientSocket = OpenSslSocket; 3229 3230 // CRL = Certificate Revocation List 3231 static immutable string[] sslErrorCodes = [ 3232 "OK (code 0)", 3233 "Unspecified SSL/TLS error (code 1)", 3234 "Unable to get TLS issuer certificate (code 2)", 3235 "Unable to get TLS CRL (code 3)", 3236 "Unable to decrypt TLS certificate signature (code 4)", 3237 "Unable to decrypt TLS CRL signature (code 5)", 3238 "Unable to decode TLS issuer public key (code 6)", 3239 "TLS certificate signature failure (code 7)", 3240 "TLS CRL signature failure (code 8)", 3241 "TLS certificate not yet valid (code 9)", 3242 "TLS certificate expired (code 10)", 3243 "TLS CRL not yet valid (code 11)", 3244 "TLS CRL expired (code 12)", 3245 "TLS error in certificate not before field (code 13)", 3246 "TLS error in certificate not after field (code 14)", 3247 "TLS error in CRL last update field (code 15)", 3248 "TLS error in CRL next update field (code 16)", 3249 "TLS system out of memory (code 17)", 3250 "TLS certificate is self-signed (code 18)", 3251 "Self-signed certificate in TLS chain (code 19)", 3252 "Unable to get TLS issuer certificate locally (code 20)", 3253 "Unable to verify TLS leaf signature (code 21)", 3254 "TLS certificate chain too long (code 22)", 3255 "TLS certificate was revoked (code 23)", 3256 "TLS CA is invalid (code 24)", 3257 "TLS error: path length exceeded (code 25)", 3258 "TLS error: invalid purpose (code 26)", 3259 "TLS error: certificate untrusted (code 27)", 3260 "TLS error: certificate rejected (code 28)", 3261 ]; 3262 3263 string getOpenSslErrorCode(long error) { 3264 if(error == 62) 3265 return "TLS certificate host name mismatch"; 3266 3267 if(error < 0 || error >= sslErrorCodes.length) 3268 return "SSL/TLS error code " ~ to!string(error); 3269 return sslErrorCodes[cast(size_t) error]; 3270 } 3271 3272 struct SSL; 3273 struct SSL_CTX; 3274 struct SSL_METHOD; 3275 struct X509_STORE_CTX; 3276 enum SSL_VERIFY_NONE = 0; 3277 enum SSL_VERIFY_PEER = 1; 3278 3279 // copy it into the buf[0 .. size] and return actual length you read. 3280 // rwflag == 0 when reading, 1 when writing. 3281 extern(C) alias pem_password_cb = int function(char* buffer, int bufferSize, int rwflag, void* userPointer); 3282 extern(C) alias print_errors_cb = int function(const char*, size_t, void*); 3283 extern(C) alias client_cert_cb = int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey); 3284 extern(C) alias keylog_cb = void function(SSL*, char*); 3285 3286 struct X509; 3287 struct X509_STORE; 3288 struct EVP_PKEY; 3289 struct X509_VERIFY_PARAM; 3290 3291 import core.stdc.config; 3292 3293 enum SSL_ERROR_WANT_READ = 2; 3294 enum SSL_ERROR_WANT_WRITE = 3; 3295 3296 struct ossllib { 3297 __gshared static extern(C) { 3298 /* these are only on older openssl versions { */ 3299 int function() SSL_library_init; 3300 void function() SSL_load_error_strings; 3301 SSL_METHOD* function() SSLv23_client_method; 3302 /* } */ 3303 3304 void function(ulong, void*) OPENSSL_init_ssl; 3305 3306 SSL_CTX* function(const SSL_METHOD*) SSL_CTX_new; 3307 SSL* function(SSL_CTX*) SSL_new; 3308 int function(SSL*, int) SSL_set_fd; 3309 int function(SSL*) SSL_connect; 3310 int function(SSL*, const void*, int) SSL_write; 3311 int function(SSL*, void*, int) SSL_read; 3312 @trusted nothrow @nogc int function(SSL*) SSL_shutdown; 3313 void function(SSL*) @nogc nothrow SSL_free; 3314 void function(SSL_CTX*) @nogc nothrow SSL_CTX_free; 3315 3316 int function(const SSL*) SSL_pending; 3317 int function (const SSL *ssl, int ret) SSL_get_error; 3318 3319 void function(SSL*, int, void*) SSL_set_verify; 3320 3321 void function(SSL*, int, c_long, void*) SSL_ctrl; 3322 3323 SSL_METHOD* function() SSLv3_client_method; 3324 SSL_METHOD* function() TLS_client_method; 3325 3326 void function(SSL_CTX*, void function(SSL*, char* line)) SSL_CTX_set_keylog_callback; 3327 3328 int function(SSL_CTX*) SSL_CTX_set_default_verify_paths; 3329 3330 X509_STORE* function(SSL_CTX*) SSL_CTX_get_cert_store; 3331 c_long function(const SSL* ssl) SSL_get_verify_result; 3332 3333 X509_VERIFY_PARAM* function(const SSL*) SSL_get0_param; 3334 3335 /+ 3336 SSL_CTX_load_verify_locations 3337 SSL_CTX_set_client_CA_list 3338 +/ 3339 3340 // client cert things 3341 void function (SSL_CTX *ctx, int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey)) SSL_CTX_set_client_cert_cb; 3342 } 3343 } 3344 3345 struct eallib { 3346 __gshared static extern(C) { 3347 /* these are only on older openssl versions { */ 3348 void function() OpenSSL_add_all_ciphers; 3349 void function() OpenSSL_add_all_digests; 3350 /* } */ 3351 3352 const(char)* function(int) OpenSSL_version; 3353 3354 void function(ulong, void*) OPENSSL_init_crypto; 3355 3356 void function(print_errors_cb, void*) ERR_print_errors_cb; 3357 3358 void function(X509*) X509_free; 3359 int function(X509_STORE*, X509*) X509_STORE_add_cert; 3360 3361 3362 X509* function(FILE *fp, X509 **x, pem_password_cb *cb, void *u) PEM_read_X509; 3363 EVP_PKEY* function(FILE *fp, EVP_PKEY **x, pem_password_cb *cb, void* userPointer) PEM_read_PrivateKey; 3364 3365 EVP_PKEY* function(FILE *fp, EVP_PKEY **a) d2i_PrivateKey_fp; 3366 X509* function(FILE *fp, X509 **x) d2i_X509_fp; 3367 3368 X509* function(X509** a, const(ubyte*)* pp, c_long length) d2i_X509; 3369 int function(X509* a, ubyte** o) i2d_X509; 3370 3371 int function(X509_VERIFY_PARAM* a, const char* b, size_t l) X509_VERIFY_PARAM_set1_host; 3372 3373 X509* function(X509_STORE_CTX *ctx) X509_STORE_CTX_get_current_cert; 3374 int function(X509_STORE_CTX *ctx) X509_STORE_CTX_get_error; 3375 } 3376 } 3377 3378 struct OpenSSL { 3379 static: 3380 3381 static Error notLoadedError; 3382 static this() { 3383 notLoadedError = new object.Error("will be overwritten"); 3384 } 3385 3386 template opDispatch(string name) { 3387 auto opDispatch(T...)(T t) { 3388 static if(__traits(hasMember, ossllib, name)) { 3389 auto ptr = __traits(getMember, ossllib, name); 3390 } else static if(__traits(hasMember, eallib, name)) { 3391 auto ptr = __traits(getMember, eallib, name); 3392 } else static assert(0); 3393 3394 if(ptr is null) { 3395 notLoadedError.msg = name; 3396 throw notLoadedError;//(name ~ " not loaded"); 3397 } 3398 return ptr(t); 3399 } 3400 } 3401 3402 // macros in the original C 3403 SSL_METHOD* SSLv23_client_method() { 3404 if(ossllib.SSLv23_client_method) 3405 return ossllib.SSLv23_client_method(); 3406 else 3407 return ossllib.TLS_client_method(); 3408 } 3409 3410 void SSL_set_tlsext_host_name(SSL* a, const char* b) { 3411 if(ossllib.SSL_ctrl) 3412 return ossllib.SSL_ctrl(a, 55 /*SSL_CTRL_SET_TLSEXT_HOSTNAME*/, 0 /*TLSEXT_NAMETYPE_host_name*/, cast(void*) b); 3413 else throw new Exception("SSL_set_tlsext_host_name not loaded"); 3414 } 3415 3416 // special case 3417 @trusted nothrow @nogc int SSL_shutdown(SSL* a) { 3418 if(ossllib.SSL_shutdown) 3419 return ossllib.SSL_shutdown(a); 3420 assert(0); 3421 } 3422 3423 void SSL_CTX_keylog_cb_func(SSL_CTX* ctx, keylog_cb func) { 3424 // this isn't in openssl 1.0 and is non-essential, so it is allowed to fail. 3425 if(ossllib.SSL_CTX_set_keylog_callback) 3426 ossllib.SSL_CTX_set_keylog_callback(ctx, func); 3427 //else throw new Exception("SSL_CTX_keylog_cb_func not loaded"); 3428 } 3429 3430 } 3431 3432 extern(C) 3433 int collectSslErrors(const char* ptr, size_t len, void* user) @trusted { 3434 string* s = cast(string*) user; 3435 3436 (*s) ~= ptr[0 .. len]; 3437 3438 return 0; 3439 } 3440 3441 3442 private __gshared void* ossllib_handle; 3443 version(Windows) 3444 private __gshared void* oeaylib_handle; 3445 else 3446 alias oeaylib_handle = ossllib_handle; 3447 version(Posix) 3448 private import core.sys.posix.dlfcn; 3449 else version(Windows) 3450 private import core.sys.windows.windows; 3451 3452 import core.stdc.stdio; 3453 import arsd.core : SynchronizableObject; 3454 3455 private __gshared SynchronizableObject loadSslMutex = new SynchronizableObject; 3456 private __gshared bool sslLoaded = false; 3457 3458 void loadOpenSsl() { 3459 if(sslLoaded) 3460 return; 3461 synchronized(loadSslMutex) { 3462 3463 version(Posix) { 3464 version(OSX) { 3465 static immutable string[] ossllibs = [ 3466 "libssl.46.dylib", 3467 "libssl.44.dylib", 3468 "libssl.43.dylib", 3469 "libssl.35.dylib", 3470 "libssl.1.1.dylib", 3471 "libssl.dylib", 3472 "/usr/local/opt/openssl/lib/libssl.1.0.0.dylib", 3473 ]; 3474 } else { 3475 static immutable string[] ossllibs = [ 3476 "libssl.so.3", 3477 "libssl.so.1.1", 3478 "libssl.so.1.0.2", 3479 "libssl.so.1.0.1", 3480 "libssl.so.1.0.0", 3481 "libssl.so", 3482 ]; 3483 } 3484 3485 foreach(lib; ossllibs) { 3486 ossllib_handle = dlopen(lib.ptr, RTLD_NOW); 3487 if(ossllib_handle !is null) break; 3488 } 3489 } else version(Windows) { 3490 version(X86_64) { 3491 ossllib_handle = LoadLibraryW("libssl-1_1-x64.dll"w.ptr); 3492 oeaylib_handle = LoadLibraryW("libcrypto-1_1-x64.dll"w.ptr); 3493 } 3494 3495 static immutable wstring[] ossllibs = [ 3496 "libssl-3-x64.dll"w, 3497 "libssl-3.dll"w, 3498 "libssl-1_1.dll"w, 3499 "libssl32.dll"w, 3500 ]; 3501 3502 if(ossllib_handle is null) 3503 foreach(lib; ossllibs) { 3504 ossllib_handle = LoadLibraryW(lib.ptr); 3505 if(ossllib_handle !is null) break; 3506 } 3507 3508 static immutable wstring[] eaylibs = [ 3509 "libcrypto-3-x64.dll"w, 3510 "libcrypto-3.dll"w, 3511 "libcrypto-1_1.dll"w, 3512 "libeay32.dll", 3513 ]; 3514 3515 if(oeaylib_handle is null) 3516 foreach(lib; eaylibs) { 3517 oeaylib_handle = LoadLibraryW(lib.ptr); 3518 if (oeaylib_handle !is null) break; 3519 } 3520 3521 if(ossllib_handle is null) { 3522 ossllib_handle = LoadLibraryW("ssleay32.dll"w.ptr); 3523 oeaylib_handle = ossllib_handle; 3524 } 3525 } 3526 3527 if(ossllib_handle is null) 3528 throw new Exception("libssl library not found"); 3529 if(oeaylib_handle is null) 3530 throw new Exception("libeay32 library not found"); 3531 3532 foreach(memberName; __traits(allMembers, ossllib)) { 3533 alias t = typeof(__traits(getMember, ossllib, memberName)); 3534 version(Posix) 3535 __traits(getMember, ossllib, memberName) = cast(t) dlsym(ossllib_handle, memberName); 3536 else version(Windows) { 3537 __traits(getMember, ossllib, memberName) = cast(t) GetProcAddress(ossllib_handle, memberName); 3538 } 3539 } 3540 3541 foreach(memberName; __traits(allMembers, eallib)) { 3542 alias t = typeof(__traits(getMember, eallib, memberName)); 3543 version(Posix) 3544 __traits(getMember, eallib, memberName) = cast(t) dlsym(oeaylib_handle, memberName); 3545 else version(Windows) { 3546 __traits(getMember, eallib, memberName) = cast(t) GetProcAddress(oeaylib_handle, memberName); 3547 } 3548 } 3549 3550 3551 if(ossllib.SSL_library_init) 3552 ossllib.SSL_library_init(); 3553 else if(ossllib.OPENSSL_init_ssl) 3554 ossllib.OPENSSL_init_ssl(0, null); 3555 else throw new Exception("couldn't init openssl"); 3556 3557 if(eallib.OpenSSL_add_all_ciphers) { 3558 eallib.OpenSSL_add_all_ciphers(); 3559 if(eallib.OpenSSL_add_all_digests is null) 3560 throw new Exception("no add digests"); 3561 eallib.OpenSSL_add_all_digests(); 3562 } else if(eallib.OPENSSL_init_crypto) 3563 eallib.OPENSSL_init_crypto(0 /*OPENSSL_INIT_ADD_ALL_CIPHERS and ALL_DIGESTS together*/, null); 3564 else throw new Exception("couldn't init crypto openssl"); 3565 3566 if(ossllib.SSL_load_error_strings) 3567 ossllib.SSL_load_error_strings(); 3568 else if(ossllib.OPENSSL_init_ssl) 3569 ossllib.OPENSSL_init_ssl(0x00200000L, null); 3570 else throw new Exception("couldn't load openssl errors"); 3571 3572 sslLoaded = true; 3573 } 3574 } 3575 3576 /+ 3577 // I'm just gonna let the OS clean this up on process termination because otherwise SSL_free 3578 // might have trouble being run from the GC after this module is unloaded. 3579 shared static ~this() { 3580 if(ossllib_handle) { 3581 version(Windows) { 3582 FreeLibrary(oeaylib_handle); 3583 FreeLibrary(ossllib_handle); 3584 } else version(Posix) 3585 dlclose(ossllib_handle); 3586 ossllib_handle = null; 3587 } 3588 ossllib.tupleof = ossllib.tupleof.init; 3589 } 3590 +/ 3591 3592 //pragma(lib, "crypto"); 3593 //pragma(lib, "ssl"); 3594 extern(C) 3595 void write_to_file(SSL* ssl, char* line) 3596 { 3597 import std.stdio; 3598 import std.string; 3599 import std.process : environment; 3600 string logfile = environment.get("SSLKEYLOGFILE"); 3601 if (logfile !is null) 3602 { 3603 auto f = std.stdio.File(logfile, "a+"); 3604 f.writeln(fromStringz(line)); 3605 f.close(); 3606 } 3607 } 3608 3609 class OpenSslSocket : Socket { 3610 private SSL* ssl; 3611 private SSL_CTX* ctx; 3612 private void initSsl(bool verifyPeer, string hostname) { 3613 ctx = OpenSSL.SSL_CTX_new(OpenSSL.SSLv23_client_method()); 3614 assert(ctx !is null); 3615 3616 debug OpenSSL.SSL_CTX_keylog_cb_func(ctx, &write_to_file); 3617 ssl = OpenSSL.SSL_new(ctx); 3618 3619 if(hostname.length) { 3620 OpenSSL.SSL_set_tlsext_host_name(ssl, toStringz(hostname)); 3621 if(verifyPeer) 3622 OpenSSL.X509_VERIFY_PARAM_set1_host(OpenSSL.SSL_get0_param(ssl), hostname.ptr, hostname.length); 3623 } 3624 3625 if(verifyPeer) { 3626 OpenSSL.SSL_CTX_set_default_verify_paths(ctx); 3627 3628 version(Windows) { 3629 loadCertificatesFromRegistry(ctx); 3630 } 3631 3632 OpenSSL.SSL_set_verify(ssl, SSL_VERIFY_PEER, &verifyCertificateFromRegistryArsdHttp); 3633 } else 3634 OpenSSL.SSL_set_verify(ssl, SSL_VERIFY_NONE, null); 3635 3636 OpenSSL.SSL_set_fd(ssl, cast(int) this.handle); // on win64 it is necessary to truncate, but the value is never large anyway see http://openssl.6102.n7.nabble.com/Sockets-windows-64-bit-td36169.html 3637 3638 3639 OpenSSL.SSL_CTX_set_client_cert_cb(ctx, &cb); 3640 } 3641 3642 extern(C) 3643 static int cb(SSL* ssl, X509** x509, EVP_PKEY** pkey) { 3644 if(HttpClient.certFilename.length && HttpClient.keyFilename.length) { 3645 FILE* fpCert = fopen((HttpClient.certFilename ~ "\0").ptr, "rb"); 3646 if(fpCert is null) 3647 return 0; 3648 scope(exit) 3649 fclose(fpCert); 3650 FILE* fpKey = fopen((HttpClient.keyFilename ~ "\0").ptr, "rb"); 3651 if(fpKey is null) 3652 return 0; 3653 scope(exit) 3654 fclose(fpKey); 3655 3656 with(CertificateFileFormat) 3657 final switch(HttpClient.certFormat) { 3658 case guess: 3659 if(HttpClient.certFilename.endsWith(".pem") || HttpClient.keyFilename.endsWith(".pem")) 3660 goto case pem; 3661 else 3662 goto case der; 3663 case pem: 3664 *x509 = OpenSSL.PEM_read_X509(fpCert, null, null, null); 3665 *pkey = OpenSSL.PEM_read_PrivateKey(fpKey, null, null, null); 3666 break; 3667 case der: 3668 *x509 = OpenSSL.d2i_X509_fp(fpCert, null); 3669 *pkey = OpenSSL.d2i_PrivateKey_fp(fpKey, null); 3670 break; 3671 } 3672 3673 return 1; 3674 } 3675 3676 return 0; 3677 } 3678 3679 final bool dataPending() { 3680 return OpenSSL.SSL_pending(ssl) > 0; 3681 } 3682 3683 @trusted 3684 override void connect(Address to) { 3685 super.connect(to); 3686 if(blocking) { 3687 do_ssl_connect(); 3688 } 3689 } 3690 3691 private string lastSocketError; 3692 3693 @trusted 3694 // returns true if it is finished, false if it would have blocked, throws if there's an error 3695 int do_ssl_connect() { 3696 if(OpenSSL.SSL_connect(ssl) == -1) { 3697 3698 auto errCode = OpenSSL.SSL_get_error(ssl, -1); 3699 if(errCode == SSL_ERROR_WANT_READ || errCode == SSL_ERROR_WANT_WRITE) { 3700 return errCode; 3701 } 3702 3703 string str; 3704 OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); 3705 3706 auto err = OpenSSL.SSL_get_verify_result(ssl); 3707 this.lastSocketError = str ~ " " ~ getOpenSslErrorCode(err); 3708 3709 throw new Exception("Secure connect failed: " ~ getOpenSslErrorCode(err)); 3710 } else this.lastSocketError = null; 3711 3712 return 0; 3713 } 3714 3715 @trusted 3716 override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) { 3717 //import std.stdio;writeln(cast(string) buf); 3718 debug(arsd_http2_verbose) writeln("ssl writing ", buf.length); 3719 auto retval = OpenSSL.SSL_write(ssl, buf.ptr, cast(uint) buf.length); 3720 3721 // don't need to throw anymore since it is checked elsewhere 3722 // code useful sometimes for debugging hence commenting instead of deleting 3723 if(retval == -1) { 3724 string str; 3725 OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); 3726 this.lastSocketError = str; 3727 3728 // throw new Exception("ssl send failed " ~ str); 3729 } else this.lastSocketError = null; 3730 return retval; 3731 3732 } 3733 override ptrdiff_t send(scope const(void)[] buf) { 3734 return send(buf, SocketFlags.NONE); 3735 } 3736 @trusted 3737 override ptrdiff_t receive(scope void[] buf, SocketFlags flags) { 3738 3739 debug(arsd_http2_verbose) writeln("ssl_read before"); 3740 auto retval = OpenSSL.SSL_read(ssl, buf.ptr, cast(int)buf.length); 3741 debug(arsd_http2_verbose) writeln("ssl_read after"); 3742 3743 // don't need to throw anymore since it is checked elsewhere 3744 // code useful sometimes for debugging hence commenting instead of deleting 3745 if(retval == -1) { 3746 3747 string str; 3748 OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); 3749 this.lastSocketError = str; 3750 3751 // throw new Exception("ssl receive failed " ~ str); 3752 } else this.lastSocketError = null; 3753 return retval; 3754 } 3755 override ptrdiff_t receive(scope void[] buf) { 3756 return receive(buf, SocketFlags.NONE); 3757 } 3758 3759 this(AddressFamily af, SocketType type = SocketType.STREAM, string hostname = null, bool verifyPeer = true) { 3760 version(Windows) __traits(getMember, this, "_blocking") = true; // lol longstanding phobos bug setting this to false on init 3761 super(af, type); 3762 initSsl(verifyPeer, hostname); 3763 } 3764 3765 override void close() scope @trusted { 3766 if(ssl) OpenSSL.SSL_shutdown(ssl); 3767 super.close(); 3768 freeSsl(); 3769 } 3770 3771 this(socket_t sock, AddressFamily af, string hostname, bool verifyPeer = true) { 3772 super(sock, af); 3773 initSsl(verifyPeer, hostname); 3774 } 3775 3776 void freeSsl() @nogc nothrow { 3777 if(ssl is null) 3778 return; 3779 OpenSSL.SSL_free(ssl); 3780 OpenSSL.SSL_CTX_free(ctx); 3781 ssl = null; 3782 ctx = null; 3783 } 3784 3785 ~this() { 3786 freeSsl(); 3787 } 3788 } 3789 } 3790 3791 3792 /++ 3793 An experimental component for working with REST apis. Note that it 3794 is a zero-argument template, so to create one, use `new HttpApiClient!()(args..)` 3795 or you will get "HttpApiClient is used as a type" compile errors. 3796 3797 This will probably not work for you yet, and I might change it significantly. 3798 3799 Requires [arsd.jsvar]. 3800 3801 3802 Here's a snippet to create a pull request on GitHub to Phobos: 3803 3804 --- 3805 auto github = new HttpApiClient!()("https://api.github.com/", "your personal api token here"); 3806 3807 // create the arguments object 3808 // see: https://developer.github.com/v3/pulls/#create-a-pull-request 3809 var args = var.emptyObject; 3810 args.title = "My Pull Request"; 3811 args.head = "yourusername:" ~ branchName; 3812 args.base = "master"; 3813 // note it is ["body"] instead of .body because `body` is a D keyword 3814 args["body"] = "My cool PR is opened by the API!"; 3815 args.maintainer_can_modify = true; 3816 3817 /+ 3818 Fun fact, you can also write that: 3819 3820 var args = [ 3821 "title": "My Pull Request".var, 3822 "head": "yourusername:" ~ branchName.var, 3823 "base" : "master".var, 3824 "body" : "My cool PR is opened by the API!".var, 3825 "maintainer_can_modify": true.var 3826 ]; 3827 3828 Note the .var constructor calls in there. If everything is the same type, you actually don't need that, but here since there's strings and bools, D won't allow the literal without explicit constructors to align them all. 3829 +/ 3830 3831 // this translates to `repos/dlang/phobos/pulls` and sends a POST request, 3832 // containing `args` as json, then immediately grabs the json result and extracts 3833 // the value `html_url` from it. `prUrl` is typed `var`, from arsd.jsvar. 3834 auto prUrl = github.rest.repos.dlang.phobos.pulls.POST(args).result.html_url; 3835 3836 writeln("Created: ", prUrl); 3837 --- 3838 3839 Why use this instead of just building the URL? Well, of course you can! This just makes 3840 it a bit more convenient than string concatenation and manages a few headers for you. 3841 3842 Subtypes could potentially add static type checks too. 3843 +/ 3844 class HttpApiClient() { 3845 import arsd.jsvar; 3846 3847 HttpClient httpClient; 3848 3849 alias HttpApiClientType = typeof(this); 3850 3851 string urlBase; 3852 string oauth2Token; 3853 string submittedContentType; 3854 string authType = "Bearer"; 3855 3856 /++ 3857 Params: 3858 3859 urlBase = The base url for the api. Tends to be something like `https://api.example.com/v2/` or similar. 3860 oauth2Token = the authorization token for the service. You'll have to get it from somewhere else. 3861 submittedContentType = the content-type of POST, PUT, etc. bodies. 3862 httpClient = an injected http client, or null if you want to use a default-constructed one 3863 3864 History: 3865 The `httpClient` param was added on December 26, 2020. 3866 +/ 3867 this(string urlBase, string oauth2Token, string submittedContentType = "application/json", HttpClient httpClient = null) { 3868 if(httpClient is null) 3869 this.httpClient = new HttpClient(); 3870 else 3871 this.httpClient = httpClient; 3872 3873 assert(urlBase[0] == 'h'); 3874 assert(urlBase[$-1] == '/'); 3875 3876 this.urlBase = urlBase; 3877 this.oauth2Token = oauth2Token; 3878 this.submittedContentType = submittedContentType; 3879 } 3880 3881 /// 3882 static struct HttpRequestWrapper { 3883 HttpApiClientType apiClient; /// 3884 HttpRequest request; /// 3885 HttpResponse _response; 3886 3887 /// 3888 this(HttpApiClientType apiClient, HttpRequest request) { 3889 this.apiClient = apiClient; 3890 this.request = request; 3891 } 3892 3893 /// Returns the full [HttpResponse] object so you can inspect the headers 3894 @property HttpResponse response() { 3895 if(_response is HttpResponse.init) 3896 _response = request.waitForCompletion(); 3897 return _response; 3898 } 3899 3900 /++ 3901 Returns the parsed JSON from the body of the response. 3902 3903 Throws on non-2xx responses. 3904 +/ 3905 var result() { 3906 return apiClient.throwOnError(response); 3907 } 3908 3909 alias request this; 3910 } 3911 3912 /// 3913 HttpRequestWrapper request(string uri, HttpVerb requestMethod = HttpVerb.GET, ubyte[] bodyBytes = null) { 3914 if(uri[0] == '/') 3915 uri = uri[1 .. $]; 3916 3917 auto u = Uri(uri).basedOn(Uri(urlBase)); 3918 3919 auto req = httpClient.navigateTo(u, requestMethod); 3920 3921 if(oauth2Token.length) 3922 req.requestParameters.headers ~= "Authorization: "~ authType ~" " ~ oauth2Token; 3923 req.requestParameters.contentType = submittedContentType; 3924 req.requestParameters.bodyData = bodyBytes; 3925 3926 return HttpRequestWrapper(this, req); 3927 } 3928 3929 /// 3930 var throwOnError(HttpResponse res) { 3931 if(res.code < 200 || res.code >= 300) 3932 throw new Exception(res.codeText ~ " " ~ res.contentText); 3933 3934 var response = var.fromJson(res.contentText); 3935 if(response.errors) { 3936 throw new Exception(response.errors.toJson()); 3937 } 3938 3939 return response; 3940 } 3941 3942 /// 3943 @property RestBuilder rest() { 3944 return RestBuilder(this, null, null); 3945 } 3946 3947 // hipchat.rest.room["Tech Team"].history 3948 // gives: "/room/Tech%20Team/history" 3949 // 3950 // hipchat.rest.room["Tech Team"].history("page", "12) 3951 /// 3952 static struct RestBuilder { 3953 HttpApiClientType apiClient; 3954 string[] pathParts; 3955 string[2][] queryParts; 3956 this(HttpApiClientType apiClient, string[] pathParts, string[2][] queryParts) { 3957 this.apiClient = apiClient; 3958 this.pathParts = pathParts; 3959 this.queryParts = queryParts; 3960 } 3961 3962 RestBuilder _SELF() { 3963 return this; 3964 } 3965 3966 /// The args are so you can call opCall on the returned 3967 /// object, despite @property being broken af in D. 3968 RestBuilder opDispatch(string str, T)(string n, T v) { 3969 return RestBuilder(apiClient, pathParts ~ str, queryParts ~ [n, to!string(v)]); 3970 } 3971 3972 /// 3973 RestBuilder opDispatch(string str)() { 3974 return RestBuilder(apiClient, pathParts ~ str, queryParts); 3975 } 3976 3977 3978 /// 3979 RestBuilder opIndex(string str) { 3980 return RestBuilder(apiClient, pathParts ~ str, queryParts); 3981 } 3982 /// 3983 RestBuilder opIndex(var str) { 3984 return RestBuilder(apiClient, pathParts ~ str.get!string, queryParts); 3985 } 3986 /// 3987 RestBuilder opIndex(int i) { 3988 return RestBuilder(apiClient, pathParts ~ to!string(i), queryParts); 3989 } 3990 3991 /// 3992 RestBuilder opCall(T)(string name, T value) { 3993 return RestBuilder(apiClient, pathParts, queryParts ~ [name, to!string(value)]); 3994 } 3995 3996 /// 3997 string toUri() { 3998 string result; 3999 foreach(idx, part; pathParts) { 4000 if(idx) 4001 result ~= "/"; 4002 result ~= encodeUriComponent(part); 4003 } 4004 result ~= "?"; 4005 foreach(idx, part; queryParts) { 4006 if(idx) 4007 result ~= "&"; 4008 result ~= encodeUriComponent(part[0]); 4009 result ~= "="; 4010 result ~= encodeUriComponent(part[1]); 4011 } 4012 4013 return result; 4014 } 4015 4016 /// 4017 final HttpRequestWrapper GET() { return _EXECUTE(HttpVerb.GET, this.toUri(), ToBytesResult.init); } 4018 /// ditto 4019 final HttpRequestWrapper DELETE() { return _EXECUTE(HttpVerb.DELETE, this.toUri(), ToBytesResult.init); } 4020 4021 // need to be able to send: JSON, urlencoded, multipart/form-data, and raw stuff. 4022 /// ditto 4023 final HttpRequestWrapper POST(T...)(T t) { return _EXECUTE(HttpVerb.POST, this.toUri(), toBytes(t)); } 4024 /// ditto 4025 final HttpRequestWrapper PATCH(T...)(T t) { return _EXECUTE(HttpVerb.PATCH, this.toUri(), toBytes(t)); } 4026 /// ditto 4027 final HttpRequestWrapper PUT(T...)(T t) { return _EXECUTE(HttpVerb.PUT, this.toUri(), toBytes(t)); } 4028 4029 struct ToBytesResult { 4030 ubyte[] bytes; 4031 string contentType; 4032 } 4033 4034 private ToBytesResult toBytes(T...)(T t) { 4035 import std.conv : to; 4036 static if(T.length == 0) 4037 return ToBytesResult(null, null); 4038 else static if(T.length == 1 && is(T[0] == var)) 4039 return ToBytesResult(cast(ubyte[]) t[0].toJson(), "application/json"); // json data 4040 else static if(T.length == 1 && (is(T[0] == string) || is(T[0] == ubyte[]))) 4041 return ToBytesResult(cast(ubyte[]) t[0], null); // raw data 4042 else static if(T.length == 1 && is(T[0] : FormData)) 4043 return ToBytesResult(t[0].toBytes, t[0].contentType); 4044 else static if(T.length > 1 && T.length % 2 == 0 && is(T[0] == string)) { 4045 // string -> value pairs for a POST request 4046 string answer; 4047 foreach(idx, val; t) { 4048 static if(idx % 2 == 0) { 4049 if(answer.length) 4050 answer ~= "&"; 4051 answer ~= encodeUriComponent(val); // it had better be a string! lol 4052 answer ~= "="; 4053 } else { 4054 answer ~= encodeUriComponent(to!string(val)); 4055 } 4056 } 4057 4058 return ToBytesResult(cast(ubyte[]) answer, "application/x-www-form-urlencoded"); 4059 } 4060 else 4061 static assert(0); // FIXME 4062 4063 } 4064 4065 HttpRequestWrapper _EXECUTE(HttpVerb verb, string uri, ubyte[] bodyBytes) { 4066 return apiClient.request(uri, verb, bodyBytes); 4067 } 4068 4069 HttpRequestWrapper _EXECUTE(HttpVerb verb, string uri, ToBytesResult tbr) { 4070 auto r = apiClient.request(uri, verb, tbr.bytes); 4071 if(tbr.contentType !is null) 4072 r.requestParameters.contentType = tbr.contentType; 4073 return r; 4074 } 4075 } 4076 } 4077 4078 4079 // see also: arsd.cgi.encodeVariables 4080 /++ 4081 Creates a multipart/form-data object that is suitable for file uploads and other kinds of POST. 4082 4083 It has a set of names and values of mime components. Names can be repeated. They will be presented in the same order in which you add them. You will mostly want to use the [append] method. 4084 4085 You can pass this directly to [HttpClient.request]. 4086 4087 Based on: https://developer.mozilla.org/en-US/docs/Web/API/FormData 4088 4089 --- 4090 auto fd = new FormData(); 4091 // add some data, plain string first 4092 fd.append("name", "Adam"); 4093 // then a file 4094 fd.append("photo", std.file.read("adam.jpg"), "image/jpeg", "adam.jpg"); 4095 4096 // post it! 4097 auto client = new HttpClient(); 4098 client.request(Uri("http://example.com/people"), fd).waitForCompletion(); 4099 --- 4100 4101 History: 4102 Added June 8, 2018 4103 +/ 4104 class FormData { 4105 static struct MimePart { 4106 string name; 4107 const(void)[] data; 4108 string contentType; 4109 string filename; 4110 } 4111 4112 private MimePart[] parts; 4113 private string boundary = "0016e64be86203dd36047610926a"; // FIXME 4114 4115 /++ 4116 Appends the given entry to the request. This can be a simple key/value pair of strings or file uploads. 4117 4118 For a simple key/value pair, leave `contentType` and `filename` as `null`. 4119 4120 For file uploads, please note that many servers require filename be given for a file upload and it may not allow you to put in a path. I suggest using [std.path.baseName] to strip off path information from a file you are loading. 4121 4122 The `contentType` is generally verified by servers for file uploads. 4123 +/ 4124 void append(string key, const(void)[] value, string contentType = null, string filename = null) { 4125 parts ~= MimePart(key, value, contentType, filename); 4126 } 4127 4128 /++ 4129 Deletes any entries from the set with the given key. 4130 4131 History: 4132 Added June 7, 2023 (dub v11.0) 4133 +/ 4134 void deleteKey(string key) { 4135 MimePart[] newParts; 4136 foreach(part; parts) 4137 if(part.name != key) 4138 newParts ~= part; 4139 parts = newParts; 4140 } 4141 4142 /++ 4143 Returns the first entry with the given key, or `MimePart.init` if there is nothing. 4144 4145 History: 4146 Added June 7, 2023 (dub v11.0) 4147 +/ 4148 MimePart get(string key) { 4149 foreach(part; parts) 4150 if(part.name == key) 4151 return part; 4152 return MimePart.init; 4153 } 4154 4155 /++ 4156 Returns the all entries with the given key. 4157 4158 History: 4159 Added June 7, 2023 (dub v11.0) 4160 +/ 4161 MimePart[] getAll(string key) { 4162 MimePart[] answer; 4163 foreach(part; parts) 4164 if(part.name == key) 4165 answer ~= part; 4166 return answer; 4167 } 4168 4169 /++ 4170 Returns true if the given key exists in the set. 4171 4172 History: 4173 Added June 7, 2023 (dub v11.0) 4174 +/ 4175 bool has(string key) { 4176 return get(key).name == key; 4177 } 4178 4179 /++ 4180 Sets the given key to the given value if it exists, or appends it if it doesn't. 4181 4182 You probably want [append] instead. 4183 4184 See_Also: 4185 [append] 4186 4187 History: 4188 Added June 7, 2023 (dub v11.0) 4189 +/ 4190 void set(string key, const(void)[] value, string contentType, string filename) { 4191 foreach(ref part; parts) 4192 if(part.name == key) { 4193 part.data = value; 4194 part.contentType = contentType; 4195 part.filename = filename; 4196 return; 4197 } 4198 4199 append(key, value, contentType, filename); 4200 } 4201 4202 /++ 4203 Returns all the current entries in the object. 4204 4205 History: 4206 Added June 7, 2023 (dub v11.0) 4207 +/ 4208 MimePart[] entries() { 4209 return parts; 4210 } 4211 4212 // FIXME: 4213 // keys iterator 4214 // values iterator 4215 4216 /++ 4217 Gets the content type header that should be set in the request. This includes the type and boundary that is applicable to the [toBytes] method. 4218 +/ 4219 string contentType() { 4220 return "multipart/form-data; boundary=" ~ boundary; 4221 } 4222 4223 /++ 4224 Returns bytes applicable for the body of this request. Use the [contentType] method to get the appropriate content type header with the right boundary. 4225 +/ 4226 ubyte[] toBytes() { 4227 string data; 4228 4229 foreach(part; parts) { 4230 data ~= "--" ~ boundary ~ "\r\n"; 4231 data ~= "Content-Disposition: form-data; name=\""~part.name~"\""; 4232 if(part.filename !is null) 4233 data ~= "; filename=\""~part.filename~"\""; 4234 data ~= "\r\n"; 4235 if(part.contentType !is null) 4236 data ~= "Content-Type: " ~ part.contentType ~ "\r\n"; 4237 data ~= "\r\n"; 4238 4239 data ~= cast(string) part.data; 4240 4241 data ~= "\r\n"; 4242 } 4243 4244 data ~= "--" ~ boundary ~ "--\r\n"; 4245 4246 return cast(ubyte[]) data; 4247 } 4248 } 4249 4250 private bool bicmp(in ubyte[] item, in char[] search) { 4251 if(item.length != search.length) return false; 4252 4253 foreach(i; 0 .. item.length) { 4254 ubyte a = item[i]; 4255 ubyte b = search[i]; 4256 if(a >= 'A' && a <= 'Z') 4257 a += 32; 4258 //if(b >= 'A' && b <= 'Z') 4259 //b += 32; 4260 if(a != b) 4261 return false; 4262 } 4263 4264 return true; 4265 } 4266 4267 public import arsd.core : WebSocketOpcode, WebSocketFrame; 4268 4269 /++ 4270 WebSocket client, based on the browser api, though also with other api options. 4271 4272 --- 4273 import arsd.http2; 4274 4275 void main() { 4276 auto ws = new WebSocket(Uri("ws://....")); 4277 4278 ws.onmessage = (in char[] msg) { 4279 ws.send("a reply"); 4280 }; 4281 4282 ws.connect(); 4283 4284 WebSocket.eventLoop(); 4285 } 4286 --- 4287 4288 Symbol_groups: 4289 foundational = 4290 Used with all API styles. 4291 4292 browser_api = 4293 API based on the standard in the browser. 4294 4295 event_loop_integration = 4296 Integrating with external event loops is done through static functions. You should 4297 call these BEFORE doing anything else with the WebSocket module or class. 4298 4299 $(PITFALL NOT IMPLEMENTED) 4300 --- 4301 WebSocket.setEventLoopProxy(arsd.simpledisplay.EventLoop.proxy.tupleof); 4302 // or something like that. it is not implemented yet. 4303 --- 4304 $(PITFALL NOT IMPLEMENTED) 4305 4306 blocking_api = 4307 The blocking API is best used when you only need basic functionality with a single connection. 4308 4309 --- 4310 WebSocketFrame msg; 4311 do { 4312 // FIXME good demo 4313 } while(msg); 4314 --- 4315 4316 Or to check for blocks before calling: 4317 4318 --- 4319 try_to_process_more: 4320 while(ws.isMessageBuffered()) { 4321 auto msg = ws.waitForNextMessage(); 4322 // process msg 4323 } 4324 if(ws.isDataPending()) { 4325 ws.lowLevelReceive(); 4326 goto try_to_process_more; 4327 } else { 4328 // nothing ready, you can do other things 4329 // or at least sleep a while before trying 4330 // to process more. 4331 if(ws.readyState == WebSocket.OPEN) { 4332 Thread.sleep(1.seconds); 4333 goto try_to_process_more; 4334 } 4335 } 4336 --- 4337 4338 History: 4339 Shared functionality between [arsd.http2] and [arsd.cgi] finally merged into [arsd.core.WebSocketBase] on November 2, 2025. 4340 4341 +/ 4342 class WebSocket : arsd.core.WebSocketBase { 4343 private Uri uri; 4344 private string[string] cookies; 4345 4346 private string host; 4347 private ushort port; 4348 private bool ssl; 4349 4350 private MonoTime timeoutFromInactivity; 4351 private MonoTime nextPing; 4352 4353 /++ 4354 wss://echo.websocket.org 4355 +/ 4356 /// Group: foundational 4357 this(Uri uri, Config config = Config.init) 4358 //in (uri.scheme == "ws" || uri.scheme == "wss") 4359 in { assert(uri.scheme == "ws" || uri.scheme == "wss"); } do 4360 { 4361 this.uri = uri; 4362 this.config = config; 4363 4364 this.receiveBuffer = new ubyte[](config.initialReceiveBufferSize); 4365 4366 host = uri.host; 4367 ssl = uri.scheme == "wss"; 4368 port = cast(ushort) (uri.port ? uri.port : ssl ? 443 : 80); 4369 4370 if(ssl) { 4371 version(with_openssl) { 4372 loadOpenSsl(); 4373 socket = new SslClientSocket(family(uri.unixSocketPath), SocketType.STREAM, host, config.verifyPeer); 4374 } else 4375 throw new Exception("SSL not compiled in"); 4376 } else 4377 socket = new Socket(family(uri.unixSocketPath), SocketType.STREAM); 4378 4379 socket.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 4380 cookies = config.cookies; 4381 } 4382 4383 /++ 4384 4385 +/ 4386 /// Group: foundational 4387 void connect() { 4388 this.isClient = true; 4389 4390 socket.blocking = false; 4391 4392 if(uri.unixSocketPath) 4393 socket.connect(new UnixAddress(uri.unixSocketPath)); 4394 else 4395 socket.connect(new InternetAddress(host, port)); // FIXME: ipv6 support... 4396 4397 4398 auto readSet = new SocketSet(); 4399 auto writeSet = new SocketSet(); 4400 4401 readSet.reset(); 4402 writeSet.reset(); 4403 4404 readSet.add(socket); 4405 writeSet.add(socket); 4406 4407 auto selectGot = Socket.select(readSet, writeSet, null, config.timeoutFromInactivity); 4408 if(selectGot == -1) { 4409 // interrupted 4410 4411 throw new Exception("Websocket connection interrupted - retry might succeed"); 4412 } else if(selectGot == 0) { 4413 // time out 4414 socket.close(); 4415 throw new Exception("Websocket connection timed out"); 4416 } else { 4417 if(writeSet.isSet(socket) || readSet.isSet(socket)) { 4418 import core.stdc.stdint; 4419 int32_t error; 4420 int retopt = socket.getOption(SocketOptionLevel.SOCKET, SocketOption.ERROR, error); 4421 if(retopt < 0 || error != 0) { 4422 socket.close(); 4423 throw new Exception("Websocket connection failed - " ~ formatSocketError(error)); 4424 } else { 4425 // FIXME: websocket handshake could and really should be async too. 4426 socket.blocking = true; // just convenience 4427 if(auto s = cast(SslClientSocket) socket) { 4428 s.do_ssl_connect(); 4429 } else { 4430 // we're ready 4431 } 4432 } 4433 } 4434 } 4435 4436 auto uri = this.uri.path.length ? this.uri.path : "/"; 4437 if(this.uri.query.length) { 4438 uri ~= "?"; 4439 uri ~= this.uri.query; 4440 } 4441 4442 // the headers really shouldn't be bigger than this, at least 4443 // the chunks i need to process 4444 ubyte[4096] bufferBacking = void; 4445 ubyte[] buffer = bufferBacking[]; 4446 size_t pos; 4447 4448 void append(in char[][] items...) { 4449 foreach(what; items) { 4450 if((pos + what.length) > buffer.length) { 4451 buffer.length += 4096; 4452 } 4453 buffer[pos .. pos + what.length] = cast(ubyte[]) what[]; 4454 pos += what.length; 4455 } 4456 } 4457 4458 append("GET ", uri, " HTTP/1.1\r\n"); 4459 append("Host: ", this.uri.host, "\r\n"); 4460 4461 append("Upgrade: websocket\r\n"); 4462 append("Connection: Upgrade\r\n"); 4463 append("Sec-WebSocket-Version: 13\r\n"); 4464 4465 // FIXME: randomize this 4466 append("Sec-WebSocket-Key: x3JEHMbDL1EzLkh9GBhXDw==\r\n"); 4467 if(cookies.length > 0) { 4468 append("Cookie: "); 4469 bool first=true; 4470 foreach(k,v;cookies) { 4471 if(first) first = false; 4472 else append("; "); 4473 append(k); 4474 append("="); 4475 append(v); 4476 } 4477 append("\r\n"); 4478 } 4479 /* 4480 //This is equivalent but has dependencies 4481 import std.format; 4482 import std.algorithm : map; 4483 append(format("cookie: %-(%s %)\r\n",cookies.byKeyValue.map!(t=>format("%s=%s",t.key,t.value)))); 4484 */ 4485 4486 if(config.protocol.length) 4487 append("Sec-WebSocket-Protocol: ", config.protocol, "\r\n"); 4488 if(config.origin.length) 4489 append("Origin: ", config.origin, "\r\n"); 4490 4491 foreach(h; config.additionalHeaders) { 4492 append(h); 4493 append("\r\n"); 4494 } 4495 4496 append("\r\n"); 4497 4498 auto remaining = buffer[0 .. pos]; 4499 //import std.stdio; writeln(host, " " , port, " ", cast(string) remaining); 4500 while(remaining.length) { 4501 auto r = socket.send(remaining); 4502 if(r < 0) 4503 throw new Exception(lastSocketError(socket)); 4504 if(r == 0) 4505 throw new Exception("unexpected connection termination"); 4506 remaining = remaining[r .. $]; 4507 } 4508 4509 // the response shouldn't be especially large at this point, just 4510 // headers for the most part. gonna try to get it in the stack buffer. 4511 // then copy stuff after headers, if any, to the frame buffer. 4512 ubyte[] used; 4513 4514 void more() { 4515 auto r = socket.receive(buffer[used.length .. $]); 4516 4517 if(r < 0) 4518 throw new Exception(lastSocketError(socket)); 4519 if(r == 0) 4520 throw new Exception("unexpected connection termination"); 4521 //import std.stdio;writef("%s", cast(string) buffer[used.length .. used.length + r]); 4522 4523 used = buffer[0 .. used.length + r]; 4524 } 4525 4526 more(); 4527 4528 import std.algorithm; 4529 if(!used.startsWith(cast(ubyte[]) "HTTP/1.1 101")) 4530 throw new Exception("didn't get a websocket answer"); 4531 // skip the status line 4532 while(used.length && used[0] != '\n') 4533 used = used[1 .. $]; 4534 4535 if(used.length == 0) 4536 throw new Exception("Remote server disconnected or didn't send enough information"); 4537 4538 if(used.length < 1) 4539 more(); 4540 4541 used = used[1 .. $]; // skip the \n 4542 4543 if(used.length == 0) 4544 more(); 4545 4546 // checks on the protocol from ehaders 4547 bool isWebsocket; 4548 bool isUpgrade; 4549 const(ubyte)[] protocol; 4550 const(ubyte)[] accept; 4551 4552 while(used.length) { 4553 if(used.length >= 2 && used[0] == '\r' && used[1] == '\n') { 4554 used = used[2 .. $]; 4555 break; // all done 4556 } 4557 int idxColon; 4558 while(idxColon < used.length && used[idxColon] != ':') 4559 idxColon++; 4560 if(idxColon == used.length) 4561 more(); 4562 auto idxStart = idxColon + 1; 4563 while(idxStart < used.length && used[idxStart] == ' ') 4564 idxStart++; 4565 if(idxStart == used.length) 4566 more(); 4567 auto idxEnd = idxStart; 4568 while(idxEnd < used.length && used[idxEnd] != '\r') 4569 idxEnd++; 4570 if(idxEnd == used.length) 4571 more(); 4572 4573 auto headerName = used[0 .. idxColon]; 4574 auto headerValue = used[idxStart .. idxEnd]; 4575 4576 // move past this header 4577 used = used[idxEnd .. $]; 4578 // and the \r\n 4579 if(2 <= used.length) 4580 used = used[2 .. $]; 4581 4582 if(headerName.bicmp("upgrade")) { 4583 if(headerValue.bicmp("websocket")) 4584 isWebsocket = true; 4585 } else if(headerName.bicmp("connection")) { 4586 if(headerValue.bicmp("upgrade")) 4587 isUpgrade = true; 4588 } else if(headerName.bicmp("sec-websocket-accept")) { 4589 accept = headerValue; 4590 } else if(headerName.bicmp("sec-websocket-protocol")) { 4591 protocol = headerValue; 4592 } 4593 4594 if(!used.length) { 4595 more(); 4596 } 4597 } 4598 4599 4600 if(!isWebsocket) 4601 throw new Exception("didn't answer as websocket"); 4602 if(!isUpgrade) 4603 throw new Exception("didn't answer as upgrade"); 4604 4605 4606 // FIXME: check protocol if config requested one 4607 // FIXME: check accept for the right hash 4608 4609 receiveBuffer[0 .. used.length] = used[]; 4610 receiveBufferUsedLength = used.length; 4611 4612 readyState_ = OPEN; 4613 4614 if(onopen) 4615 onopen(); 4616 4617 nextPing = MonoTime.currTime + config.pingFrequency.msecs; 4618 timeoutFromInactivity = MonoTime.currTime + config.timeoutFromInactivity; 4619 4620 registerActiveSocket(this); 4621 } 4622 4623 /++ 4624 Is data pending on the socket? Also check [isMessageBuffered] to see if there 4625 is already a message in memory too. 4626 4627 If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered] 4628 again. 4629 +/ 4630 /// Group: blocking_api 4631 public override bool isDataPending(Duration timeout = 0.seconds) { 4632 static SocketSet readSet; 4633 if(readSet is null) 4634 readSet = new SocketSet(); 4635 4636 version(with_openssl) 4637 if(auto s = cast(SslClientSocket) socket) { 4638 // select doesn't handle the case with stuff 4639 // left in the ssl buffer so i'm checking it separately 4640 if(s.dataPending()) { 4641 return true; 4642 } 4643 } 4644 4645 readSet.reset(); 4646 4647 readSet.add(socket); 4648 4649 //tryAgain: 4650 auto selectGot = Socket.select(readSet, null, null, timeout); 4651 if(selectGot == 0) { /* timeout */ 4652 // timeout 4653 return false; 4654 } else if(selectGot == -1) { /* interrupted */ 4655 return false; 4656 } else { /* ready */ 4657 if(readSet.isSet(socket)) { 4658 return true; 4659 } 4660 } 4661 4662 return false; 4663 } 4664 4665 protected override void llsend(ubyte[] d) { 4666 if(readyState == CONNECTING) 4667 throw new Exception("WebSocket not connected when trying to send. Did you forget to call connect(); ?"); 4668 //connect(); 4669 //import std.stdio; writeln("LLSEND: ", d); 4670 while(d.length) { 4671 auto r = socket.send(d); 4672 if(r < 0 && wouldHaveBlocked()) { 4673 // FIXME: i should register for a write wakeup 4674 version(use_arsd_core) assert(0); 4675 import core.thread; 4676 Thread.sleep(1.msecs); 4677 continue; 4678 } 4679 //import core.stdc.errno; import std.stdio; writeln(errno); 4680 if(r <= 0) { 4681 // import std.stdio; writeln(GetLastError()); 4682 throw new Exception("Socket send failed"); 4683 } 4684 d = d[r .. $]; 4685 } 4686 } 4687 4688 protected override void llshutdown() { 4689 // import std.stdio; writeln("LLCLOSE"); 4690 socket.shutdown(SocketShutdown.SEND); 4691 } 4692 4693 protected override void llclose() { 4694 socket.close(); 4695 } 4696 4697 protected override void unregisterAsActiveSocket() { 4698 unregisterActiveSocket(this); 4699 } 4700 4701 protected override arsd.core.WebSocketFrame waitGotNothing() { 4702 return arsd.core.WebSocketFrame.init; 4703 } 4704 4705 // FIXME: if i remove this function it still compiles, but it is abstract, so compiler bug still? 4706 protected override bool connectionClosedInMiddleOfMessage() { 4707 return false; 4708 } 4709 4710 private void autoprocess() { 4711 // FIXME 4712 do { 4713 processOnce(); 4714 } while(lowLevelReceive()); 4715 } 4716 4717 4718 /++ 4719 Waits for more data off the low-level socket and adds it to the pending buffer. 4720 4721 Returns `true` if the connection is still active. 4722 +/ 4723 /// Group: blocking_api 4724 public override bool lowLevelReceive() { 4725 if(readyState == CONNECTING) 4726 throw new Exception("WebSocket not connected when trying to receive. Did you forget to call connect(); ?"); 4727 if (receiveBufferUsedLength == receiveBuffer.length) 4728 { 4729 if (receiveBuffer.length == config.maximumReceiveBufferSize) 4730 throw new Exception("Maximum receive buffer size exhausted"); 4731 4732 import std.algorithm : min; 4733 receiveBuffer.length = min(receiveBuffer.length + config.initialReceiveBufferSize, 4734 config.maximumReceiveBufferSize); 4735 } 4736 auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]); 4737 if(r == 0) 4738 return false; 4739 if(r < 0 && wouldHaveBlocked()) 4740 return true; 4741 if(r <= 0) { 4742 //import std.stdio; writeln(WSAGetLastError()); 4743 return false; 4744 } 4745 receiveBufferUsedLength += r; 4746 return true; 4747 } 4748 4749 private Socket socket; 4750 4751 // returns true if still active 4752 private static bool readyToRead(WebSocket sock) { 4753 sock.timeoutFromInactivity = MonoTime.currTime + sock.config.timeoutFromInactivity; 4754 if(!sock.lowLevelReceive()) { 4755 sock.readyState_ = CLOSED; 4756 4757 if(sock.onerror) 4758 sock.onerror(); 4759 4760 if(sock.onclose) 4761 sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection lost", false, lastSocketError(sock.socket))); 4762 4763 unregisterActiveSocket(sock); 4764 sock.socket.close(); 4765 return false; 4766 } 4767 while(sock.processOnce().populated) {} 4768 return true; 4769 } 4770 4771 // returns true if still active, false if not 4772 private static bool timeoutAndPingCheck(WebSocket sock, MonoTime now, Duration* minimumTimeoutForSelect) { 4773 auto diff = sock.timeoutFromInactivity - now; 4774 if(diff <= 0.msecs) { 4775 // it timed out 4776 if(sock.onerror) 4777 sock.onerror(); 4778 4779 if(sock.onclose) 4780 sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection timed out", false, null)); 4781 4782 sock.readyState_ = CLOSED; 4783 unregisterActiveSocket(sock); 4784 sock.socket.close(); 4785 return false; 4786 } 4787 4788 if(minimumTimeoutForSelect && diff < *minimumTimeoutForSelect) 4789 *minimumTimeoutForSelect = diff; 4790 4791 diff = sock.nextPing - now; 4792 4793 if(diff <= 0.msecs) { 4794 //sock.send(`{"action": "ping"}`); 4795 sock.ping(); 4796 sock.nextPing = now + sock.config.pingFrequency.msecs; 4797 } else { 4798 if(minimumTimeoutForSelect && diff < *minimumTimeoutForSelect) 4799 *minimumTimeoutForSelect = diff; 4800 } 4801 4802 return true; 4803 } 4804 4805 /* 4806 const int bufferedAmount // amount pending 4807 const string extensions 4808 4809 const string protocol 4810 const string url 4811 */ 4812 4813 static { 4814 /++ 4815 Runs an event loop with all known websockets on this thread until all websockets 4816 are closed or unregistered, or until you call [exitEventLoop], or set `*localLoopExited` 4817 to false (please note it may take a few seconds until it checks that flag again; it may 4818 not exit immediately). 4819 4820 History: 4821 The `localLoopExited` parameter was added August 22, 2022 (dub v10.9) 4822 4823 See_Also: 4824 [addToSimpledisplayEventLoop] 4825 +/ 4826 void eventLoop(shared(bool)* localLoopExited = null) { 4827 import core.atomic; 4828 atomicOp!"+="(numberOfEventLoops, 1); 4829 scope(exit) { 4830 if(atomicOp!"-="(numberOfEventLoops, 1) <= 0) 4831 loopExited = false; // reset it so we can reenter 4832 } 4833 4834 version(use_arsd_core) { 4835 loopExited = false; 4836 4837 import arsd.core; 4838 getThisThreadEventLoop().run(() => WebSocket.activeSockets.length == 0 || loopExited || (localLoopExited !is null && *localLoopExited == true)); 4839 } else { 4840 static SocketSet readSet; 4841 4842 if(readSet is null) 4843 readSet = new SocketSet(); 4844 4845 loopExited = false; 4846 4847 outermost: while(!loopExited && (localLoopExited is null || (*localLoopExited == false))) { 4848 readSet.reset(); 4849 4850 Duration timeout = 3.seconds; 4851 4852 auto now = MonoTime.currTime; 4853 bool hadAny; 4854 foreach(sock; activeSockets) { 4855 if(!timeoutAndPingCheck(sock, now, &timeout)) 4856 continue outermost; 4857 4858 readSet.add(sock.socket); 4859 hadAny = true; 4860 } 4861 4862 if(!hadAny) { 4863 // import std.stdio; writeln("had none"); 4864 return; 4865 } 4866 4867 tryAgain: 4868 // import std.stdio; writeln(timeout); 4869 auto selectGot = Socket.select(readSet, null, null, timeout); 4870 if(selectGot == 0) { /* timeout */ 4871 // timeout 4872 continue; // it will be handled at the top of the loop 4873 } else if(selectGot == -1) { /* interrupted */ 4874 goto tryAgain; 4875 } else { 4876 foreach(sock; activeSockets) { 4877 if(readSet.isSet(sock.socket)) { 4878 if(!readyToRead(sock)) 4879 continue outermost; 4880 selectGot--; 4881 if(selectGot <= 0) 4882 break; 4883 } 4884 } 4885 } 4886 } 4887 } 4888 } 4889 4890 private static shared(int) numberOfEventLoops; 4891 4892 private __gshared bool loopExited; 4893 /++ 4894 Exits all running [WebSocket.eventLoop]s next time they loop around. You can call this from a signal handler or another thread. 4895 4896 Please note they may not loop around to check the flag for several seconds. Any new event loops will exit immediately until 4897 all current ones are closed. Once all event loops are exited, the flag is cleared and you can start the loop again. 4898 4899 This function is likely to be deprecated in the future due to its quirks and imprecise name. 4900 +/ 4901 void exitEventLoop() { 4902 loopExited = true; 4903 } 4904 4905 WebSocket[] activeSockets; 4906 4907 void registerActiveSocket(WebSocket s) { 4908 // ensure it isn't already there... 4909 assert(s !is null); 4910 if(s.registered) 4911 return; 4912 s.activeSocketArrayIndex = activeSockets.length; 4913 activeSockets ~= s; 4914 s.registered = true; 4915 version(use_arsd_core) { 4916 version(Posix) 4917 s.unregisterToken = arsd.core.getThisThreadEventLoop().addCallbackOnFdReadable(s.socket.handle, new arsd.core.CallbackHelper(() { s.readyToRead(s); })); 4918 } 4919 } 4920 void unregisterActiveSocket(WebSocket s) { 4921 version(use_arsd_core) { 4922 s.unregisterToken.unregister(); 4923 } 4924 4925 auto i = s.activeSocketArrayIndex; 4926 assert(activeSockets[i] is s); 4927 4928 activeSockets[i] = activeSockets[$-1]; 4929 activeSockets[i].activeSocketArrayIndex = i; 4930 activeSockets = activeSockets[0 .. $-1]; 4931 activeSockets.assumeSafeAppend(); 4932 s.registered = false; 4933 } 4934 } 4935 4936 private bool registered; 4937 private size_t activeSocketArrayIndex; 4938 version(use_arsd_core) { 4939 static import arsd.core; 4940 arsd.core.ICoreEventLoop.UnregisterToken unregisterToken; 4941 } 4942 } 4943 4944 private template imported(string mod) { 4945 mixin(`import imported = ` ~ mod ~ `;`); 4946 } 4947 4948 /++ 4949 Warning: you should call this AFTER websocket.connect or else it might throw on connect because the function sets nonblocking mode and the connect function doesn't handle that well (it throws on the "would block" condition in that function. easier to just do that first) 4950 +/ 4951 template addToSimpledisplayEventLoop() { 4952 import arsd.simpledisplay; 4953 void addToSimpledisplayEventLoop(WebSocket ws, imported!"arsd.simpledisplay".SimpleWindow window) { 4954 version(use_arsd_core) 4955 return; // already done implicitly 4956 4957 version(Windows) 4958 auto event = WSACreateEvent(); 4959 // FIXME: supposed to close event too 4960 4961 void midprocess() { 4962 version(Windows) 4963 ResetEvent(event); 4964 if(!ws.lowLevelReceive()) { 4965 ws.readyState_ = WebSocket.CLOSED; 4966 WebSocket.unregisterActiveSocket(ws); 4967 ws.socket.close(); 4968 return; 4969 } 4970 while(ws.processOnce().populated) {} 4971 } 4972 4973 version(Posix) { 4974 auto reader = new PosixFdReader(&midprocess, ws.socket.handle); 4975 } else version(none) { 4976 if(WSAAsyncSelect(ws.socket.handle, window.hwnd, WM_USER + 150, FD_CLOSE | FD_READ)) 4977 throw new Exception("WSAAsyncSelect"); 4978 4979 window.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 4980 if(hwnd !is window.impl.hwnd) 4981 return 1; // we don't care... 4982 switch(msg) { 4983 case WM_USER + 150: // socket activity 4984 switch(LOWORD(lParam)) { 4985 case FD_READ: 4986 case FD_CLOSE: 4987 midprocess(); 4988 break; 4989 default: 4990 // nothing 4991 } 4992 break; 4993 default: return 1; // not handled, pass it on 4994 } 4995 return 0; 4996 }; 4997 4998 } else version(Windows) { 4999 ws.socket.blocking = false; // the WSAEventSelect does this anyway and doing it here lets phobos know about it. 5000 //CreateEvent(null, 0, 0, null); 5001 if(!event) { 5002 throw new Exception("WSACreateEvent"); 5003 } 5004 if(WSAEventSelect(ws.socket.handle, event, 1/*FD_READ*/ | (1<<5)/*FD_CLOSE*/)) { 5005 //import std.stdio; writeln(WSAGetLastError()); 5006 throw new Exception("WSAEventSelect"); 5007 } 5008 5009 auto handle = new WindowsHandleReader(&midprocess, event); 5010 5011 /+ 5012 static class Ready {} 5013 5014 Ready thisr = new Ready; 5015 5016 justCommunication.addEventListener((Ready r) { 5017 if(r is thisr) 5018 midprocess(); 5019 }); 5020 5021 import core.thread; 5022 auto thread = new Thread({ 5023 while(true) { 5024 WSAWaitForMultipleEvents(1, &event, true, -1/*WSA_INFINITE*/, false); 5025 justCommunication.postEvent(thisr); 5026 } 5027 }); 5028 thread.isDaemon = true; 5029 thread.start; 5030 +/ 5031 5032 } else static assert(0, "unsupported OS"); 5033 } 5034 } 5035 5036 version(Windows) { 5037 import core.sys.windows.windows; 5038 import core.sys.windows.winsock2; 5039 } 5040 5041 version(none) { 5042 extern(Windows) int WSAAsyncSelect(SOCKET, HWND, uint, int); 5043 enum int FD_CLOSE = 1 << 5; 5044 enum int FD_READ = 1 << 0; 5045 enum int WM_USER = 1024; 5046 } 5047 5048 version(Windows) { 5049 import core.stdc.config; 5050 extern(Windows) 5051 int WSAEventSelect(SOCKET, HANDLE /* to an Event */, c_long); 5052 5053 extern(Windows) 5054 HANDLE WSACreateEvent(); 5055 5056 extern(Windows) 5057 DWORD WSAWaitForMultipleEvents(DWORD, HANDLE*, BOOL, DWORD, BOOL); 5058 } 5059 5060 private extern(C) 5061 int verifyCertificateFromRegistryArsdHttp(int preverify_ok, X509_STORE_CTX* ctx) { 5062 version(Windows) { 5063 if(preverify_ok) 5064 return 1; 5065 5066 auto err_cert = OpenSSL.X509_STORE_CTX_get_current_cert(ctx); 5067 auto err = OpenSSL.X509_STORE_CTX_get_error(ctx); 5068 5069 if(err == 62) 5070 return 0; // hostname mismatch is an error we can trust; that means OpenSSL already found the certificate and rejected it 5071 5072 auto len = OpenSSL.i2d_X509(err_cert, null); 5073 if(len == -1) 5074 return 0; 5075 ubyte[] buffer = new ubyte[](len); 5076 auto ptr = buffer.ptr; 5077 len = OpenSSL.i2d_X509(err_cert, &ptr); 5078 if(len != buffer.length) 5079 return 0; 5080 5081 5082 CERT_CHAIN_PARA thing; 5083 thing.cbSize = thing.sizeof; 5084 auto context = CertCreateCertificateContext(X509_ASN_ENCODING, buffer.ptr, cast(int) buffer.length); 5085 if(context is null) 5086 return 0; 5087 scope(exit) CertFreeCertificateContext(context); 5088 5089 PCCERT_CHAIN_CONTEXT chain; 5090 if(CertGetCertificateChain(null, context, null, null, &thing, 0, null, &chain)) { 5091 scope(exit) 5092 CertFreeCertificateChain(chain); 5093 5094 DWORD errorStatus = chain.TrustStatus.dwErrorStatus; 5095 5096 if(errorStatus == 0) 5097 return 1; // Windows approved it, OK carry on 5098 // otherwise, sustain OpenSSL's original ruling 5099 } 5100 5101 return 0; 5102 } else { 5103 return preverify_ok; 5104 } 5105 } 5106 5107 5108 version(Windows) { 5109 pragma(lib, "crypt32"); 5110 import core.sys.windows.wincrypt; 5111 extern(Windows) { 5112 PCCERT_CONTEXT CertEnumCertificatesInStore(HCERTSTORE hCertStore, PCCERT_CONTEXT pPrevCertContext); 5113 // BOOL CertGetCertificateChain(HCERTCHAINENGINE hChainEngine, PCCERT_CONTEXT pCertContext, LPFILETIME pTime, HCERTSTORE hAdditionalStore, PCERT_CHAIN_PARA pChainPara, DWORD dwFlags, LPVOID pvReserved, PCCERT_CHAIN_CONTEXT *ppChainContext); 5114 PCCERT_CONTEXT CertCreateCertificateContext(DWORD dwCertEncodingType, const BYTE *pbCertEncoded, DWORD cbCertEncoded); 5115 } 5116 5117 void loadCertificatesFromRegistry(SSL_CTX* ctx) { 5118 auto store = CertOpenSystemStore(0, "ROOT"); 5119 if(store is null) { 5120 // import std.stdio; writeln("failed"); 5121 return; 5122 } 5123 scope(exit) 5124 CertCloseStore(store, 0); 5125 5126 X509_STORE* ssl_store = OpenSSL.SSL_CTX_get_cert_store(ctx); 5127 PCCERT_CONTEXT c; 5128 while((c = CertEnumCertificatesInStore(store, c)) !is null) { 5129 FILETIME na = c.pCertInfo.NotAfter; 5130 SYSTEMTIME st; 5131 FileTimeToSystemTime(&na, &st); 5132 5133 /+ 5134 _CRYPTOAPI_BLOB i = cast() c.pCertInfo.Issuer; 5135 5136 char[256] buffer; 5137 auto p = CertNameToStrA(X509_ASN_ENCODING, &i, CERT_SIMPLE_NAME_STR, buffer.ptr, cast(int) buffer.length); 5138 import std.stdio; writeln(buffer[0 .. p]); 5139 +/ 5140 5141 if(st.wYear <= 2021) { 5142 // see: https://www.openssl.org/blog/blog/2021/09/13/LetsEncryptRootCertExpire/ 5143 continue; // no point keeping an expired root cert and it can break Let's Encrypt anyway 5144 } 5145 5146 const(ubyte)* thing = c.pbCertEncoded; 5147 auto x509 = OpenSSL.d2i_X509(null, &thing, c.cbCertEncoded); 5148 if (x509) { 5149 auto success = OpenSSL.X509_STORE_add_cert(ssl_store, x509); 5150 //if(!success) 5151 //writeln("FAILED HERE"); 5152 OpenSSL.X509_free(x509); 5153 } else { 5154 //writeln("FAILED"); 5155 } 5156 } 5157 5158 CertFreeCertificateContext(c); 5159 5160 // import core.stdc.stdio; printf("%s\n", OpenSSL.OpenSSL_version(0)); 5161 } 5162 5163 5164 // because i use the FILE* in PEM_read_X509 and friends 5165 // gotta use this to bridge the MS C runtime functions 5166 // might be able to just change those to only use the BIO versions 5167 // instead 5168 5169 // only on MS C runtime 5170 version(CRuntime_Microsoft) {} else version=no_openssl_applink; 5171 5172 version(no_openssl_applink) {} else { 5173 private extern(C) { 5174 void _open(); 5175 void _read(); 5176 void _write(); 5177 void _lseek(); 5178 void _close(); 5179 int _fileno(FILE*); 5180 int _setmode(int, int); 5181 } 5182 export extern(C) void** OPENSSL_Applink() { 5183 import core.stdc.stdio; 5184 5185 static extern(C) void* app_stdin() { return cast(void*) stdin; } 5186 static extern(C) void* app_stdout() { return cast(void*) stdout; } 5187 static extern(C) void* app_stderr() { return cast(void*) stderr; } 5188 static extern(C) int app_feof(FILE* fp) { return feof(fp); } 5189 static extern(C) int app_ferror(FILE* fp) { return ferror(fp); } 5190 static extern(C) void app_clearerr(FILE* fp) { return clearerr(fp); } 5191 static extern(C) int app_fileno(FILE* fp) { return _fileno(fp); } 5192 static extern(C) int app_fsetmod(FILE* fp, char mod) { 5193 return _setmode(_fileno(fp), mod == 'b' ? _O_BINARY : _O_TEXT); 5194 } 5195 5196 static immutable void*[] table = [ 5197 cast(void*) 22, // applink max 5198 5199 &app_stdin, 5200 &app_stdout, 5201 &app_stderr, 5202 &fprintf, 5203 &fgets, 5204 &fread, 5205 &fwrite, 5206 &app_fsetmod, 5207 &app_feof, 5208 &fclose, 5209 5210 &fopen, 5211 &fseek, 5212 &ftell, 5213 &fflush, 5214 &app_ferror, 5215 &app_clearerr, 5216 &app_fileno, 5217 5218 &_open, 5219 &_read, 5220 &_write, 5221 &_lseek, 5222 &_close, 5223 ]; 5224 static assert(table.length == 23); 5225 5226 return cast(void**) table.ptr; 5227 } 5228 } 5229 } 5230 5231 unittest { 5232 auto client = new HttpClient(); 5233 auto response = client.navigateTo(Uri("data:,Hello%2C%20World%21")).waitForCompletion(); 5234 assert(response.contentTypeMimeType == "text/plain", response.contentType); 5235 assert(response.contentText == "Hello, World!", response.contentText); 5236 5237 response = client.navigateTo(Uri("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")).waitForCompletion(); 5238 assert(response.contentTypeMimeType == "text/plain", response.contentType); 5239 assert(response.contentText == "Hello, World!", response.contentText); 5240 5241 response = client.navigateTo(Uri("data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E")).waitForCompletion(); 5242 assert(response.contentTypeMimeType == "text/html", response.contentType); 5243 assert(response.contentText == "<h1>Hello, World!</h1>", response.contentText); 5244 } 5245 5246 version(arsd_http2_unittests) 5247 unittest { 5248 import core.thread; 5249 5250 static void server() { 5251 import std.socket; 5252 auto socket = new TcpSocket(); 5253 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 5254 socket.bind(new InternetAddress(12346)); 5255 socket.listen(1); 5256 auto s = socket.accept(); 5257 socket.close(); 5258 5259 ubyte[1024] thing; 5260 auto g = s.receive(thing[]); 5261 5262 /+ 5263 string response = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 9\r\n\r\nHello!!??"; 5264 auto packetSize = 2; 5265 +/ 5266 5267 auto packetSize = 1; 5268 string response = "HTTP/1.1 200 OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nHello!\r\n0\r\n\r\n"; 5269 5270 while(response.length) { 5271 s.send(response[0 .. packetSize]); 5272 response = response[packetSize .. $]; 5273 //import std.stdio; writeln(response); 5274 } 5275 5276 s.close(); 5277 } 5278 5279 auto thread = new Thread(&server); 5280 thread.start; 5281 5282 Thread.sleep(200.msecs); 5283 5284 auto response = get("http://localhost:12346/").waitForCompletion; 5285 assert(response.code == 200); 5286 //import std.stdio; writeln(response); 5287 5288 foreach(site; ["https://dlang.org/", "http://arsdnet.net", "https://phobos.dpldocs.info"]) { 5289 response = get(site).waitForCompletion; 5290 assert(response.code == 200); 5291 } 5292 5293 thread.join; 5294 } 5295 5296 /+ 5297 so the url params are arguments. it knows the request 5298 internally. other params are properties on the req 5299 5300 names may have different paths... those will just add ForSomething i think. 5301 5302 auto req = api.listMergeRequests 5303 req.page = 10; 5304 5305 or 5306 req.page(1) 5307 .bar("foo") 5308 5309 req.execute(); 5310 5311 5312 everything in the response is nullable access through the 5313 dynamic object, just with property getters there. need to make 5314 it static generated tho 5315 5316 other messages may be: isPresent and getDynamic 5317 5318 5319 AND/OR what about doing it like the rails objects 5320 5321 BroadcastMessage.get(4) 5322 // various properties 5323 5324 // it lists what you updated 5325 5326 BroadcastMessage.foo().bar().put(5) 5327 +/