The OpenD Programming Language

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