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