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