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