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