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