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"; 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"; 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 3521 // From sslsocket.d, but this is the maintained version! 3522 version(use_openssl) { 3523 alias SslClientSocket = OpenSslSocket; 3524 3525 // CRL = Certificate Revocation List 3526 static immutable string[] sslErrorCodes = [ 3527 "OK (code 0)", 3528 "Unspecified SSL/TLS error (code 1)", 3529 "Unable to get TLS issuer certificate (code 2)", 3530 "Unable to get TLS CRL (code 3)", 3531 "Unable to decrypt TLS certificate signature (code 4)", 3532 "Unable to decrypt TLS CRL signature (code 5)", 3533 "Unable to decode TLS issuer public key (code 6)", 3534 "TLS certificate signature failure (code 7)", 3535 "TLS CRL signature failure (code 8)", 3536 "TLS certificate not yet valid (code 9)", 3537 "TLS certificate expired (code 10)", 3538 "TLS CRL not yet valid (code 11)", 3539 "TLS CRL expired (code 12)", 3540 "TLS error in certificate not before field (code 13)", 3541 "TLS error in certificate not after field (code 14)", 3542 "TLS error in CRL last update field (code 15)", 3543 "TLS error in CRL next update field (code 16)", 3544 "TLS system out of memory (code 17)", 3545 "TLS certificate is self-signed (code 18)", 3546 "Self-signed certificate in TLS chain (code 19)", 3547 "Unable to get TLS issuer certificate locally (code 20)", 3548 "Unable to verify TLS leaf signature (code 21)", 3549 "TLS certificate chain too long (code 22)", 3550 "TLS certificate was revoked (code 23)", 3551 "TLS CA is invalid (code 24)", 3552 "TLS error: path length exceeded (code 25)", 3553 "TLS error: invalid purpose (code 26)", 3554 "TLS error: certificate untrusted (code 27)", 3555 "TLS error: certificate rejected (code 28)", 3556 ]; 3557 3558 string getOpenSslErrorCode(long error) { 3559 if(error == 62) 3560 return "TLS certificate host name mismatch"; 3561 3562 if(error < 0 || error >= sslErrorCodes.length) 3563 return "SSL/TLS error code " ~ to!string(error); 3564 return sslErrorCodes[cast(size_t) error]; 3565 } 3566 3567 struct SSL; 3568 struct SSL_CTX; 3569 struct SSL_METHOD; 3570 struct X509_STORE_CTX; 3571 enum SSL_VERIFY_NONE = 0; 3572 enum SSL_VERIFY_PEER = 1; 3573 3574 // copy it into the buf[0 .. size] and return actual length you read. 3575 // rwflag == 0 when reading, 1 when writing. 3576 extern(C) alias pem_password_cb = int function(char* buffer, int bufferSize, int rwflag, void* userPointer); 3577 extern(C) alias print_errors_cb = int function(const char*, size_t, void*); 3578 extern(C) alias client_cert_cb = int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey); 3579 extern(C) alias keylog_cb = void function(SSL*, char*); 3580 3581 struct X509; 3582 struct X509_STORE; 3583 struct EVP_PKEY; 3584 struct X509_VERIFY_PARAM; 3585 3586 import core.stdc.config; 3587 3588 enum SSL_ERROR_WANT_READ = 2; 3589 enum SSL_ERROR_WANT_WRITE = 3; 3590 3591 struct ossllib { 3592 __gshared static extern(C) { 3593 /* these are only on older openssl versions { */ 3594 int function() SSL_library_init; 3595 void function() SSL_load_error_strings; 3596 SSL_METHOD* function() SSLv23_client_method; 3597 /* } */ 3598 3599 void function(ulong, void*) OPENSSL_init_ssl; 3600 3601 SSL_CTX* function(const SSL_METHOD*) SSL_CTX_new; 3602 SSL* function(SSL_CTX*) SSL_new; 3603 int function(SSL*, int) SSL_set_fd; 3604 int function(SSL*) SSL_connect; 3605 int function(SSL*, const void*, int) SSL_write; 3606 int function(SSL*, void*, int) SSL_read; 3607 @trusted nothrow @nogc int function(SSL*) SSL_shutdown; 3608 void function(SSL*) SSL_free; 3609 void function(SSL_CTX*) SSL_CTX_free; 3610 3611 int function(const SSL*) SSL_pending; 3612 int function (const SSL *ssl, int ret) SSL_get_error; 3613 3614 void function(SSL*, int, void*) SSL_set_verify; 3615 3616 void function(SSL*, int, c_long, void*) SSL_ctrl; 3617 3618 SSL_METHOD* function() SSLv3_client_method; 3619 SSL_METHOD* function() TLS_client_method; 3620 3621 void function(SSL_CTX*, void function(SSL*, char* line)) SSL_CTX_set_keylog_callback; 3622 3623 int function(SSL_CTX*) SSL_CTX_set_default_verify_paths; 3624 3625 X509_STORE* function(SSL_CTX*) SSL_CTX_get_cert_store; 3626 c_long function(const SSL* ssl) SSL_get_verify_result; 3627 3628 X509_VERIFY_PARAM* function(const SSL*) SSL_get0_param; 3629 3630 /+ 3631 SSL_CTX_load_verify_locations 3632 SSL_CTX_set_client_CA_list 3633 +/ 3634 3635 // client cert things 3636 void function (SSL_CTX *ctx, int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey)) SSL_CTX_set_client_cert_cb; 3637 } 3638 } 3639 3640 struct eallib { 3641 __gshared static extern(C) { 3642 /* these are only on older openssl versions { */ 3643 void function() OpenSSL_add_all_ciphers; 3644 void function() OpenSSL_add_all_digests; 3645 /* } */ 3646 3647 const(char)* function(int) OpenSSL_version; 3648 3649 void function(ulong, void*) OPENSSL_init_crypto; 3650 3651 void function(print_errors_cb, void*) ERR_print_errors_cb; 3652 3653 void function(X509*) X509_free; 3654 int function(X509_STORE*, X509*) X509_STORE_add_cert; 3655 3656 3657 X509* function(FILE *fp, X509 **x, pem_password_cb *cb, void *u) PEM_read_X509; 3658 EVP_PKEY* function(FILE *fp, EVP_PKEY **x, pem_password_cb *cb, void* userPointer) PEM_read_PrivateKey; 3659 3660 EVP_PKEY* function(FILE *fp, EVP_PKEY **a) d2i_PrivateKey_fp; 3661 X509* function(FILE *fp, X509 **x) d2i_X509_fp; 3662 3663 X509* function(X509** a, const(ubyte*)* pp, c_long length) d2i_X509; 3664 int function(X509* a, ubyte** o) i2d_X509; 3665 3666 int function(X509_VERIFY_PARAM* a, const char* b, size_t l) X509_VERIFY_PARAM_set1_host; 3667 3668 X509* function(X509_STORE_CTX *ctx) X509_STORE_CTX_get_current_cert; 3669 int function(X509_STORE_CTX *ctx) X509_STORE_CTX_get_error; 3670 } 3671 } 3672 3673 struct OpenSSL { 3674 static: 3675 3676 template opDispatch(string name) { 3677 auto opDispatch(T...)(T t) { 3678 static if(__traits(hasMember, ossllib, name)) { 3679 auto ptr = __traits(getMember, ossllib, name); 3680 } else static if(__traits(hasMember, eallib, name)) { 3681 auto ptr = __traits(getMember, eallib, name); 3682 } else static assert(0); 3683 3684 if(ptr is null) 3685 throw new Exception(name ~ " not loaded"); 3686 return ptr(t); 3687 } 3688 } 3689 3690 // macros in the original C 3691 SSL_METHOD* SSLv23_client_method() { 3692 if(ossllib.SSLv23_client_method) 3693 return ossllib.SSLv23_client_method(); 3694 else 3695 return ossllib.TLS_client_method(); 3696 } 3697 3698 void SSL_set_tlsext_host_name(SSL* a, const char* b) { 3699 if(ossllib.SSL_ctrl) 3700 return ossllib.SSL_ctrl(a, 55 /*SSL_CTRL_SET_TLSEXT_HOSTNAME*/, 0 /*TLSEXT_NAMETYPE_host_name*/, cast(void*) b); 3701 else throw new Exception("SSL_set_tlsext_host_name not loaded"); 3702 } 3703 3704 // special case 3705 @trusted nothrow @nogc int SSL_shutdown(SSL* a) { 3706 if(ossllib.SSL_shutdown) 3707 return ossllib.SSL_shutdown(a); 3708 assert(0); 3709 } 3710 3711 void SSL_CTX_keylog_cb_func(SSL_CTX* ctx, keylog_cb func) { 3712 // this isn't in openssl 1.0 and is non-essential, so it is allowed to fail. 3713 if(ossllib.SSL_CTX_set_keylog_callback) 3714 ossllib.SSL_CTX_set_keylog_callback(ctx, func); 3715 //else throw new Exception("SSL_CTX_keylog_cb_func not loaded"); 3716 } 3717 3718 } 3719 3720 extern(C) 3721 int collectSslErrors(const char* ptr, size_t len, void* user) @trusted { 3722 string* s = cast(string*) user; 3723 3724 (*s) ~= ptr[0 .. len]; 3725 3726 return 0; 3727 } 3728 3729 3730 private __gshared void* ossllib_handle; 3731 version(Windows) 3732 private __gshared void* oeaylib_handle; 3733 else 3734 alias oeaylib_handle = ossllib_handle; 3735 version(Posix) 3736 private import core.sys.posix.dlfcn; 3737 else version(Windows) 3738 private import core.sys.windows.windows; 3739 3740 import core.stdc.stdio; 3741 3742 private __gshared Object loadSslMutex = new Object; 3743 private __gshared bool sslLoaded = false; 3744 3745 void loadOpenSsl() { 3746 if(sslLoaded) 3747 return; 3748 synchronized(loadSslMutex) { 3749 3750 version(Posix) { 3751 version(OSX) { 3752 static immutable string[] ossllibs = [ 3753 "libssl.46.dylib", 3754 "libssl.44.dylib", 3755 "libssl.43.dylib", 3756 "libssl.35.dylib", 3757 "libssl.1.1.dylib", 3758 "libssl.dylib", 3759 "/usr/local/opt/openssl/lib/libssl.1.0.0.dylib", 3760 ]; 3761 } else { 3762 static immutable string[] ossllibs = [ 3763 "libssl.so.3", 3764 "libssl.so.1.1", 3765 "libssl.so.1.0.2", 3766 "libssl.so.1.0.1", 3767 "libssl.so.1.0.0", 3768 "libssl.so", 3769 ]; 3770 } 3771 3772 foreach(lib; ossllibs) { 3773 ossllib_handle = dlopen(lib.ptr, RTLD_NOW); 3774 if(ossllib_handle !is null) break; 3775 } 3776 } else version(Windows) { 3777 version(X86_64) { 3778 ossllib_handle = LoadLibraryW("libssl-1_1-x64.dll"w.ptr); 3779 oeaylib_handle = LoadLibraryW("libcrypto-1_1-x64.dll"w.ptr); 3780 } 3781 3782 static immutable wstring[] ossllibs = [ 3783 "libssl-3-x64.dll"w, 3784 "libssl-3.dll"w, 3785 "libssl-1_1.dll"w, 3786 "libssl32.dll"w, 3787 ]; 3788 3789 if(ossllib_handle is null) 3790 foreach(lib; ossllibs) { 3791 ossllib_handle = LoadLibraryW(lib.ptr); 3792 if(ossllib_handle !is null) break; 3793 } 3794 3795 static immutable wstring[] eaylibs = [ 3796 "libcrypto-3-x64.dll"w, 3797 "libcrypto-3.dll"w, 3798 "libcrypto-1_1.dll"w, 3799 "libeay32.dll", 3800 ]; 3801 3802 if(oeaylib_handle is null) 3803 foreach(lib; eaylibs) { 3804 oeaylib_handle = LoadLibraryW(lib.ptr); 3805 if (oeaylib_handle !is null) break; 3806 } 3807 3808 if(ossllib_handle is null) { 3809 ossllib_handle = LoadLibraryW("ssleay32.dll"w.ptr); 3810 oeaylib_handle = ossllib_handle; 3811 } 3812 } 3813 3814 if(ossllib_handle is null) 3815 throw new Exception("libssl library not found"); 3816 if(oeaylib_handle is null) 3817 throw new Exception("libeay32 library not found"); 3818 3819 foreach(memberName; __traits(allMembers, ossllib)) { 3820 alias t = typeof(__traits(getMember, ossllib, memberName)); 3821 version(Posix) 3822 __traits(getMember, ossllib, memberName) = cast(t) dlsym(ossllib_handle, memberName); 3823 else version(Windows) { 3824 __traits(getMember, ossllib, memberName) = cast(t) GetProcAddress(ossllib_handle, memberName); 3825 } 3826 } 3827 3828 foreach(memberName; __traits(allMembers, eallib)) { 3829 alias t = typeof(__traits(getMember, eallib, memberName)); 3830 version(Posix) 3831 __traits(getMember, eallib, memberName) = cast(t) dlsym(oeaylib_handle, memberName); 3832 else version(Windows) { 3833 __traits(getMember, eallib, memberName) = cast(t) GetProcAddress(oeaylib_handle, memberName); 3834 } 3835 } 3836 3837 3838 if(ossllib.SSL_library_init) 3839 ossllib.SSL_library_init(); 3840 else if(ossllib.OPENSSL_init_ssl) 3841 ossllib.OPENSSL_init_ssl(0, null); 3842 else throw new Exception("couldn't init openssl"); 3843 3844 if(eallib.OpenSSL_add_all_ciphers) { 3845 eallib.OpenSSL_add_all_ciphers(); 3846 if(eallib.OpenSSL_add_all_digests is null) 3847 throw new Exception("no add digests"); 3848 eallib.OpenSSL_add_all_digests(); 3849 } else if(eallib.OPENSSL_init_crypto) 3850 eallib.OPENSSL_init_crypto(0 /*OPENSSL_INIT_ADD_ALL_CIPHERS and ALL_DIGESTS together*/, null); 3851 else throw new Exception("couldn't init crypto openssl"); 3852 3853 if(ossllib.SSL_load_error_strings) 3854 ossllib.SSL_load_error_strings(); 3855 else if(ossllib.OPENSSL_init_ssl) 3856 ossllib.OPENSSL_init_ssl(0x00200000L, null); 3857 else throw new Exception("couldn't load openssl errors"); 3858 3859 sslLoaded = true; 3860 } 3861 } 3862 3863 /+ 3864 // I'm just gonna let the OS clean this up on process termination because otherwise SSL_free 3865 // might have trouble being run from the GC after this module is unloaded. 3866 shared static ~this() { 3867 if(ossllib_handle) { 3868 version(Windows) { 3869 FreeLibrary(oeaylib_handle); 3870 FreeLibrary(ossllib_handle); 3871 } else version(Posix) 3872 dlclose(ossllib_handle); 3873 ossllib_handle = null; 3874 } 3875 ossllib.tupleof = ossllib.tupleof.init; 3876 } 3877 +/ 3878 3879 //pragma(lib, "crypto"); 3880 //pragma(lib, "ssl"); 3881 extern(C) 3882 void write_to_file(SSL* ssl, char* line) 3883 { 3884 import std.stdio; 3885 import std.string; 3886 import std.process : environment; 3887 string logfile = environment.get("SSLKEYLOGFILE"); 3888 if (logfile !is null) 3889 { 3890 auto f = std.stdio.File(logfile, "a+"); 3891 f.writeln(fromStringz(line)); 3892 f.close(); 3893 } 3894 } 3895 3896 class OpenSslSocket : Socket { 3897 private SSL* ssl; 3898 private SSL_CTX* ctx; 3899 private void initSsl(bool verifyPeer, string hostname) { 3900 ctx = OpenSSL.SSL_CTX_new(OpenSSL.SSLv23_client_method()); 3901 assert(ctx !is null); 3902 3903 debug OpenSSL.SSL_CTX_keylog_cb_func(ctx, &write_to_file); 3904 ssl = OpenSSL.SSL_new(ctx); 3905 3906 if(hostname.length) { 3907 OpenSSL.SSL_set_tlsext_host_name(ssl, toStringz(hostname)); 3908 if(verifyPeer) 3909 OpenSSL.X509_VERIFY_PARAM_set1_host(OpenSSL.SSL_get0_param(ssl), hostname.ptr, hostname.length); 3910 } 3911 3912 if(verifyPeer) { 3913 OpenSSL.SSL_CTX_set_default_verify_paths(ctx); 3914 3915 version(Windows) { 3916 loadCertificatesFromRegistry(ctx); 3917 } 3918 3919 OpenSSL.SSL_set_verify(ssl, SSL_VERIFY_PEER, &verifyCertificateFromRegistryArsdHttp); 3920 } else 3921 OpenSSL.SSL_set_verify(ssl, SSL_VERIFY_NONE, null); 3922 3923 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 3924 3925 3926 OpenSSL.SSL_CTX_set_client_cert_cb(ctx, &cb); 3927 } 3928 3929 extern(C) 3930 static int cb(SSL* ssl, X509** x509, EVP_PKEY** pkey) { 3931 if(HttpClient.certFilename.length && HttpClient.keyFilename.length) { 3932 FILE* fpCert = fopen((HttpClient.certFilename ~ "\0").ptr, "rb"); 3933 if(fpCert is null) 3934 return 0; 3935 scope(exit) 3936 fclose(fpCert); 3937 FILE* fpKey = fopen((HttpClient.keyFilename ~ "\0").ptr, "rb"); 3938 if(fpKey is null) 3939 return 0; 3940 scope(exit) 3941 fclose(fpKey); 3942 3943 with(CertificateFileFormat) 3944 final switch(HttpClient.certFormat) { 3945 case guess: 3946 if(HttpClient.certFilename.endsWith(".pem") || HttpClient.keyFilename.endsWith(".pem")) 3947 goto case pem; 3948 else 3949 goto case der; 3950 case pem: 3951 *x509 = OpenSSL.PEM_read_X509(fpCert, null, null, null); 3952 *pkey = OpenSSL.PEM_read_PrivateKey(fpKey, null, null, null); 3953 break; 3954 case der: 3955 *x509 = OpenSSL.d2i_X509_fp(fpCert, null); 3956 *pkey = OpenSSL.d2i_PrivateKey_fp(fpKey, null); 3957 break; 3958 } 3959 3960 return 1; 3961 } 3962 3963 return 0; 3964 } 3965 3966 bool dataPending() { 3967 return OpenSSL.SSL_pending(ssl) > 0; 3968 } 3969 3970 @trusted 3971 override void connect(Address to) { 3972 super.connect(to); 3973 if(blocking) { 3974 do_ssl_connect(); 3975 } 3976 } 3977 3978 @trusted 3979 // returns true if it is finished, false if it would have blocked, throws if there's an error 3980 int do_ssl_connect() { 3981 if(OpenSSL.SSL_connect(ssl) == -1) { 3982 3983 auto errCode = OpenSSL.SSL_get_error(ssl, -1); 3984 if(errCode == SSL_ERROR_WANT_READ || errCode == SSL_ERROR_WANT_WRITE) { 3985 return errCode; 3986 } 3987 3988 string str; 3989 OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); 3990 int i; 3991 auto err = OpenSSL.SSL_get_verify_result(ssl); 3992 //printf("wtf\n"); 3993 //scanf("%d\n", i); 3994 throw new Exception("Secure connect failed: " ~ getOpenSslErrorCode(err)); 3995 } 3996 3997 return 0; 3998 } 3999 4000 @trusted 4001 override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) { 4002 //import std.stdio;writeln(cast(string) buf); 4003 debug(arsd_http2_verbose) writeln("ssl writing ", buf.length); 4004 auto retval = OpenSSL.SSL_write(ssl, buf.ptr, cast(uint) buf.length); 4005 4006 // don't need to throw anymore since it is checked elsewhere 4007 // code useful sometimes for debugging hence commenting instead of deleting 4008 version(none) 4009 if(retval == -1) { 4010 4011 string str; 4012 OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); 4013 int i; 4014 4015 //printf("wtf\n"); 4016 //scanf("%d\n", i); 4017 4018 throw new Exception("ssl send failed " ~ str); 4019 } 4020 return retval; 4021 4022 } 4023 override ptrdiff_t send(scope const(void)[] buf) { 4024 return send(buf, SocketFlags.NONE); 4025 } 4026 @trusted 4027 override ptrdiff_t receive(scope void[] buf, SocketFlags flags) { 4028 4029 debug(arsd_http2_verbose) writeln("ssl_read before"); 4030 auto retval = OpenSSL.SSL_read(ssl, buf.ptr, cast(int)buf.length); 4031 debug(arsd_http2_verbose) writeln("ssl_read after"); 4032 4033 // don't need to throw anymore since it is checked elsewhere 4034 // code useful sometimes for debugging hence commenting instead of deleting 4035 version(none) 4036 if(retval == -1) { 4037 4038 string str; 4039 OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); 4040 int i; 4041 4042 //printf("wtf\n"); 4043 //scanf("%d\n", i); 4044 4045 throw new Exception("ssl receive failed " ~ str); 4046 } 4047 return retval; 4048 } 4049 override ptrdiff_t receive(scope void[] buf) { 4050 return receive(buf, SocketFlags.NONE); 4051 } 4052 4053 this(AddressFamily af, SocketType type = SocketType.STREAM, string hostname = null, bool verifyPeer = true) { 4054 version(Windows) __traits(getMember, this, "_blocking") = true; // lol longstanding phobos bug setting this to false on init 4055 super(af, type); 4056 initSsl(verifyPeer, hostname); 4057 } 4058 4059 override void close() scope { 4060 if(ssl) OpenSSL.SSL_shutdown(ssl); 4061 super.close(); 4062 } 4063 4064 this(socket_t sock, AddressFamily af, string hostname, bool verifyPeer = true) { 4065 super(sock, af); 4066 initSsl(verifyPeer, hostname); 4067 } 4068 4069 void freeSsl() { 4070 if(ssl is null) 4071 return; 4072 OpenSSL.SSL_free(ssl); 4073 OpenSSL.SSL_CTX_free(ctx); 4074 ssl = null; 4075 } 4076 4077 ~this() { 4078 freeSsl(); 4079 } 4080 } 4081 } 4082 4083 4084 /++ 4085 An experimental component for working with REST apis. Note that it 4086 is a zero-argument template, so to create one, use `new HttpApiClient!()(args..)` 4087 or you will get "HttpApiClient is used as a type" compile errors. 4088 4089 This will probably not work for you yet, and I might change it significantly. 4090 4091 Requires [arsd.jsvar]. 4092 4093 4094 Here's a snippet to create a pull request on GitHub to Phobos: 4095 4096 --- 4097 auto github = new HttpApiClient!()("https://api.github.com/", "your personal api token here"); 4098 4099 // create the arguments object 4100 // see: https://developer.github.com/v3/pulls/#create-a-pull-request 4101 var args = var.emptyObject; 4102 args.title = "My Pull Request"; 4103 args.head = "yourusername:" ~ branchName; 4104 args.base = "master"; 4105 // note it is ["body"] instead of .body because `body` is a D keyword 4106 args["body"] = "My cool PR is opened by the API!"; 4107 args.maintainer_can_modify = true; 4108 4109 /+ 4110 Fun fact, you can also write that: 4111 4112 var args = [ 4113 "title": "My Pull Request".var, 4114 "head": "yourusername:" ~ branchName.var, 4115 "base" : "master".var, 4116 "body" : "My cool PR is opened by the API!".var, 4117 "maintainer_can_modify": true.var 4118 ]; 4119 4120 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. 4121 +/ 4122 4123 // this translates to `repos/dlang/phobos/pulls` and sends a POST request, 4124 // containing `args` as json, then immediately grabs the json result and extracts 4125 // the value `html_url` from it. `prUrl` is typed `var`, from arsd.jsvar. 4126 auto prUrl = github.rest.repos.dlang.phobos.pulls.POST(args).result.html_url; 4127 4128 writeln("Created: ", prUrl); 4129 --- 4130 4131 Why use this instead of just building the URL? Well, of course you can! This just makes 4132 it a bit more convenient than string concatenation and manages a few headers for you. 4133 4134 Subtypes could potentially add static type checks too. 4135 +/ 4136 class HttpApiClient() { 4137 import arsd.jsvar; 4138 4139 HttpClient httpClient; 4140 4141 alias HttpApiClientType = typeof(this); 4142 4143 string urlBase; 4144 string oauth2Token; 4145 string submittedContentType; 4146 string authType = "Bearer"; 4147 4148 /++ 4149 Params: 4150 4151 urlBase = The base url for the api. Tends to be something like `https://api.example.com/v2/` or similar. 4152 oauth2Token = the authorization token for the service. You'll have to get it from somewhere else. 4153 submittedContentType = the content-type of POST, PUT, etc. bodies. 4154 httpClient = an injected http client, or null if you want to use a default-constructed one 4155 4156 History: 4157 The `httpClient` param was added on December 26, 2020. 4158 +/ 4159 this(string urlBase, string oauth2Token, string submittedContentType = "application/json", HttpClient httpClient = null) { 4160 if(httpClient is null) 4161 this.httpClient = new HttpClient(); 4162 else 4163 this.httpClient = httpClient; 4164 4165 assert(urlBase[0] == 'h'); 4166 assert(urlBase[$-1] == '/'); 4167 4168 this.urlBase = urlBase; 4169 this.oauth2Token = oauth2Token; 4170 this.submittedContentType = submittedContentType; 4171 } 4172 4173 /// 4174 static struct HttpRequestWrapper { 4175 HttpApiClientType apiClient; /// 4176 HttpRequest request; /// 4177 HttpResponse _response; 4178 4179 /// 4180 this(HttpApiClientType apiClient, HttpRequest request) { 4181 this.apiClient = apiClient; 4182 this.request = request; 4183 } 4184 4185 /// Returns the full [HttpResponse] object so you can inspect the headers 4186 @property HttpResponse response() { 4187 if(_response is HttpResponse.init) 4188 _response = request.waitForCompletion(); 4189 return _response; 4190 } 4191 4192 /++ 4193 Returns the parsed JSON from the body of the response. 4194 4195 Throws on non-2xx responses. 4196 +/ 4197 var result() { 4198 return apiClient.throwOnError(response); 4199 } 4200 4201 alias request this; 4202 } 4203 4204 /// 4205 HttpRequestWrapper request(string uri, HttpVerb requestMethod = HttpVerb.GET, ubyte[] bodyBytes = null) { 4206 if(uri[0] == '/') 4207 uri = uri[1 .. $]; 4208 4209 auto u = Uri(uri).basedOn(Uri(urlBase)); 4210 4211 auto req = httpClient.navigateTo(u, requestMethod); 4212 4213 if(oauth2Token.length) 4214 req.requestParameters.headers ~= "Authorization: "~ authType ~" " ~ oauth2Token; 4215 req.requestParameters.contentType = submittedContentType; 4216 req.requestParameters.bodyData = bodyBytes; 4217 4218 return HttpRequestWrapper(this, req); 4219 } 4220 4221 /// 4222 var throwOnError(HttpResponse res) { 4223 if(res.code < 200 || res.code >= 300) 4224 throw new Exception(res.codeText ~ " " ~ res.contentText); 4225 4226 var response = var.fromJson(res.contentText); 4227 if(response.errors) { 4228 throw new Exception(response.errors.toJson()); 4229 } 4230 4231 return response; 4232 } 4233 4234 /// 4235 @property RestBuilder rest() { 4236 return RestBuilder(this, null, null); 4237 } 4238 4239 // hipchat.rest.room["Tech Team"].history 4240 // gives: "/room/Tech%20Team/history" 4241 // 4242 // hipchat.rest.room["Tech Team"].history("page", "12) 4243 /// 4244 static struct RestBuilder { 4245 HttpApiClientType apiClient; 4246 string[] pathParts; 4247 string[2][] queryParts; 4248 this(HttpApiClientType apiClient, string[] pathParts, string[2][] queryParts) { 4249 this.apiClient = apiClient; 4250 this.pathParts = pathParts; 4251 this.queryParts = queryParts; 4252 } 4253 4254 RestBuilder _SELF() { 4255 return this; 4256 } 4257 4258 /// The args are so you can call opCall on the returned 4259 /// object, despite @property being broken af in D. 4260 RestBuilder opDispatch(string str, T)(string n, T v) { 4261 return RestBuilder(apiClient, pathParts ~ str, queryParts ~ [n, to!string(v)]); 4262 } 4263 4264 /// 4265 RestBuilder opDispatch(string str)() { 4266 return RestBuilder(apiClient, pathParts ~ str, queryParts); 4267 } 4268 4269 4270 /// 4271 RestBuilder opIndex(string str) { 4272 return RestBuilder(apiClient, pathParts ~ str, queryParts); 4273 } 4274 /// 4275 RestBuilder opIndex(var str) { 4276 return RestBuilder(apiClient, pathParts ~ str.get!string, queryParts); 4277 } 4278 /// 4279 RestBuilder opIndex(int i) { 4280 return RestBuilder(apiClient, pathParts ~ to!string(i), queryParts); 4281 } 4282 4283 /// 4284 RestBuilder opCall(T)(string name, T value) { 4285 return RestBuilder(apiClient, pathParts, queryParts ~ [name, to!string(value)]); 4286 } 4287 4288 /// 4289 string toUri() { 4290 string result; 4291 foreach(idx, part; pathParts) { 4292 if(idx) 4293 result ~= "/"; 4294 result ~= encodeUriComponent(part); 4295 } 4296 result ~= "?"; 4297 foreach(idx, part; queryParts) { 4298 if(idx) 4299 result ~= "&"; 4300 result ~= encodeUriComponent(part[0]); 4301 result ~= "="; 4302 result ~= encodeUriComponent(part[1]); 4303 } 4304 4305 return result; 4306 } 4307 4308 /// 4309 final HttpRequestWrapper GET() { return _EXECUTE(HttpVerb.GET, this.toUri(), ToBytesResult.init); } 4310 /// ditto 4311 final HttpRequestWrapper DELETE() { return _EXECUTE(HttpVerb.DELETE, this.toUri(), ToBytesResult.init); } 4312 4313 // need to be able to send: JSON, urlencoded, multipart/form-data, and raw stuff. 4314 /// ditto 4315 final HttpRequestWrapper POST(T...)(T t) { return _EXECUTE(HttpVerb.POST, this.toUri(), toBytes(t)); } 4316 /// ditto 4317 final HttpRequestWrapper PATCH(T...)(T t) { return _EXECUTE(HttpVerb.PATCH, this.toUri(), toBytes(t)); } 4318 /// ditto 4319 final HttpRequestWrapper PUT(T...)(T t) { return _EXECUTE(HttpVerb.PUT, this.toUri(), toBytes(t)); } 4320 4321 struct ToBytesResult { 4322 ubyte[] bytes; 4323 string contentType; 4324 } 4325 4326 private ToBytesResult toBytes(T...)(T t) { 4327 import std.conv : to; 4328 static if(T.length == 0) 4329 return ToBytesResult(null, null); 4330 else static if(T.length == 1 && is(T[0] == var)) 4331 return ToBytesResult(cast(ubyte[]) t[0].toJson(), "application/json"); // json data 4332 else static if(T.length == 1 && (is(T[0] == string) || is(T[0] == ubyte[]))) 4333 return ToBytesResult(cast(ubyte[]) t[0], null); // raw data 4334 else static if(T.length == 1 && is(T[0] : FormData)) 4335 return ToBytesResult(t[0].toBytes, t[0].contentType); 4336 else static if(T.length > 1 && T.length % 2 == 0 && is(T[0] == string)) { 4337 // string -> value pairs for a POST request 4338 string answer; 4339 foreach(idx, val; t) { 4340 static if(idx % 2 == 0) { 4341 if(answer.length) 4342 answer ~= "&"; 4343 answer ~= encodeUriComponent(val); // it had better be a string! lol 4344 answer ~= "="; 4345 } else { 4346 answer ~= encodeUriComponent(to!string(val)); 4347 } 4348 } 4349 4350 return ToBytesResult(cast(ubyte[]) answer, "application/x-www-form-urlencoded"); 4351 } 4352 else 4353 static assert(0); // FIXME 4354 4355 } 4356 4357 HttpRequestWrapper _EXECUTE(HttpVerb verb, string uri, ubyte[] bodyBytes) { 4358 return apiClient.request(uri, verb, bodyBytes); 4359 } 4360 4361 HttpRequestWrapper _EXECUTE(HttpVerb verb, string uri, ToBytesResult tbr) { 4362 auto r = apiClient.request(uri, verb, tbr.bytes); 4363 if(tbr.contentType !is null) 4364 r.requestParameters.contentType = tbr.contentType; 4365 return r; 4366 } 4367 } 4368 } 4369 4370 4371 // see also: arsd.cgi.encodeVariables 4372 /++ 4373 Creates a multipart/form-data object that is suitable for file uploads and other kinds of POST. 4374 4375 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. 4376 4377 You can pass this directly to [HttpClient.request]. 4378 4379 Based on: https://developer.mozilla.org/en-US/docs/Web/API/FormData 4380 4381 --- 4382 auto fd = new FormData(); 4383 // add some data, plain string first 4384 fd.append("name", "Adam"); 4385 // then a file 4386 fd.append("photo", std.file.read("adam.jpg"), "image/jpeg", "adam.jpg"); 4387 4388 // post it! 4389 auto client = new HttpClient(); 4390 client.request(Uri("http://example.com/people"), fd).waitForCompletion(); 4391 --- 4392 4393 History: 4394 Added June 8, 2018 4395 +/ 4396 class FormData { 4397 static struct MimePart { 4398 string name; 4399 const(void)[] data; 4400 string contentType; 4401 string filename; 4402 } 4403 4404 private MimePart[] parts; 4405 private string boundary = "0016e64be86203dd36047610926a"; // FIXME 4406 4407 /++ 4408 Appends the given entry to the request. This can be a simple key/value pair of strings or file uploads. 4409 4410 For a simple key/value pair, leave `contentType` and `filename` as `null`. 4411 4412 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. 4413 4414 The `contentType` is generally verified by servers for file uploads. 4415 +/ 4416 void append(string key, const(void)[] value, string contentType = null, string filename = null) { 4417 parts ~= MimePart(key, value, contentType, filename); 4418 } 4419 4420 /++ 4421 Deletes any entries from the set with the given key. 4422 4423 History: 4424 Added June 7, 2023 (dub v11.0) 4425 +/ 4426 void deleteKey(string key) { 4427 MimePart[] newParts; 4428 foreach(part; parts) 4429 if(part.name != key) 4430 newParts ~= part; 4431 parts = newParts; 4432 } 4433 4434 /++ 4435 Returns the first entry with the given key, or `MimePart.init` if there is nothing. 4436 4437 History: 4438 Added June 7, 2023 (dub v11.0) 4439 +/ 4440 MimePart get(string key) { 4441 foreach(part; parts) 4442 if(part.name == key) 4443 return part; 4444 return MimePart.init; 4445 } 4446 4447 /++ 4448 Returns the all entries with the given key. 4449 4450 History: 4451 Added June 7, 2023 (dub v11.0) 4452 +/ 4453 MimePart[] getAll(string key) { 4454 MimePart[] answer; 4455 foreach(part; parts) 4456 if(part.name == key) 4457 answer ~= part; 4458 return answer; 4459 } 4460 4461 /++ 4462 Returns true if the given key exists in the set. 4463 4464 History: 4465 Added June 7, 2023 (dub v11.0) 4466 +/ 4467 bool has(string key) { 4468 return get(key).name == key; 4469 } 4470 4471 /++ 4472 Sets the given key to the given value if it exists, or appends it if it doesn't. 4473 4474 You probably want [append] instead. 4475 4476 See_Also: 4477 [append] 4478 4479 History: 4480 Added June 7, 2023 (dub v11.0) 4481 +/ 4482 void set(string key, const(void)[] value, string contentType, string filename) { 4483 foreach(ref part; parts) 4484 if(part.name == key) { 4485 part.data = value; 4486 part.contentType = contentType; 4487 part.filename = filename; 4488 return; 4489 } 4490 4491 append(key, value, contentType, filename); 4492 } 4493 4494 /++ 4495 Returns all the current entries in the object. 4496 4497 History: 4498 Added June 7, 2023 (dub v11.0) 4499 +/ 4500 MimePart[] entries() { 4501 return parts; 4502 } 4503 4504 // FIXME: 4505 // keys iterator 4506 // values iterator 4507 4508 /++ 4509 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. 4510 +/ 4511 string contentType() { 4512 return "multipart/form-data; boundary=" ~ boundary; 4513 } 4514 4515 /++ 4516 Returns bytes applicable for the body of this request. Use the [contentType] method to get the appropriate content type header with the right boundary. 4517 +/ 4518 ubyte[] toBytes() { 4519 string data; 4520 4521 foreach(part; parts) { 4522 data ~= "--" ~ boundary ~ "\r\n"; 4523 data ~= "Content-Disposition: form-data; name=\""~part.name~"\""; 4524 if(part.filename !is null) 4525 data ~= "; filename=\""~part.filename~"\""; 4526 data ~= "\r\n"; 4527 if(part.contentType !is null) 4528 data ~= "Content-Type: " ~ part.contentType ~ "\r\n"; 4529 data ~= "\r\n"; 4530 4531 data ~= cast(string) part.data; 4532 4533 data ~= "\r\n"; 4534 } 4535 4536 data ~= "--" ~ boundary ~ "--\r\n"; 4537 4538 return cast(ubyte[]) data; 4539 } 4540 } 4541 4542 private bool bicmp(in ubyte[] item, in char[] search) { 4543 if(item.length != search.length) return false; 4544 4545 foreach(i; 0 .. item.length) { 4546 ubyte a = item[i]; 4547 ubyte b = search[i]; 4548 if(a >= 'A' && a <= 'Z') 4549 a += 32; 4550 //if(b >= 'A' && b <= 'Z') 4551 //b += 32; 4552 if(a != b) 4553 return false; 4554 } 4555 4556 return true; 4557 } 4558 4559 /++ 4560 WebSocket client, based on the browser api, though also with other api options. 4561 4562 --- 4563 import arsd.http2; 4564 4565 void main() { 4566 auto ws = new WebSocket(Uri("ws://....")); 4567 4568 ws.onmessage = (in char[] msg) { 4569 ws.send("a reply"); 4570 }; 4571 4572 ws.connect(); 4573 4574 WebSocket.eventLoop(); 4575 } 4576 --- 4577 4578 Symbol_groups: 4579 foundational = 4580 Used with all API styles. 4581 4582 browser_api = 4583 API based on the standard in the browser. 4584 4585 event_loop_integration = 4586 Integrating with external event loops is done through static functions. You should 4587 call these BEFORE doing anything else with the WebSocket module or class. 4588 4589 $(PITFALL NOT IMPLEMENTED) 4590 --- 4591 WebSocket.setEventLoopProxy(arsd.simpledisplay.EventLoop.proxy.tupleof); 4592 // or something like that. it is not implemented yet. 4593 --- 4594 $(PITFALL NOT IMPLEMENTED) 4595 4596 blocking_api = 4597 The blocking API is best used when you only need basic functionality with a single connection. 4598 4599 --- 4600 WebSocketFrame msg; 4601 do { 4602 // FIXME good demo 4603 } while(msg); 4604 --- 4605 4606 Or to check for blocks before calling: 4607 4608 --- 4609 try_to_process_more: 4610 while(ws.isMessageBuffered()) { 4611 auto msg = ws.waitForNextMessage(); 4612 // process msg 4613 } 4614 if(ws.isDataPending()) { 4615 ws.lowLevelReceive(); 4616 goto try_to_process_more; 4617 } else { 4618 // nothing ready, you can do other things 4619 // or at least sleep a while before trying 4620 // to process more. 4621 if(ws.readyState == WebSocket.OPEN) { 4622 Thread.sleep(1.seconds); 4623 goto try_to_process_more; 4624 } 4625 } 4626 --- 4627 4628 +/ 4629 class WebSocket { 4630 private Uri uri; 4631 private string[string] cookies; 4632 4633 private string host; 4634 private ushort port; 4635 private bool ssl; 4636 4637 // used to decide if we mask outgoing msgs 4638 private bool isClient; 4639 4640 private MonoTime timeoutFromInactivity; 4641 private MonoTime nextPing; 4642 4643 /++ 4644 wss://echo.websocket.org 4645 +/ 4646 /// Group: foundational 4647 this(Uri uri, Config config = Config.init) 4648 //in (uri.scheme == "ws" || uri.scheme == "wss") 4649 in { assert(uri.scheme == "ws" || uri.scheme == "wss"); } do 4650 { 4651 this.uri = uri; 4652 this.config = config; 4653 4654 this.receiveBuffer = new ubyte[](config.initialReceiveBufferSize); 4655 4656 host = uri.host; 4657 ssl = uri.scheme == "wss"; 4658 port = cast(ushort) (uri.port ? uri.port : ssl ? 443 : 80); 4659 4660 if(ssl) { 4661 version(with_openssl) { 4662 loadOpenSsl(); 4663 socket = new SslClientSocket(family(uri.unixSocketPath), SocketType.STREAM, host, config.verifyPeer); 4664 } else 4665 throw new Exception("SSL not compiled in"); 4666 } else 4667 socket = new Socket(family(uri.unixSocketPath), SocketType.STREAM); 4668 4669 socket.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); 4670 cookies = config.cookies; 4671 } 4672 4673 /++ 4674 4675 +/ 4676 /// Group: foundational 4677 void connect() { 4678 this.isClient = true; 4679 4680 socket.blocking = false; 4681 4682 if(uri.unixSocketPath) 4683 socket.connect(new UnixAddress(uri.unixSocketPath)); 4684 else 4685 socket.connect(new InternetAddress(host, port)); // FIXME: ipv6 support... 4686 4687 4688 auto readSet = new SocketSet(); 4689 auto writeSet = new SocketSet(); 4690 4691 readSet.reset(); 4692 writeSet.reset(); 4693 4694 readSet.add(socket); 4695 writeSet.add(socket); 4696 4697 auto selectGot = Socket.select(readSet, writeSet, null, config.timeoutFromInactivity); 4698 if(selectGot == -1) { 4699 // interrupted 4700 4701 throw new Exception("Websocket connection interrupted - retry might succeed"); 4702 } else if(selectGot == 0) { 4703 // time out 4704 socket.close(); 4705 throw new Exception("Websocket connection timed out"); 4706 } else { 4707 if(writeSet.isSet(socket) || readSet.isSet(socket)) { 4708 import core.stdc.stdint; 4709 int32_t error; 4710 int retopt = socket.getOption(SocketOptionLevel.SOCKET, SocketOption.ERROR, error); 4711 if(retopt < 0 || error != 0) { 4712 socket.close(); 4713 throw new Exception("Websocket connection failed - " ~ formatSocketError(error)); 4714 } else { 4715 // FIXME: websocket handshake could and really should be async too. 4716 socket.blocking = true; // just convenience 4717 if(auto s = cast(SslClientSocket) socket) { 4718 s.do_ssl_connect(); 4719 } else { 4720 // we're ready 4721 } 4722 } 4723 } 4724 } 4725 4726 auto uri = this.uri.path.length ? this.uri.path : "/"; 4727 if(this.uri.query.length) { 4728 uri ~= "?"; 4729 uri ~= this.uri.query; 4730 } 4731 4732 // the headers really shouldn't be bigger than this, at least 4733 // the chunks i need to process 4734 ubyte[4096] bufferBacking = void; 4735 ubyte[] buffer = bufferBacking[]; 4736 size_t pos; 4737 4738 void append(in char[][] items...) { 4739 foreach(what; items) { 4740 if((pos + what.length) > buffer.length) { 4741 buffer.length += 4096; 4742 } 4743 buffer[pos .. pos + what.length] = cast(ubyte[]) what[]; 4744 pos += what.length; 4745 } 4746 } 4747 4748 append("GET ", uri, " HTTP/1.1\r\n"); 4749 append("Host: ", this.uri.host, "\r\n"); 4750 4751 append("Upgrade: websocket\r\n"); 4752 append("Connection: Upgrade\r\n"); 4753 append("Sec-WebSocket-Version: 13\r\n"); 4754 4755 // FIXME: randomize this 4756 append("Sec-WebSocket-Key: x3JEHMbDL1EzLkh9GBhXDw==\r\n"); 4757 if(cookies.length > 0) { 4758 append("Cookie: "); 4759 bool first=true; 4760 foreach(k,v;cookies) { 4761 if(first) first = false; 4762 else append("; "); 4763 append(k); 4764 append("="); 4765 append(v); 4766 } 4767 append("\r\n"); 4768 } 4769 /* 4770 //This is equivalent but has dependencies 4771 import std.format; 4772 import std.algorithm : map; 4773 append(format("cookie: %-(%s %)\r\n",cookies.byKeyValue.map!(t=>format("%s=%s",t.key,t.value)))); 4774 */ 4775 4776 if(config.protocol.length) 4777 append("Sec-WebSocket-Protocol: ", config.protocol, "\r\n"); 4778 if(config.origin.length) 4779 append("Origin: ", config.origin, "\r\n"); 4780 4781 foreach(h; config.additionalHeaders) { 4782 append(h); 4783 append("\r\n"); 4784 } 4785 4786 append("\r\n"); 4787 4788 auto remaining = buffer[0 .. pos]; 4789 //import std.stdio; writeln(host, " " , port, " ", cast(string) remaining); 4790 while(remaining.length) { 4791 auto r = socket.send(remaining); 4792 if(r < 0) 4793 throw new Exception(lastSocketError()); 4794 if(r == 0) 4795 throw new Exception("unexpected connection termination"); 4796 remaining = remaining[r .. $]; 4797 } 4798 4799 // the response shouldn't be especially large at this point, just 4800 // headers for the most part. gonna try to get it in the stack buffer. 4801 // then copy stuff after headers, if any, to the frame buffer. 4802 ubyte[] used; 4803 4804 void more() { 4805 auto r = socket.receive(buffer[used.length .. $]); 4806 4807 if(r < 0) 4808 throw new Exception(lastSocketError()); 4809 if(r == 0) 4810 throw new Exception("unexpected connection termination"); 4811 //import std.stdio;writef("%s", cast(string) buffer[used.length .. used.length + r]); 4812 4813 used = buffer[0 .. used.length + r]; 4814 } 4815 4816 more(); 4817 4818 import std.algorithm; 4819 if(!used.startsWith(cast(ubyte[]) "HTTP/1.1 101")) 4820 throw new Exception("didn't get a websocket answer"); 4821 // skip the status line 4822 while(used.length && used[0] != '\n') 4823 used = used[1 .. $]; 4824 4825 if(used.length == 0) 4826 throw new Exception("Remote server disconnected or didn't send enough information"); 4827 4828 if(used.length < 1) 4829 more(); 4830 4831 used = used[1 .. $]; // skip the \n 4832 4833 if(used.length == 0) 4834 more(); 4835 4836 // checks on the protocol from ehaders 4837 bool isWebsocket; 4838 bool isUpgrade; 4839 const(ubyte)[] protocol; 4840 const(ubyte)[] accept; 4841 4842 while(used.length) { 4843 if(used.length >= 2 && used[0] == '\r' && used[1] == '\n') { 4844 used = used[2 .. $]; 4845 break; // all done 4846 } 4847 int idxColon; 4848 while(idxColon < used.length && used[idxColon] != ':') 4849 idxColon++; 4850 if(idxColon == used.length) 4851 more(); 4852 auto idxStart = idxColon + 1; 4853 while(idxStart < used.length && used[idxStart] == ' ') 4854 idxStart++; 4855 if(idxStart == used.length) 4856 more(); 4857 auto idxEnd = idxStart; 4858 while(idxEnd < used.length && used[idxEnd] != '\r') 4859 idxEnd++; 4860 if(idxEnd == used.length) 4861 more(); 4862 4863 auto headerName = used[0 .. idxColon]; 4864 auto headerValue = used[idxStart .. idxEnd]; 4865 4866 // move past this header 4867 used = used[idxEnd .. $]; 4868 // and the \r\n 4869 if(2 <= used.length) 4870 used = used[2 .. $]; 4871 4872 if(headerName.bicmp("upgrade")) { 4873 if(headerValue.bicmp("websocket")) 4874 isWebsocket = true; 4875 } else if(headerName.bicmp("connection")) { 4876 if(headerValue.bicmp("upgrade")) 4877 isUpgrade = true; 4878 } else if(headerName.bicmp("sec-websocket-accept")) { 4879 accept = headerValue; 4880 } else if(headerName.bicmp("sec-websocket-protocol")) { 4881 protocol = headerValue; 4882 } 4883 4884 if(!used.length) { 4885 more(); 4886 } 4887 } 4888 4889 4890 if(!isWebsocket) 4891 throw new Exception("didn't answer as websocket"); 4892 if(!isUpgrade) 4893 throw new Exception("didn't answer as upgrade"); 4894 4895 4896 // FIXME: check protocol if config requested one 4897 // FIXME: check accept for the right hash 4898 4899 receiveBuffer[0 .. used.length] = used[]; 4900 receiveBufferUsedLength = used.length; 4901 4902 readyState_ = OPEN; 4903 4904 if(onopen) 4905 onopen(); 4906 4907 nextPing = MonoTime.currTime + config.pingFrequency.msecs; 4908 timeoutFromInactivity = MonoTime.currTime + config.timeoutFromInactivity; 4909 4910 registerActiveSocket(this); 4911 } 4912 4913 /++ 4914 Is data pending on the socket? Also check [isMessageBuffered] to see if there 4915 is already a message in memory too. 4916 4917 If this returns `true`, you can call [lowLevelReceive], then try [isMessageBuffered] 4918 again. 4919 +/ 4920 /// Group: blocking_api 4921 public bool isDataPending(Duration timeout = 0.seconds) { 4922 static SocketSet readSet; 4923 if(readSet is null) 4924 readSet = new SocketSet(); 4925 4926 version(with_openssl) 4927 if(auto s = cast(SslClientSocket) socket) { 4928 // select doesn't handle the case with stuff 4929 // left in the ssl buffer so i'm checking it separately 4930 if(s.dataPending()) { 4931 return true; 4932 } 4933 } 4934 4935 readSet.reset(); 4936 4937 readSet.add(socket); 4938 4939 //tryAgain: 4940 auto selectGot = Socket.select(readSet, null, null, timeout); 4941 if(selectGot == 0) { /* timeout */ 4942 // timeout 4943 return false; 4944 } else if(selectGot == -1) { /* interrupted */ 4945 return false; 4946 } else { /* ready */ 4947 if(readSet.isSet(socket)) { 4948 return true; 4949 } 4950 } 4951 4952 return false; 4953 } 4954 4955 private void llsend(ubyte[] d) { 4956 if(readyState == CONNECTING) 4957 throw new Exception("WebSocket not connected when trying to send. Did you forget to call connect(); ?"); 4958 //connect(); 4959 //import std.stdio; writeln("LLSEND: ", d); 4960 while(d.length) { 4961 auto r = socket.send(d); 4962 if(r < 0 && wouldHaveBlocked()) { 4963 // FIXME: i should register for a write wakeup 4964 version(use_arsd_core) assert(0); 4965 import core.thread; 4966 Thread.sleep(1.msecs); 4967 continue; 4968 } 4969 //import core.stdc.errno; import std.stdio; writeln(errno); 4970 if(r <= 0) { 4971 // import std.stdio; writeln(GetLastError()); 4972 throw new Exception("Socket send failed"); 4973 } 4974 d = d[r .. $]; 4975 } 4976 } 4977 4978 private void llclose() { 4979 // import std.stdio; writeln("LLCLOSE"); 4980 socket.shutdown(SocketShutdown.SEND); 4981 } 4982 4983 /++ 4984 Waits for more data off the low-level socket and adds it to the pending buffer. 4985 4986 Returns `true` if the connection is still active. 4987 +/ 4988 /// Group: blocking_api 4989 public bool lowLevelReceive() { 4990 if(readyState == CONNECTING) 4991 throw new Exception("WebSocket not connected when trying to receive. Did you forget to call connect(); ?"); 4992 if (receiveBufferUsedLength == receiveBuffer.length) 4993 { 4994 if (receiveBuffer.length == config.maximumReceiveBufferSize) 4995 throw new Exception("Maximum receive buffer size exhausted"); 4996 4997 import std.algorithm : min; 4998 receiveBuffer.length = min(receiveBuffer.length + config.initialReceiveBufferSize, 4999 config.maximumReceiveBufferSize); 5000 } 5001 auto r = socket.receive(receiveBuffer[receiveBufferUsedLength .. $]); 5002 if(r == 0) 5003 return false; 5004 if(r < 0 && wouldHaveBlocked()) 5005 return true; 5006 if(r <= 0) { 5007 //import std.stdio; writeln(WSAGetLastError()); 5008 return false; 5009 } 5010 receiveBufferUsedLength += r; 5011 return true; 5012 } 5013 5014 private Socket socket; 5015 5016 /* copy/paste section { */ 5017 5018 private int readyState_; 5019 private ubyte[] receiveBuffer; 5020 private size_t receiveBufferUsedLength; 5021 5022 private Config config; 5023 5024 enum CONNECTING = 0; /// Socket has been created. The connection is not yet open. 5025 enum OPEN = 1; /// The connection is open and ready to communicate. 5026 enum CLOSING = 2; /// The connection is in the process of closing. 5027 enum CLOSED = 3; /// The connection is closed or couldn't be opened. 5028 5029 /++ 5030 5031 +/ 5032 /// Group: foundational 5033 static struct Config { 5034 /++ 5035 These control the size of the receive buffer. 5036 5037 It starts at the initial size, will temporarily 5038 balloon up to the maximum size, and will reuse 5039 a buffer up to the likely size. 5040 5041 Anything larger than the maximum size will cause 5042 the connection to be aborted and an exception thrown. 5043 This is to protect you against a peer trying to 5044 exhaust your memory, while keeping the user-level 5045 processing simple. 5046 +/ 5047 size_t initialReceiveBufferSize = 4096; 5048 size_t likelyReceiveBufferSize = 4096; /// ditto 5049 size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto 5050 5051 /++ 5052 Maximum combined size of a message. 5053 +/ 5054 size_t maximumMessageSize = 10 * 1024 * 1024; 5055 5056 string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value; 5057 string origin; /// Origin URL to send with the handshake, if desired. 5058 string protocol; /// the protocol header, if desired. 5059 5060 /++ 5061 Additional headers to put in the HTTP request. These should be formatted `Name: value`, like for example: 5062 5063 --- 5064 Config config; 5065 config.additionalHeaders ~= "Authorization: Bearer your_auth_token_here"; 5066 --- 5067 5068 History: 5069 Added February 19, 2021 (included in dub version 9.2) 5070 +/ 5071 string[] additionalHeaders; 5072 5073 /++ 5074 Amount of time (in msecs) of idleness after which to send an automatic ping 5075 5076 Please note how this interacts with [timeoutFromInactivity] - a ping counts as activity that 5077 keeps the socket alive. 5078 +/ 5079 int pingFrequency = 5000; 5080 5081 /++ 5082 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. 5083 5084 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! 5085 5086 History: 5087 Added March 31, 2021 (included in dub version 9.4) 5088 +/ 5089 Duration timeoutFromInactivity = 1.minutes; 5090 5091 /++ 5092 For https connections, if this is `true`, it will fail to connect if the TLS certificate can not be 5093 verified. Setting this to `false` will skip this check and allow the connection to continue anyway. 5094 5095 History: 5096 Added April 5, 2022 (dub v10.8) 5097 5098 Prior to this, it always used the global (but undocumented) `defaultVerifyPeer` setting, and sometimes 5099 even if it was true, it would skip the verification. Now, it always respects this local setting. 5100 +/ 5101 bool verifyPeer = true; 5102 } 5103 5104 /++ 5105 Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED]. 5106 +/ 5107 int readyState() { 5108 return readyState_; 5109 } 5110 5111 /++ 5112 Closes the connection, sending a graceful teardown message to the other side. 5113 If you provide no arguments, it sends code 1000, normal closure. If you provide 5114 a code, you should also provide a short reason string. 5115 5116 Params: 5117 code = reason code. 5118 5119 0-999 are invalid. 5120 1000-2999 are defined by the RFC. [https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1] 5121 1000 - normal finish 5122 1001 - endpoint going away 5123 1002 - protocol error 5124 1003 - unacceptable data received (e.g. binary message when you can't handle it) 5125 1004 - reserved 5126 1005 - missing status code (should not be set except by implementations) 5127 1006 - abnormal connection closure (should only be set by implementations) 5128 1007 - inconsistent data received (i.e. utf-8 decode error in text message) 5129 1008 - policy violation 5130 1009 - received message too big 5131 1010 - client aborting due to required extension being unsupported by the server 5132 1011 - server had unexpected failure 5133 1015 - reserved for TLS handshake failure 5134 3000-3999 are to be registered with IANA. 5135 4000-4999 are private-use custom codes depending on the application. These are what you'd most commonly set here. 5136 5137 reason = <= 123 bytes of human-readable reason text, used for logs and debugging 5138 5139 History: 5140 The default `code` was changed to 1000 on January 9, 2023. Previously it was 0, 5141 but also ignored anyway. 5142 5143 On May 11, 2024, the optional arguments were changed to overloads since if you provide a code, you should also provide a reason. 5144 +/ 5145 /// Group: foundational 5146 void close() { 5147 close(1000, null); 5148 } 5149 5150 /// ditto 5151 void close(int code, string reason) 5152 //in (reason.length < 123) 5153 in { assert(reason.length <= 123); } do 5154 { 5155 if(readyState_ != OPEN) 5156 return; // it cool, we done 5157 WebSocketFrame wss; 5158 wss.fin = true; 5159 wss.masked = this.isClient; 5160 wss.opcode = WebSocketOpcode.close; 5161 wss.data = [ubyte((code >> 8) & 0xff), ubyte(code & 0xff)] ~ cast(ubyte[]) reason.dup; 5162 wss.send(&llsend); 5163 5164 readyState_ = CLOSING; 5165 5166 closeCalled = true; 5167 5168 llclose(); 5169 } 5170 5171 deprecated("If you provide a code, please also provide a reason string") void close(int code) { 5172 close(code, null); 5173 } 5174 5175 5176 private bool closeCalled; 5177 5178 /++ 5179 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. 5180 +/ 5181 /// Group: foundational 5182 void ping(in ubyte[] data = null) { 5183 WebSocketFrame wss; 5184 wss.fin = true; 5185 wss.masked = this.isClient; 5186 wss.opcode = WebSocketOpcode.ping; 5187 if(data !is null) wss.data = data.dup; 5188 wss.send(&llsend); 5189 } 5190 5191 /++ 5192 Sends a pong message to the server. This is normally done automatically in response to pings. 5193 +/ 5194 /// Group: foundational 5195 void pong(in ubyte[] data = null) { 5196 WebSocketFrame wss; 5197 wss.fin = true; 5198 wss.masked = this.isClient; 5199 wss.opcode = WebSocketOpcode.pong; 5200 if(data !is null) wss.data = data.dup; 5201 wss.send(&llsend); 5202 } 5203 5204 /++ 5205 Sends a text message through the websocket. 5206 +/ 5207 /// Group: foundational 5208 void send(in char[] textData) { 5209 WebSocketFrame wss; 5210 wss.fin = true; 5211 wss.masked = this.isClient; 5212 wss.opcode = WebSocketOpcode.text; 5213 wss.data = cast(ubyte[]) textData.dup; 5214 wss.send(&llsend); 5215 } 5216 5217 /++ 5218 Sends a binary message through the websocket. 5219 +/ 5220 /// Group: foundational 5221 void send(in ubyte[] binaryData) { 5222 WebSocketFrame wss; 5223 wss.masked = this.isClient; 5224 wss.fin = true; 5225 wss.opcode = WebSocketOpcode.binary; 5226 wss.data = cast(ubyte[]) binaryData.dup; 5227 wss.send(&llsend); 5228 } 5229 5230 /++ 5231 Waits for and returns the next complete message on the socket. 5232 5233 Note that the onmessage function is still called, right before 5234 this returns. 5235 +/ 5236 /// Group: blocking_api 5237 public WebSocketFrame waitForNextMessage() { 5238 do { 5239 auto m = processOnce(); 5240 if(m.populated) 5241 return m; 5242 } while(lowLevelReceive()); 5243 5244 return WebSocketFrame.init; // FIXME? maybe. 5245 } 5246 5247 /++ 5248 Tells if [waitForNextMessage] would block. 5249 +/ 5250 /// Group: blocking_api 5251 public bool waitForNextMessageWouldBlock() { 5252 checkAgain: 5253 if(isMessageBuffered()) 5254 return false; 5255 if(!isDataPending()) 5256 return true; 5257 while(isDataPending()) 5258 if(lowLevelReceive() == false) 5259 return false; 5260 goto checkAgain; 5261 } 5262 5263 /++ 5264 Is there a message in the buffer already? 5265 If `true`, [waitForNextMessage] is guaranteed to return immediately. 5266 If `false`, check [isDataPending] as the next step. 5267 +/ 5268 /// Group: blocking_api 5269 public bool isMessageBuffered() { 5270 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 5271 auto s = d; 5272 if(d.length) { 5273 auto orig = d; 5274 auto m = WebSocketFrame.read(d); 5275 // that's how it indicates that it needs more data 5276 if(d !is orig) 5277 return true; 5278 } 5279 5280 return false; 5281 } 5282 5283 private ubyte continuingType; 5284 private ubyte[] continuingData; 5285 //private size_t continuingDataLength; 5286 5287 private WebSocketFrame processOnce() { 5288 ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; 5289 auto s = d; 5290 // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer. 5291 WebSocketFrame m; 5292 if(d.length) { 5293 auto orig = d; 5294 m = WebSocketFrame.read(d); 5295 // that's how it indicates that it needs more data 5296 if(d is orig) 5297 return WebSocketFrame.init; 5298 m.unmaskInPlace(); 5299 switch(m.opcode) { 5300 case WebSocketOpcode.continuation: 5301 if(continuingData.length + m.data.length > config.maximumMessageSize) 5302 throw new Exception("message size exceeded"); 5303 5304 continuingData ~= m.data; 5305 if(m.fin) { 5306 if(ontextmessage) 5307 ontextmessage(cast(char[]) continuingData); 5308 if(onbinarymessage) 5309 onbinarymessage(continuingData); 5310 5311 continuingData = null; 5312 } 5313 break; 5314 case WebSocketOpcode.text: 5315 if(m.fin) { 5316 if(ontextmessage) 5317 ontextmessage(m.textData); 5318 } else { 5319 continuingType = m.opcode; 5320 //continuingDataLength = 0; 5321 continuingData = null; 5322 continuingData ~= m.data; 5323 } 5324 break; 5325 case WebSocketOpcode.binary: 5326 if(m.fin) { 5327 if(onbinarymessage) 5328 onbinarymessage(m.data); 5329 } else { 5330 continuingType = m.opcode; 5331 //continuingDataLength = 0; 5332 continuingData = null; 5333 continuingData ~= m.data; 5334 } 5335 break; 5336 case WebSocketOpcode.close: 5337 5338 //import std.stdio; writeln("closed ", cast(string) m.data); 5339 5340 ushort code = CloseEvent.StandardCloseCodes.noStatusCodePresent; 5341 const(char)[] reason; 5342 5343 if(m.data.length >= 2) { 5344 code = (m.data[0] << 8) | m.data[1]; 5345 reason = (cast(char[]) m.data[2 .. $]); 5346 } 5347 5348 if(onclose) 5349 onclose(CloseEvent(code, reason, true)); 5350 5351 // if we receive one and haven't sent one back we're supposed to echo it back and close. 5352 if(!closeCalled) 5353 close(code, reason.idup); 5354 5355 readyState_ = CLOSED; 5356 5357 unregisterActiveSocket(this); 5358 socket.close(); 5359 break; 5360 case WebSocketOpcode.ping: 5361 // import std.stdio; writeln("ping received ", m.data); 5362 pong(m.data); 5363 break; 5364 case WebSocketOpcode.pong: 5365 // import std.stdio; writeln("pong received ", m.data); 5366 // just really references it is still alive, nbd. 5367 break; 5368 default: // ignore though i could and perhaps should throw too 5369 } 5370 } 5371 5372 if(d.length) { 5373 m.data = m.data.dup(); 5374 } 5375 5376 import core.stdc.string; 5377 memmove(receiveBuffer.ptr, d.ptr, d.length); 5378 receiveBufferUsedLength = d.length; 5379 5380 return m; 5381 } 5382 5383 private void autoprocess() { 5384 // FIXME 5385 do { 5386 processOnce(); 5387 } while(lowLevelReceive()); 5388 } 5389 5390 /++ 5391 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. 5392 5393 $(PITFALL 5394 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. 5395 ) 5396 5397 History: 5398 Added March 19, 2023 (dub v11.0). 5399 +/ 5400 static struct CloseEvent { 5401 ushort code; 5402 const(char)[] reason; 5403 bool wasClean; 5404 5405 string extendedErrorInformationUnstable; 5406 5407 /++ 5408 See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 for details. 5409 +/ 5410 enum StandardCloseCodes { 5411 purposeFulfilled = 1000, 5412 goingAway = 1001, 5413 protocolError = 1002, 5414 unacceptableData = 1003, // e.g. got text message when you can only handle binary 5415 Reserved = 1004, 5416 noStatusCodePresent = 1005, // not set by endpoint. 5417 abnormalClosure = 1006, // not set by endpoint. closed without a Close control. FIXME: maybe keep a copy of errno around for these 5418 inconsistentData = 1007, // e.g. utf8 validation failed 5419 genericPolicyViolation = 1008, 5420 messageTooBig = 1009, 5421 clientRequiredExtensionMissing = 1010, // only the client should send this 5422 unnexpectedCondition = 1011, 5423 unverifiedCertificate = 1015, // not set by client 5424 } 5425 } 5426 5427 /++ 5428 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. 5429 5430 History: 5431 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. 5432 5433 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. 5434 +/ 5435 arsd.core.FlexibleDelegate!(void delegate(CloseEvent event)) onclose; 5436 void delegate() onerror; /// 5437 void delegate(in char[]) ontextmessage; /// 5438 void delegate(in ubyte[]) onbinarymessage; /// 5439 void delegate() onopen; /// 5440 5441 /++ 5442 5443 +/ 5444 /// Group: browser_api 5445 void onmessage(void delegate(in char[]) dg) { 5446 ontextmessage = dg; 5447 } 5448 5449 /// ditto 5450 void onmessage(void delegate(in ubyte[]) dg) { 5451 onbinarymessage = dg; 5452 } 5453 5454 /* } end copy/paste */ 5455 5456 // returns true if still active 5457 private static bool readyToRead(WebSocket sock) { 5458 sock.timeoutFromInactivity = MonoTime.currTime + sock.config.timeoutFromInactivity; 5459 if(!sock.lowLevelReceive()) { 5460 sock.readyState_ = CLOSED; 5461 5462 if(sock.onerror) 5463 sock.onerror(); 5464 5465 if(sock.onclose) 5466 sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection lost", false, lastSocketError())); 5467 5468 unregisterActiveSocket(sock); 5469 sock.socket.close(); 5470 return false; 5471 } 5472 while(sock.processOnce().populated) {} 5473 return true; 5474 } 5475 5476 // returns true if still active, false if not 5477 private static bool timeoutAndPingCheck(WebSocket sock, MonoTime now, Duration* minimumTimeoutForSelect) { 5478 auto diff = sock.timeoutFromInactivity - now; 5479 if(diff <= 0.msecs) { 5480 // it timed out 5481 if(sock.onerror) 5482 sock.onerror(); 5483 5484 if(sock.onclose) 5485 sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection timed out", false, null)); 5486 5487 sock.readyState_ = CLOSED; 5488 unregisterActiveSocket(sock); 5489 sock.socket.close(); 5490 return false; 5491 } 5492 5493 if(minimumTimeoutForSelect && diff < *minimumTimeoutForSelect) 5494 *minimumTimeoutForSelect = diff; 5495 5496 diff = sock.nextPing - now; 5497 5498 if(diff <= 0.msecs) { 5499 //sock.send(`{"action": "ping"}`); 5500 sock.ping(); 5501 sock.nextPing = now + sock.config.pingFrequency.msecs; 5502 } else { 5503 if(minimumTimeoutForSelect && diff < *minimumTimeoutForSelect) 5504 *minimumTimeoutForSelect = diff; 5505 } 5506 5507 return true; 5508 } 5509 5510 /* 5511 const int bufferedAmount // amount pending 5512 const string extensions 5513 5514 const string protocol 5515 const string url 5516 */ 5517 5518 static { 5519 /++ 5520 Runs an event loop with all known websockets on this thread until all websockets 5521 are closed or unregistered, or until you call [exitEventLoop], or set `*localLoopExited` 5522 to false (please note it may take a few seconds until it checks that flag again; it may 5523 not exit immediately). 5524 5525 History: 5526 The `localLoopExited` parameter was added August 22, 2022 (dub v10.9) 5527 5528 See_Also: 5529 [addToSimpledisplayEventLoop] 5530 +/ 5531 void eventLoop(shared(bool)* localLoopExited = null) { 5532 import core.atomic; 5533 atomicOp!"+="(numberOfEventLoops, 1); 5534 scope(exit) { 5535 if(atomicOp!"-="(numberOfEventLoops, 1) <= 0) 5536 loopExited = false; // reset it so we can reenter 5537 } 5538 5539 version(use_arsd_core) { 5540 loopExited = false; 5541 5542 import arsd.core; 5543 getThisThreadEventLoop().run(() => WebSocket.activeSockets.length == 0 || loopExited || (localLoopExited !is null && *localLoopExited == true)); 5544 } else { 5545 static SocketSet readSet; 5546 5547 if(readSet is null) 5548 readSet = new SocketSet(); 5549 5550 loopExited = false; 5551 5552 outermost: while(!loopExited && (localLoopExited is null || (*localLoopExited == false))) { 5553 readSet.reset(); 5554 5555 Duration timeout = 3.seconds; 5556 5557 auto now = MonoTime.currTime; 5558 bool hadAny; 5559 foreach(sock; activeSockets) { 5560 if(!timeoutAndPingCheck(sock, now, &timeout)) 5561 continue outermost; 5562 5563 readSet.add(sock.socket); 5564 hadAny = true; 5565 } 5566 5567 if(!hadAny) { 5568 // import std.stdio; writeln("had none"); 5569 return; 5570 } 5571 5572 tryAgain: 5573 // import std.stdio; writeln(timeout); 5574 auto selectGot = Socket.select(readSet, null, null, timeout); 5575 if(selectGot == 0) { /* timeout */ 5576 // timeout 5577 continue; // it will be handled at the top of the loop 5578 } else if(selectGot == -1) { /* interrupted */ 5579 goto tryAgain; 5580 } else { 5581 foreach(sock; activeSockets) { 5582 if(readSet.isSet(sock.socket)) { 5583 if(!readyToRead(sock)) 5584 continue outermost; 5585 selectGot--; 5586 if(selectGot <= 0) 5587 break; 5588 } 5589 } 5590 } 5591 } 5592 } 5593 } 5594 5595 private static shared(int) numberOfEventLoops; 5596 5597 private __gshared bool loopExited; 5598 /++ 5599 Exits all running [WebSocket.eventLoop]s next time they loop around. You can call this from a signal handler or another thread. 5600 5601 Please note they may not loop around to check the flag for several seconds. Any new event loops will exit immediately until 5602 all current ones are closed. Once all event loops are exited, the flag is cleared and you can start the loop again. 5603 5604 This function is likely to be deprecated in the future due to its quirks and imprecise name. 5605 +/ 5606 void exitEventLoop() { 5607 loopExited = true; 5608 } 5609 5610 WebSocket[] activeSockets; 5611 5612 void registerActiveSocket(WebSocket s) { 5613 // ensure it isn't already there... 5614 assert(s !is null); 5615 if(s.registered) 5616 return; 5617 s.activeSocketArrayIndex = activeSockets.length; 5618 activeSockets ~= s; 5619 s.registered = true; 5620 version(use_arsd_core) { 5621 s.unregisterToken = arsd.core.getThisThreadEventLoop().addCallbackOnFdReadable(s.socket.handle, new arsd.core.CallbackHelper(() { s.readyToRead(s); })); 5622 } 5623 } 5624 void unregisterActiveSocket(WebSocket s) { 5625 version(use_arsd_core) { 5626 s.unregisterToken.unregister(); 5627 } 5628 5629 auto i = s.activeSocketArrayIndex; 5630 assert(activeSockets[i] is s); 5631 5632 activeSockets[i] = activeSockets[$-1]; 5633 activeSockets[i].activeSocketArrayIndex = i; 5634 activeSockets = activeSockets[0 .. $-1]; 5635 activeSockets.assumeSafeAppend(); 5636 s.registered = false; 5637 } 5638 } 5639 5640 private bool registered; 5641 private size_t activeSocketArrayIndex; 5642 version(use_arsd_core) { 5643 static import arsd.core; 5644 arsd.core.ICoreEventLoop.UnregisterToken unregisterToken; 5645 } 5646 } 5647 5648 private template imported(string mod) { 5649 mixin(`import imported = ` ~ mod ~ `;`); 5650 } 5651 5652 /++ 5653 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) 5654 +/ 5655 template addToSimpledisplayEventLoop() { 5656 import arsd.simpledisplay; 5657 void addToSimpledisplayEventLoop(WebSocket ws, imported!"arsd.simpledisplay".SimpleWindow window) { 5658 version(use_arsd_core) 5659 return; // already done implicitly 5660 5661 version(Windows) 5662 auto event = WSACreateEvent(); 5663 // FIXME: supposed to close event too 5664 5665 void midprocess() { 5666 version(Windows) 5667 ResetEvent(event); 5668 if(!ws.lowLevelReceive()) { 5669 ws.readyState_ = WebSocket.CLOSED; 5670 WebSocket.unregisterActiveSocket(ws); 5671 ws.socket.close(); 5672 return; 5673 } 5674 while(ws.processOnce().populated) {} 5675 } 5676 5677 version(Posix) { 5678 auto reader = new PosixFdReader(&midprocess, ws.socket.handle); 5679 } else version(none) { 5680 if(WSAAsyncSelect(ws.socket.handle, window.hwnd, WM_USER + 150, FD_CLOSE | FD_READ)) 5681 throw new Exception("WSAAsyncSelect"); 5682 5683 window.handleNativeEvent = delegate int(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 5684 if(hwnd !is window.impl.hwnd) 5685 return 1; // we don't care... 5686 switch(msg) { 5687 case WM_USER + 150: // socket activity 5688 switch(LOWORD(lParam)) { 5689 case FD_READ: 5690 case FD_CLOSE: 5691 midprocess(); 5692 break; 5693 default: 5694 // nothing 5695 } 5696 break; 5697 default: return 1; // not handled, pass it on 5698 } 5699 return 0; 5700 }; 5701 5702 } else version(Windows) { 5703 ws.socket.blocking = false; // the WSAEventSelect does this anyway and doing it here lets phobos know about it. 5704 //CreateEvent(null, 0, 0, null); 5705 if(!event) { 5706 throw new Exception("WSACreateEvent"); 5707 } 5708 if(WSAEventSelect(ws.socket.handle, event, 1/*FD_READ*/ | (1<<5)/*FD_CLOSE*/)) { 5709 //import std.stdio; writeln(WSAGetLastError()); 5710 throw new Exception("WSAEventSelect"); 5711 } 5712 5713 auto handle = new WindowsHandleReader(&midprocess, event); 5714 5715 /+ 5716 static class Ready {} 5717 5718 Ready thisr = new Ready; 5719 5720 justCommunication.addEventListener((Ready r) { 5721 if(r is thisr) 5722 midprocess(); 5723 }); 5724 5725 import core.thread; 5726 auto thread = new Thread({ 5727 while(true) { 5728 WSAWaitForMultipleEvents(1, &event, true, -1/*WSA_INFINITE*/, false); 5729 justCommunication.postEvent(thisr); 5730 } 5731 }); 5732 thread.isDaemon = true; 5733 thread.start; 5734 +/ 5735 5736 } else static assert(0, "unsupported OS"); 5737 } 5738 } 5739 5740 version(Windows) { 5741 import core.sys.windows.windows; 5742 import core.sys.windows.winsock2; 5743 } 5744 5745 version(none) { 5746 extern(Windows) int WSAAsyncSelect(SOCKET, HWND, uint, int); 5747 enum int FD_CLOSE = 1 << 5; 5748 enum int FD_READ = 1 << 0; 5749 enum int WM_USER = 1024; 5750 } 5751 5752 version(Windows) { 5753 import core.stdc.config; 5754 extern(Windows) 5755 int WSAEventSelect(SOCKET, HANDLE /* to an Event */, c_long); 5756 5757 extern(Windows) 5758 HANDLE WSACreateEvent(); 5759 5760 extern(Windows) 5761 DWORD WSAWaitForMultipleEvents(DWORD, HANDLE*, BOOL, DWORD, BOOL); 5762 } 5763 5764 /* copy/paste from cgi.d */ 5765 public { 5766 enum WebSocketOpcode : ubyte { 5767 continuation = 0, 5768 text = 1, 5769 binary = 2, 5770 // 3, 4, 5, 6, 7 RESERVED 5771 close = 8, 5772 ping = 9, 5773 pong = 10, 5774 // 11,12,13,14,15 RESERVED 5775 } 5776 5777 public struct WebSocketFrame { 5778 private bool populated; 5779 bool fin; 5780 bool rsv1; 5781 bool rsv2; 5782 bool rsv3; 5783 WebSocketOpcode opcode; // 4 bits 5784 bool masked; 5785 ubyte lengthIndicator; // don't set this when building one to send 5786 ulong realLength; // don't use when sending 5787 ubyte[4] maskingKey; // don't set this when sending 5788 ubyte[] data; 5789 5790 static WebSocketFrame simpleMessage(WebSocketOpcode opcode, in void[] data) { 5791 WebSocketFrame msg; 5792 msg.fin = true; 5793 msg.opcode = opcode; 5794 msg.data = cast(ubyte[]) data.dup; // it is mutated below when masked, so need to be cautious and copy it, sigh 5795 5796 return msg; 5797 } 5798 5799 private void send(scope void delegate(ubyte[]) llsend) { 5800 ubyte[64] headerScratch; 5801 int headerScratchPos = 0; 5802 5803 realLength = data.length; 5804 5805 { 5806 ubyte b1; 5807 b1 |= cast(ubyte) opcode; 5808 b1 |= rsv3 ? (1 << 4) : 0; 5809 b1 |= rsv2 ? (1 << 5) : 0; 5810 b1 |= rsv1 ? (1 << 6) : 0; 5811 b1 |= fin ? (1 << 7) : 0; 5812 5813 headerScratch[0] = b1; 5814 headerScratchPos++; 5815 } 5816 5817 { 5818 headerScratchPos++; // we'll set header[1] at the end of this 5819 auto rlc = realLength; 5820 ubyte b2; 5821 b2 |= masked ? (1 << 7) : 0; 5822 5823 assert(headerScratchPos == 2); 5824 5825 if(realLength > 65535) { 5826 // use 64 bit length 5827 b2 |= 0x7f; 5828 5829 // FIXME: double check endinaness 5830 foreach(i; 0 .. 8) { 5831 headerScratch[2 + 7 - i] = rlc & 0x0ff; 5832 rlc >>>= 8; 5833 } 5834 5835 headerScratchPos += 8; 5836 } else if(realLength > 125) { 5837 // use 16 bit length 5838 b2 |= 0x7e; 5839 5840 // FIXME: double check endinaness 5841 foreach(i; 0 .. 2) { 5842 headerScratch[2 + 1 - i] = rlc & 0x0ff; 5843 rlc >>>= 8; 5844 } 5845 5846 headerScratchPos += 2; 5847 } else { 5848 // use 7 bit length 5849 b2 |= realLength & 0b_0111_1111; 5850 } 5851 5852 headerScratch[1] = b2; 5853 } 5854 5855 //assert(!masked, "masking key not properly implemented"); 5856 if(masked) { 5857 import std.random; 5858 foreach(ref item; maskingKey) 5859 item = uniform(ubyte.min, ubyte.max); 5860 headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[]; 5861 headerScratchPos += 4; 5862 5863 // we'll just mask it in place... 5864 int keyIdx = 0; 5865 foreach(i; 0 .. data.length) { 5866 data[i] = data[i] ^ maskingKey[keyIdx]; 5867 if(keyIdx == 3) 5868 keyIdx = 0; 5869 else 5870 keyIdx++; 5871 } 5872 } 5873 5874 //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data); 5875 llsend(headerScratch[0 .. headerScratchPos]); 5876 if(data.length) 5877 llsend(data); 5878 } 5879 5880 static WebSocketFrame read(ref ubyte[] d) { 5881 WebSocketFrame msg; 5882 5883 auto orig = d; 5884 5885 WebSocketFrame needsMoreData() { 5886 d = orig; 5887 return WebSocketFrame.init; 5888 } 5889 5890 if(d.length < 2) 5891 return needsMoreData(); 5892 5893 ubyte b = d[0]; 5894 5895 msg.populated = true; 5896 5897 msg.opcode = cast(WebSocketOpcode) (b & 0x0f); 5898 b >>= 4; 5899 msg.rsv3 = b & 0x01; 5900 b >>= 1; 5901 msg.rsv2 = b & 0x01; 5902 b >>= 1; 5903 msg.rsv1 = b & 0x01; 5904 b >>= 1; 5905 msg.fin = b & 0x01; 5906 5907 b = d[1]; 5908 msg.masked = (b & 0b1000_0000) ? true : false; 5909 msg.lengthIndicator = b & 0b0111_1111; 5910 5911 d = d[2 .. $]; 5912 5913 if(msg.lengthIndicator == 0x7e) { 5914 // 16 bit length 5915 msg.realLength = 0; 5916 5917 if(d.length < 2) return needsMoreData(); 5918 5919 foreach(i; 0 .. 2) { 5920 msg.realLength |= d[0] << ((1-i) * 8); 5921 d = d[1 .. $]; 5922 } 5923 } else if(msg.lengthIndicator == 0x7f) { 5924 // 64 bit length 5925 msg.realLength = 0; 5926 5927 if(d.length < 8) return needsMoreData(); 5928 5929 foreach(i; 0 .. 8) { 5930 msg.realLength |= ulong(d[0]) << ((7-i) * 8); 5931 d = d[1 .. $]; 5932 } 5933 } else { 5934 // 7 bit length 5935 msg.realLength = msg.lengthIndicator; 5936 } 5937 5938 if(msg.masked) { 5939 5940 if(d.length < 4) return needsMoreData(); 5941 5942 msg.maskingKey = d[0 .. 4]; 5943 d = d[4 .. $]; 5944 } 5945 5946 if(msg.realLength > d.length) { 5947 return needsMoreData(); 5948 } 5949 5950 msg.data = d[0 .. cast(size_t) msg.realLength]; 5951 d = d[cast(size_t) msg.realLength .. $]; 5952 5953 return msg; 5954 } 5955 5956 void unmaskInPlace() { 5957 if(this.masked) { 5958 int keyIdx = 0; 5959 foreach(i; 0 .. this.data.length) { 5960 this.data[i] = this.data[i] ^ this.maskingKey[keyIdx]; 5961 if(keyIdx == 3) 5962 keyIdx = 0; 5963 else 5964 keyIdx++; 5965 } 5966 } 5967 } 5968 5969 char[] textData() { 5970 return cast(char[]) data; 5971 } 5972 } 5973 } 5974 5975 private extern(C) 5976 int verifyCertificateFromRegistryArsdHttp(int preverify_ok, X509_STORE_CTX* ctx) { 5977 version(Windows) { 5978 if(preverify_ok) 5979 return 1; 5980 5981 auto err_cert = OpenSSL.X509_STORE_CTX_get_current_cert(ctx); 5982 auto err = OpenSSL.X509_STORE_CTX_get_error(ctx); 5983 5984 if(err == 62) 5985 return 0; // hostname mismatch is an error we can trust; that means OpenSSL already found the certificate and rejected it 5986 5987 auto len = OpenSSL.i2d_X509(err_cert, null); 5988 if(len == -1) 5989 return 0; 5990 ubyte[] buffer = new ubyte[](len); 5991 auto ptr = buffer.ptr; 5992 len = OpenSSL.i2d_X509(err_cert, &ptr); 5993 if(len != buffer.length) 5994 return 0; 5995 5996 5997 CERT_CHAIN_PARA thing; 5998 thing.cbSize = thing.sizeof; 5999 auto context = CertCreateCertificateContext(X509_ASN_ENCODING, buffer.ptr, cast(int) buffer.length); 6000 if(context is null) 6001 return 0; 6002 scope(exit) CertFreeCertificateContext(context); 6003 6004 PCCERT_CHAIN_CONTEXT chain; 6005 if(CertGetCertificateChain(null, context, null, null, &thing, 0, null, &chain)) { 6006 scope(exit) 6007 CertFreeCertificateChain(chain); 6008 6009 DWORD errorStatus = chain.TrustStatus.dwErrorStatus; 6010 6011 if(errorStatus == 0) 6012 return 1; // Windows approved it, OK carry on 6013 // otherwise, sustain OpenSSL's original ruling 6014 } 6015 6016 return 0; 6017 } else { 6018 return preverify_ok; 6019 } 6020 } 6021 6022 6023 version(Windows) { 6024 pragma(lib, "crypt32"); 6025 import core.sys.windows.wincrypt; 6026 extern(Windows) { 6027 PCCERT_CONTEXT CertEnumCertificatesInStore(HCERTSTORE hCertStore, PCCERT_CONTEXT pPrevCertContext); 6028 // BOOL CertGetCertificateChain(HCERTCHAINENGINE hChainEngine, PCCERT_CONTEXT pCertContext, LPFILETIME pTime, HCERTSTORE hAdditionalStore, PCERT_CHAIN_PARA pChainPara, DWORD dwFlags, LPVOID pvReserved, PCCERT_CHAIN_CONTEXT *ppChainContext); 6029 PCCERT_CONTEXT CertCreateCertificateContext(DWORD dwCertEncodingType, const BYTE *pbCertEncoded, DWORD cbCertEncoded); 6030 } 6031 6032 void loadCertificatesFromRegistry(SSL_CTX* ctx) { 6033 auto store = CertOpenSystemStore(0, "ROOT"); 6034 if(store is null) { 6035 // import std.stdio; writeln("failed"); 6036 return; 6037 } 6038 scope(exit) 6039 CertCloseStore(store, 0); 6040 6041 X509_STORE* ssl_store = OpenSSL.SSL_CTX_get_cert_store(ctx); 6042 PCCERT_CONTEXT c; 6043 while((c = CertEnumCertificatesInStore(store, c)) !is null) { 6044 FILETIME na = c.pCertInfo.NotAfter; 6045 SYSTEMTIME st; 6046 FileTimeToSystemTime(&na, &st); 6047 6048 /+ 6049 _CRYPTOAPI_BLOB i = cast() c.pCertInfo.Issuer; 6050 6051 char[256] buffer; 6052 auto p = CertNameToStrA(X509_ASN_ENCODING, &i, CERT_SIMPLE_NAME_STR, buffer.ptr, cast(int) buffer.length); 6053 import std.stdio; writeln(buffer[0 .. p]); 6054 +/ 6055 6056 if(st.wYear <= 2021) { 6057 // see: https://www.openssl.org/blog/blog/2021/09/13/LetsEncryptRootCertExpire/ 6058 continue; // no point keeping an expired root cert and it can break Let's Encrypt anyway 6059 } 6060 6061 const(ubyte)* thing = c.pbCertEncoded; 6062 auto x509 = OpenSSL.d2i_X509(null, &thing, c.cbCertEncoded); 6063 if (x509) { 6064 auto success = OpenSSL.X509_STORE_add_cert(ssl_store, x509); 6065 //if(!success) 6066 //writeln("FAILED HERE"); 6067 OpenSSL.X509_free(x509); 6068 } else { 6069 //writeln("FAILED"); 6070 } 6071 } 6072 6073 CertFreeCertificateContext(c); 6074 6075 // import core.stdc.stdio; printf("%s\n", OpenSSL.OpenSSL_version(0)); 6076 } 6077 6078 6079 // because i use the FILE* in PEM_read_X509 and friends 6080 // gotta use this to bridge the MS C runtime functions 6081 // might be able to just change those to only use the BIO versions 6082 // instead 6083 6084 // only on MS C runtime 6085 version(CRuntime_Microsoft) {} else version=no_openssl_applink; 6086 6087 version(no_openssl_applink) {} else { 6088 private extern(C) { 6089 void _open(); 6090 void _read(); 6091 void _write(); 6092 void _lseek(); 6093 void _close(); 6094 int _fileno(FILE*); 6095 int _setmode(int, int); 6096 } 6097 export extern(C) void** OPENSSL_Applink() { 6098 import core.stdc.stdio; 6099 6100 static extern(C) void* app_stdin() { return cast(void*) stdin; } 6101 static extern(C) void* app_stdout() { return cast(void*) stdout; } 6102 static extern(C) void* app_stderr() { return cast(void*) stderr; } 6103 static extern(C) int app_feof(FILE* fp) { return feof(fp); } 6104 static extern(C) int app_ferror(FILE* fp) { return ferror(fp); } 6105 static extern(C) void app_clearerr(FILE* fp) { return clearerr(fp); } 6106 static extern(C) int app_fileno(FILE* fp) { return _fileno(fp); } 6107 static extern(C) int app_fsetmod(FILE* fp, char mod) { 6108 return _setmode(_fileno(fp), mod == 'b' ? _O_BINARY : _O_TEXT); 6109 } 6110 6111 static immutable void*[] table = [ 6112 cast(void*) 22, // applink max 6113 6114 &app_stdin, 6115 &app_stdout, 6116 &app_stderr, 6117 &fprintf, 6118 &fgets, 6119 &fread, 6120 &fwrite, 6121 &app_fsetmod, 6122 &app_feof, 6123 &fclose, 6124 6125 &fopen, 6126 &fseek, 6127 &ftell, 6128 &fflush, 6129 &app_ferror, 6130 &app_clearerr, 6131 &app_fileno, 6132 6133 &_open, 6134 &_read, 6135 &_write, 6136 &_lseek, 6137 &_close, 6138 ]; 6139 static assert(table.length == 23); 6140 6141 return cast(void**) table.ptr; 6142 } 6143 } 6144 } 6145 6146 unittest { 6147 auto client = new HttpClient(); 6148 auto response = client.navigateTo(Uri("data:,Hello%2C%20World%21")).waitForCompletion(); 6149 assert(response.contentTypeMimeType == "text/plain", response.contentType); 6150 assert(response.contentText == "Hello, World!", response.contentText); 6151 6152 response = client.navigateTo(Uri("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")).waitForCompletion(); 6153 assert(response.contentTypeMimeType == "text/plain", response.contentType); 6154 assert(response.contentText == "Hello, World!", response.contentText); 6155 6156 response = client.navigateTo(Uri("data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E")).waitForCompletion(); 6157 assert(response.contentTypeMimeType == "text/html", response.contentType); 6158 assert(response.contentText == "<h1>Hello, World!</h1>", response.contentText); 6159 } 6160 6161 version(arsd_http2_unittests) 6162 unittest { 6163 import core.thread; 6164 6165 static void server() { 6166 import std.socket; 6167 auto socket = new TcpSocket(); 6168 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 6169 socket.bind(new InternetAddress(12346)); 6170 socket.listen(1); 6171 auto s = socket.accept(); 6172 socket.close(); 6173 6174 ubyte[1024] thing; 6175 auto g = s.receive(thing[]); 6176 6177 /+ 6178 string response = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 9\r\n\r\nHello!!??"; 6179 auto packetSize = 2; 6180 +/ 6181 6182 auto packetSize = 1; 6183 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"; 6184 6185 while(response.length) { 6186 s.send(response[0 .. packetSize]); 6187 response = response[packetSize .. $]; 6188 //import std.stdio; writeln(response); 6189 } 6190 6191 s.close(); 6192 } 6193 6194 auto thread = new Thread(&server); 6195 thread.start; 6196 6197 Thread.sleep(200.msecs); 6198 6199 auto response = get("http://localhost:12346/").waitForCompletion; 6200 assert(response.code == 200); 6201 //import std.stdio; writeln(response); 6202 6203 foreach(site; ["https://dlang.org/", "http://arsdnet.net", "https://phobos.dpldocs.info"]) { 6204 response = get(site).waitForCompletion; 6205 assert(response.code == 200); 6206 } 6207 6208 thread.join; 6209 } 6210 6211 /+ 6212 so the url params are arguments. it knows the request 6213 internally. other params are properties on the req 6214 6215 names may have different paths... those will just add ForSomething i think. 6216 6217 auto req = api.listMergeRequests 6218 req.page = 10; 6219 6220 or 6221 req.page(1) 6222 .bar("foo") 6223 6224 req.execute(); 6225 6226 6227 everything in the response is nullable access through the 6228 dynamic object, just with property getters there. need to make 6229 it static generated tho 6230 6231 other messages may be: isPresent and getDynamic 6232 6233 6234 AND/OR what about doing it like the rails objects 6235 6236 BroadcastMessage.get(4) 6237 // various properties 6238 6239 // it lists what you updated 6240 6241 BroadcastMessage.foo().bar().put(5) 6242 +/