diff --git a/apng.d b/apng.d index dacecce..83c2cb2 100644 --- a/apng.d +++ b/apng.d @@ -728,7 +728,7 @@ ApngAnimation readApng(in ubyte[] data, bool strictApng = false, scope ApngAnima obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $]; break; default: - obj.handleOtherChunk(chunk); + obj.handleOtherChunkWhenLoading(chunk); } } diff --git a/cgi.d b/cgi.d index 5fc18af..89b33d7 100644 --- a/cgi.d +++ b/cgi.d @@ -3510,11 +3510,11 @@ struct RequestServer { foundHost = false; } if(foundUid) { - privDropUserId = to!int(arg); + privilegesDropToUid = to!uid_t(arg); foundUid = false; } if(foundGid) { - privDropGroupId = to!int(arg); + privilegesDropToGid = to!gid_t(arg); foundGid = false; } if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host") @@ -3528,7 +3528,37 @@ struct RequestServer { } } - // FIXME: the privDropUserId/group id need to be set in here instead of global + version(Windows) { + private alias uid_t = int; + private alias gid_t = int; + } + + /// user (uid) to drop privileges to + /// 0 … do nothing + uid_t privilegesDropToUid = 0; + /// group (gid) to drop privileges to + /// 0 … do nothing + gid_t privilegesDropToGid = 0; + + private void dropPrivileges() { + version(Posix) { + import core.sys.posix.unistd; + + if (privilegesDropToGid != 0 && setgid(privilegesDropToGid) != 0) + throw new Exception("Dropping privileges via setgid() failed."); + + if (privilegesDropToUid != 0 && setuid(privilegesDropToUid) != 0) + throw new Exception("Dropping privileges via setuid() failed."); + } + else { + // FIXME: Windows? + //pragma(msg, "Dropping privileges is not implemented for this platform"); + } + + // done, set zero + privilegesDropToGid = 0; + privilegesDropToUid = 0; + } /++ Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders @@ -3557,7 +3587,7 @@ struct RequestServer { bool tcp; void delegate() cleanup; - auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1); + auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1, &dropPrivileges); auto connection = socket.accept(); doThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection); @@ -3680,28 +3710,6 @@ else private __gshared bool globalStopFlag = false; -private int privDropUserId; -private int privDropGroupId; - -// Added Jan 11, 2021 -private void dropPrivs() { - version(Posix) { - import core.sys.posix.unistd; - - auto userId = privDropUserId; - auto groupId = privDropGroupId; - - if((userId != 0 || groupId != 0) && getuid() == 0) { - if(groupId) - setgid(groupId); - if(userId) - setuid(userId); - } - - } - // FIXME: Windows? -} - version(embedded_httpd_processes) void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) { import core.sys.posix.unistd; @@ -3745,7 +3753,7 @@ void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer param close(sock); throw new Exception("listen"); } - dropPrivs(); + params.dropPrivileges(); } version(embedded_httpd_processes_accept_after_fork) {} else { @@ -5153,9 +5161,13 @@ import core.atomic; /** To use this thing: + --- void handler(Socket s) { do something... } - auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler); + auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler, &delegateThatDropsPrivileges); manager.listen(); + --- + + The 4th parameter is optional. I suggest you use BufferedInputRange(connection) to handle the input. As a packet comes in, you will get control. You can just continue; though to fetch more. @@ -5355,15 +5367,15 @@ class ListeningConnectionManager { private void dg_handler(Socket s) { fhandler(s); } - this(string host, ushort port, void function(Socket) handler) { + this(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null) { fhandler = handler; - this(host, port, &dg_handler); + this(host, port, &dg_handler, dropPrivs); } - this(string host, ushort port, void delegate(Socket) handler) { + this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null) { this.handler = handler; - listener = startListening(host, port, tcp, cleanup, 128); + listener = startListening(host, port, tcp, cleanup, 128, dropPrivs); version(cgi_use_fiber) version(cgi_use_fork) listener.blocking = false; @@ -5376,7 +5388,7 @@ class ListeningConnectionManager { void delegate(Socket) handler; } -Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue) { +Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue, void delegate() dropPrivs) { Socket listener; if(host.startsWith("unix:")) { version(Posix) { @@ -5424,7 +5436,8 @@ Socket startListening(string host, ushort port, ref bool tcp, ref void delegate( listener.listen(backQueue); - dropPrivs(); + if (dropPrivs !is null) // can be null, backwards compatibility + dropPrivs(); return listener; } @@ -10791,7 +10804,7 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp // man 2 sendfile assert(urlPrefix[0] == '/'); if(filename is null) - filename = urlPrefix[1 .. $]; + filename = decodeComponent(urlPrefix[1 .. $]); // FIXME is this actually correct? if(contentType is null) { contentType = contentTypeFromFileExtension(filename); } @@ -10873,7 +10886,7 @@ auto serveStaticFileDirectory(string urlPrefix, string directory = null) { assert(directory[$-1] == '/'); static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { - auto file = cgi.pathInfo[urlPrefix.length .. $]; + auto file = decodeComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct if(file.indexOf("/") != -1 || file.indexOf("\\") != -1) return false; diff --git a/http2.d b/http2.d index 4833243..3e56ecf 100644 --- a/http2.d +++ b/http2.d @@ -566,7 +566,7 @@ private class UnixAddress : Address { struct Uri { alias toString this; // blargh idk a url really is a string, but should it be implicit? - // scheme//userinfo@host:port/path?query#fragment + // scheme://userinfo@host:port/path?query#fragment string scheme; /// e.g. "http" in "http://example.com/" string userinfo; /// the username (and possibly a password) in the uri @@ -578,7 +578,42 @@ struct Uri { /// Breaks down a uri string to its components this(string uri) { - reparse(uri); + size_t lastGoodIndex; + foreach(char ch; uri) { + if(ch > 127) { + break; + } + lastGoodIndex++; + } + + string replacement = uri[0 .. lastGoodIndex]; + foreach(char ch; uri[lastGoodIndex .. $]) { + if(ch > 127) { + // need to percent-encode any non-ascii in it + char[3] buffer; + buffer[0] = '%'; + + auto first = ch / 16; + auto second = ch % 16; + first += (first >= 10) ? ('A'-10) : '0'; + second += (second >= 10) ? ('A'-10) : '0'; + + buffer[1] = cast(char) first; + buffer[2] = cast(char) second; + + replacement ~= buffer[]; + } else { + replacement ~= ch; + } + } + + reparse(replacement); + } + + /// Returns `port` if set, otherwise if scheme is https 443, otherwise always 80 + int effectivePort() const @property nothrow pure @safe @nogc { + return port != 0 ? port + : scheme == "https" ? 443 : 80; } private string unixSocketPath = null; @@ -991,10 +1026,8 @@ class HttpRequest { requestParameters.method = method; requestParameters.unixSocketPath = where.unixSocketPath; requestParameters.host = parts.host; - requestParameters.port = cast(ushort) parts.port; + requestParameters.port = cast(ushort) parts.effectivePort; requestParameters.ssl = parts.scheme == "https"; - if(parts.port == 0) - requestParameters.port = requestParameters.ssl ? 443 : 80; requestParameters.uri = parts.path.length ? parts.path : "/"; if(parts.query.length) { requestParameters.uri ~= "?"; @@ -1108,13 +1141,18 @@ class HttpRequest { } headers ~= "\r\n"; + bool specSaysRequestAlwaysHasBody = + requestParameters.method == HttpVerb.POST || + requestParameters.method == HttpVerb.PUT || + requestParameters.method == HttpVerb.PATCH; + if(requestParameters.userAgent.length) headers ~= "User-Agent: "~requestParameters.userAgent~"\r\n"; if(requestParameters.contentType.length) headers ~= "Content-Type: "~requestParameters.contentType~"\r\n"; if(requestParameters.authorization.length) headers ~= "Authorization: "~requestParameters.authorization~"\r\n"; - if(requestParameters.bodyData.length) + if(requestParameters.bodyData.length || specSaysRequestAlwaysHasBody) headers ~= "Content-Length: "~to!string(requestParameters.bodyData.length)~"\r\n"; if(requestParameters.acceptGzip) headers ~= "Accept-Encoding: gzip\r\n"; @@ -2403,17 +2441,7 @@ class HttpClient { or set [HttpRequest.retainCookies|request.retainCookies] to `true` on the returned object. But see important implementation shortcomings on [retainCookies]. +/ HttpRequest request(Uri uri, HttpVerb method = HttpVerb.GET, ubyte[] bodyData = null, string contentType = null) { - string proxyToUse; - switch(uri.scheme) { - case "http": - proxyToUse = httpProxy; - break; - case "https": - proxyToUse = httpsProxy; - break; - default: - proxyToUse = null; - } + string proxyToUse = getProxyFor(uri); auto request = new HttpRequest(this, uri, method, cache, defaultTimeout, proxyToUse); @@ -2466,6 +2494,8 @@ class HttpClient { The environment variables are used, if present, on all operating systems. History: + no_proxy support added April 13, 2022 + Added April 12, 2021 (included in dub v9.5) Bugs: @@ -2477,10 +2507,240 @@ class HttpClient { import std.process; httpProxy = environment.get("http_proxy", environment.get("HTTP_PROXY", null)); httpsProxy = environment.get("https_proxy", environment.get("HTTPS_PROXY", null)); + auto noProxy = environment.get("no_proxy", environment.get("NO_PROXY", null)); + if (noProxy.length) { + proxyIgnore = noProxy.split(","); + foreach (ref rule; proxyIgnore) + rule = rule.strip; + } // FIXME: on Windows, I should use the Internet Explorer proxy settings } + /++ + Checks if the given uri should be proxied according to the httpProxy, httpsProxy, proxyIgnore + variables and returns either httpProxy, httpsProxy or null. + + If neither `httpProxy` or `httpsProxy` are set this always returns `null`. Same if `proxyIgnore` + contains `*`. + + DNS is not resolved for proxyIgnore IPs, only IPs match IPs and hosts match hosts. + +/ + string getProxyFor(Uri uri) { + string proxyToUse; + switch(uri.scheme) { + case "http": + proxyToUse = httpProxy; + break; + case "https": + proxyToUse = httpsProxy; + break; + default: + proxyToUse = null; + } + + if (proxyToUse.length) { + foreach (ignore; proxyIgnore) { + if (matchProxyIgnore(ignore, uri)) { + return null; + } + } + } + + return proxyToUse; + } + + /// Returns -1 on error, otherwise the IP as uint. Parsing is very strict. + private static long tryParseIPv4(scope const(char)[] s) nothrow { + import std.algorithm : findSplit, all; + import std.ascii : isDigit; + + static int parseNum(scope const(char)[] num) nothrow { + if (num.length < 1 || num.length > 3 || !num.representation.all!isDigit) + return -1; + try { + auto ret = num.to!int; + return ret > 255 ? -1 : ret; + } catch (Exception) { + assert(false); + } + } + + if (s.length < "0.0.0.0".length || s.length > "255.255.255.255".length) + return -1; + auto firstPair = s.findSplit("."); + auto secondPair = firstPair[2].findSplit("."); + auto thirdPair = secondPair[2].findSplit("."); + auto a = parseNum(firstPair[0]); + auto b = parseNum(secondPair[0]); + auto c = parseNum(thirdPair[0]); + auto d = parseNum(thirdPair[2]); + if (a < 0 || b < 0 || c < 0 || d < 0) + return -1; + return (cast(uint)a << 24) | (b << 16) | (c << 8) | (d); + } + + unittest { + assert(tryParseIPv4("0.0.0.0") == 0); + assert(tryParseIPv4("127.0.0.1") == 0x7f000001); + assert(tryParseIPv4("162.217.114.56") == 0xa2d97238); + assert(tryParseIPv4("256.0.0.1") == -1); + assert(tryParseIPv4("0.0.0.-2") == -1); + assert(tryParseIPv4("0.0.0.a") == -1); + assert(tryParseIPv4("0.0.0") == -1); + assert(tryParseIPv4("0.0.0.0.0") == -1); + } + + /++ + Returns true if the given no_proxy rule matches the uri. + + Invalid IP ranges are silently ignored and return false. + + See $(LREF proxyIgnore). + +/ + static bool matchProxyIgnore(scope const(char)[] rule, scope const Uri uri) nothrow { + import std.algorithm; + import std.ascii : isDigit; + import std.uni : sicmp; + + string uriHost = uri.host; + if (uriHost.length && uriHost[$ - 1] == '.') + uriHost = uriHost[0 .. $ - 1]; + + if (rule == "*") + return true; + while (rule.length && rule[0] == '.') rule = rule[1 .. $]; + + static int parsePort(scope const(char)[] portStr) nothrow { + if (portStr.length < 1 || portStr.length > 5 || !portStr.representation.all!isDigit) + return -1; + try { + return portStr.to!int; + } catch (Exception) { + assert(false, "to!int should succeed"); + } + } + + if (sicmp(rule, uriHost) == 0 + || (uriHost.length > rule.length + && sicmp(rule, uriHost[$ - rule.length .. $]) == 0 + && uriHost[$ - rule.length - 1] == '.')) + return true; + + if (rule.startsWith("[")) { // IPv6 + // below code is basically nothrow lastIndexOfAny("]:") + ptrdiff_t lastColon = cast(ptrdiff_t) rule.length - 1; + while (lastColon >= 0) { + if (rule[lastColon] == ']' || rule[lastColon] == ':') + break; + lastColon--; + } + if (lastColon == -1) + return false; // malformed + + if (rule[lastColon] == ':') { // match with port + auto port = parsePort(rule[lastColon + 1 .. $]); + if (port != -1) { + if (uri.effectivePort != port.to!int) + return false; + return uriHost == rule[0 .. lastColon]; + } + } + // exact match of host already done above + } else { + auto slash = rule.lastIndexOfNothrow('/'); + if (slash == -1) { // no IP range + auto colon = rule.lastIndexOfNothrow(':'); + auto host = colon == -1 ? rule : rule[0 .. colon]; + auto port = colon != -1 ? parsePort(rule[colon + 1 .. $]) : -1; + auto ip = tryParseIPv4(host); + if (ip == -1) { // not an IPv4, test for host with port + return port != -1 + && uri.effectivePort == port + && uriHost == host; + } else { + // perform IPv4 equals + auto other = tryParseIPv4(uriHost); + if (other == -1) + return false; // rule == IPv4, uri != IPv4 + if (port != -1) + return uri.effectivePort == port + && uriHost == host; + else + return uriHost == host; + } + } else { + auto maskStr = rule[slash + 1 .. $]; + auto ip = tryParseIPv4(rule[0 .. slash]); + if (ip == -1) + return false; + if (maskStr.length && maskStr.length < 3 && maskStr.representation.all!isDigit) { + // IPv4 range match + int mask; + try { + mask = maskStr.to!int; + } catch (Exception) { + assert(false); + } + + auto other = tryParseIPv4(uriHost); + if (other == -1) + return false; // rule == IPv4, uri != IPv4 + + if (mask == 0) // matches all + return true; + if (mask > 32) // matches none + return false; + + auto shift = 32 - mask; + return cast(uint)other >> shift + == cast(uint)ip >> shift; + } + } + } + return false; + } + + unittest { + assert(matchProxyIgnore("0.0.0.0/0", Uri("http://127.0.0.1:80/a"))); + assert(matchProxyIgnore("0.0.0.0/0", Uri("http://127.0.0.1/a"))); + assert(!matchProxyIgnore("0.0.0.0/0", Uri("https://dlang.org/a"))); + assert(matchProxyIgnore("*", Uri("https://dlang.org/a"))); + assert(matchProxyIgnore("127.0.0.0/8", Uri("http://127.0.0.1:80/a"))); + assert(matchProxyIgnore("127.0.0.0/8", Uri("http://127.0.0.1/a"))); + assert(matchProxyIgnore("127.0.0.1", Uri("http://127.0.0.1:1234/a"))); + assert(!matchProxyIgnore("127.0.0.1:80", Uri("http://127.0.0.1:1234/a"))); + assert(!matchProxyIgnore("127.0.0.1/8", Uri("http://localhost/a"))); // no DNS resolution / guessing + assert(!matchProxyIgnore("0.0.0.0/1", Uri("http://localhost/a")) + && !matchProxyIgnore("128.0.0.0/1", Uri("http://localhost/a"))); // no DNS resolution / guessing 2 + foreach (m; 1 .. 32) { + assert(matchProxyIgnore(text("127.0.0.1/", m), Uri("http://127.0.0.1/a"))); + assert(!matchProxyIgnore(text("127.0.0.1/", m), Uri("http://128.0.0.1/a"))); + bool expectedMatch = m <= 24; + assert(expectedMatch == matchProxyIgnore(text("127.0.1.0/", m), Uri("http://127.0.1.128/a")), m.to!string); + } + assert(matchProxyIgnore("localhost", Uri("http://localhost/a"))); + assert(matchProxyIgnore("localhost", Uri("http://foo.localhost/a"))); + assert(matchProxyIgnore("localhost", Uri("http://foo.localhost./a"))); + assert(matchProxyIgnore(".localhost", Uri("http://localhost/a"))); + assert(matchProxyIgnore(".localhost", Uri("http://foo.localhost/a"))); + assert(matchProxyIgnore(".localhost", Uri("http://foo.localhost./a"))); + assert(!matchProxyIgnore("foo.localhost", Uri("http://localhost/a"))); + assert(matchProxyIgnore("foo.localhost", Uri("http://foo.localhost/a"))); + assert(matchProxyIgnore("foo.localhost", Uri("http://foo.localhost./a"))); + assert(!matchProxyIgnore("bar.localhost", Uri("http://localhost/a"))); + assert(!matchProxyIgnore("bar.localhost", Uri("http://foo.localhost/a"))); + assert(!matchProxyIgnore("bar.localhost", Uri("http://foo.localhost./a"))); + assert(!matchProxyIgnore("bar.localhost", Uri("http://bbar.localhost./a"))); + assert(matchProxyIgnore("[::1]", Uri("http://[::1]/a"))); + assert(!matchProxyIgnore("[::1]", Uri("http://[::2]/a"))); + assert(matchProxyIgnore("[::1]:80", Uri("http://[::1]/a"))); + assert(!matchProxyIgnore("[::1]:443", Uri("http://[::1]/a"))); + assert(!matchProxyIgnore("[::1]:80", Uri("https://[::1]/a"))); + assert(matchProxyIgnore("[::1]:443", Uri("https://[::1]/a"))); + assert(matchProxyIgnore("google.com", Uri("https://GOOGLE.COM/a"))); + } + /++ Proxies to use for requests. The [HttpClient] constructor will set these to the system values, then you can reset it to `null` if you want to override and not use the proxy after all, or you @@ -2496,6 +2756,28 @@ class HttpClient { string httpProxy; /// ditto string httpsProxy; + /++ + List of hosts or ips, optionally including a port, where not to proxy. + + Each entry may be one of the following formats: + - `127.0.0.1` (IPv4, any port) + - `127.0.0.1:1234` (IPv4, specific port) + - `127.0.0.1/8` (IPv4 range / CIDR block, any port) + - `[::1]` (IPv6, any port) + - `[::1]:1234` (IPv6, specific port) + - `*` (all hosts and ports, basically don't proxy at all anymore) + - `.domain.name`, `domain.name` (don't proxy the specified domain, + leading dots are stripped and subdomains are also not proxied) + - `.domain.name:1234`, `domain.name:1234` (same as above, with specific port) + + No DNS resolution or regex is done in this list. + + See https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/ + + History: + Added April 13, 2022 + +/ + string[] proxyIgnore; /// See [retainCookies] for important caveats. void setCookie(string name, string value, string domain = null) { @@ -2572,6 +2854,17 @@ class HttpClient { private CookieHeader[][string] cookies; } +private ptrdiff_t lastIndexOfNothrow(T)(scope T[] arr, T value) nothrow +{ + ptrdiff_t ret = cast(ptrdiff_t)arr.length - 1; + while (ret >= 0) { + if (arr[ret] == value) + return ret; + ret--; + } + return ret; +} + interface ICache { /++ 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). @@ -2772,20 +3065,69 @@ void main() { version(use_openssl) { alias SslClientSocket = OpenSslSocket; - // macros in the original C - SSL_METHOD* SSLv23_client_method() { - if(ossllib.SSLv23_client_method) - return ossllib.SSLv23_client_method(); - else - return ossllib.TLS_client_method(); + // CRL = Certificate Revocation List + static immutable string[] sslErrorCodes = [ + "OK (code 0)", + "Unspecified SSL/TLS error (code 1)", + "Unable to get TLS issuer certificate (code 2)", + "Unable to get TLS CRL (code 3)", + "Unable to decrypt TLS certificate signature (code 4)", + "Unable to decrypt TLS CRL signature (code 5)", + "Unable to decode TLS issuer public key (code 6)", + "TLS certificate signature failure (code 7)", + "TLS CRL signature failure (code 8)", + "TLS certificate not yet valid (code 9)", + "TLS certificate expired (code 10)", + "TLS CRL not yet valid (code 11)", + "TLS CRL expired (code 12)", + "TLS error in certificate not before field (code 13)", + "TLS error in certificate not after field (code 14)", + "TLS error in CRL last update field (code 15)", + "TLS error in CRL next update field (code 16)", + "TLS system out of memory (code 17)", + "TLS certificate is self-signed (code 18)", + "Self-signed certificate in TLS chain (code 19)", + "Unable to get TLS issuer certificate locally (code 20)", + "Unable to verify TLS leaf signature (code 21)", + "TLS certificate chain too long (code 22)", + "TLS certificate was revoked (code 23)", + "TLS CA is invalid (code 24)", + "TLS error: path length exceeded (code 25)", + "TLS error: invalid purpose (code 26)", + "TLS error: certificate untrusted (code 27)", + "TLS error: certificate rejected (code 28)", + ]; + + string getOpenSslErrorCode(long error) { + if(error == 62) + return "TLS certificate host name mismatch"; + + if(error < 0 || error >= sslErrorCodes.length) + return "SSL/TLS error code " ~ to!string(error); + return sslErrorCodes[cast(size_t) error]; } - struct SSL {} - struct SSL_CTX {} - struct SSL_METHOD {} + struct SSL; + struct SSL_CTX; + struct SSL_METHOD; + struct X509_STORE_CTX; enum SSL_VERIFY_NONE = 0; enum SSL_VERIFY_PEER = 1; + // copy it into the buf[0 .. size] and return actual length you read. + // rwflag == 0 when reading, 1 when writing. + extern(C) alias pem_password_cb = int function(char* buffer, int bufferSize, int rwflag, void* userPointer); + extern(C) alias print_errors_cb = int function(const char*, size_t, void*); + extern(C) alias client_cert_cb = int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey); + extern(C) alias keylog_cb = void function(SSL*, char*); + + struct X509; + struct X509_STORE; + struct EVP_PKEY; + struct X509_VERIFY_PARAM; + + import core.stdc.config; + struct ossllib { __gshared static extern(C) { /* these are only on older openssl versions { */ @@ -2822,26 +3164,17 @@ version(use_openssl) { X509_STORE* function(SSL_CTX*) SSL_CTX_get_cert_store; c_long function(const SSL* ssl) SSL_get_verify_result; + X509_VERIFY_PARAM* function(const SSL*) SSL_get0_param; + /+ SSL_CTX_load_verify_locations SSL_CTX_set_client_CA_list +/ - // client cert things void function (SSL_CTX *ctx, int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey)) SSL_CTX_set_client_cert_cb; } } - // copy it into the buf[0 .. size] and return actual length you read. - // rwflag == 0 when reading, 1 when writing. - extern(C) - alias pem_password_cb = int function(char* buffer, int bufferSize, int rwflag, void* userPointer); - - struct X509; - struct X509_STORE; - struct EVP_PKEY; - - import core.stdc.config; struct eallib { __gshared static extern(C) { @@ -2850,6 +3183,8 @@ version(use_openssl) { void function() OpenSSL_add_all_digests; /* } */ + const(char)* function(int) OpenSSL_version; + void function(ulong, void*) OPENSSL_init_crypto; void function(print_errors_cb, void*) ERR_print_errors_cb; @@ -2865,154 +3200,60 @@ version(use_openssl) { X509* function(FILE *fp, X509 **x) d2i_X509_fp; X509* function(X509** a, const(ubyte*)* pp, c_long length) d2i_X509; + int function(X509* a, ubyte** o) i2d_X509; + + int function(X509_VERIFY_PARAM* a, const char* b, size_t l) X509_VERIFY_PARAM_set1_host; + + X509* function(X509_STORE_CTX *ctx) X509_STORE_CTX_get_current_cert; + int function(X509_STORE_CTX *ctx) X509_STORE_CTX_get_error; } } - extern(C) - alias print_errors_cb = int function(const char*, size_t, void*); + struct OpenSSL { + static: - int SSL_CTX_set_default_verify_paths(SSL_CTX* a) { - if(ossllib.SSL_CTX_set_default_verify_paths) - return ossllib.SSL_CTX_set_default_verify_paths(a); - else throw new Exception("SSL_CTX_set_default_verify_paths not loaded"); - } + template opDispatch(string name) { + auto opDispatch(T...)(T t) { + static if(__traits(hasMember, ossllib, name)) { + auto ptr = __traits(getMember, ossllib, name); + } else static if(__traits(hasMember, eallib, name)) { + auto ptr = __traits(getMember, eallib, name); + } else static assert(0); - c_long SSL_get_verify_result(const SSL* ssl) { - if(ossllib.SSL_get_verify_result) - return ossllib.SSL_get_verify_result(ssl); - else throw new Exception("SSL_get_verify_result not loaded"); - } + if(ptr is null) + throw new Exception(name ~ " not loaded"); + return ptr(t); + } + } - X509_STORE* SSL_CTX_get_cert_store(SSL_CTX* a) { - if(ossllib.SSL_CTX_get_cert_store) - return ossllib.SSL_CTX_get_cert_store(a); - else throw new Exception("SSL_CTX_get_cert_store not loaded"); - } + // macros in the original C + SSL_METHOD* SSLv23_client_method() { + if(ossllib.SSLv23_client_method) + return ossllib.SSLv23_client_method(); + else + return ossllib.TLS_client_method(); + } - SSL_CTX* SSL_CTX_new(const SSL_METHOD* a) { - if(ossllib.SSL_CTX_new) - return ossllib.SSL_CTX_new(a); - else throw new Exception("SSL_CTX_new not loaded"); - } - SSL* SSL_new(SSL_CTX* a) { - if(ossllib.SSL_new) - return ossllib.SSL_new(a); - else throw new Exception("SSL_new not loaded"); - } - int SSL_set_fd(SSL* a, int b) { - if(ossllib.SSL_set_fd) - return ossllib.SSL_set_fd(a, b); - else throw new Exception("SSL_set_fd not loaded"); - } + void SSL_set_tlsext_host_name(SSL* a, const char* b) { + if(ossllib.SSL_ctrl) + return ossllib.SSL_ctrl(a, 55 /*SSL_CTRL_SET_TLSEXT_HOSTNAME*/, 0 /*TLSEXT_NAMETYPE_host_name*/, cast(void*) b); + else throw new Exception("SSL_set_tlsext_host_name not loaded"); + } - extern(C) - alias client_cert_cb = int function(SSL *ssl, X509 **x509, EVP_PKEY **pkey); + // special case + @trusted nothrow @nogc int SSL_shutdown(SSL* a) { + if(ossllib.SSL_shutdown) + return ossllib.SSL_shutdown(a); + assert(0); + } - void SSL_CTX_set_client_cert_cb(SSL_CTX *ctx, client_cert_cb cb) { - if(ossllib.SSL_CTX_set_client_cert_cb) - return ossllib.SSL_CTX_set_client_cert_cb(ctx, cb); - else throw new Exception("SSL_CTX_set_client_cert_cb not loaded"); - } + void SSL_CTX_keylog_cb_func(SSL_CTX* ctx, keylog_cb func) { + // this isn't in openssl 1.0 and is non-essential, so it is allowed to fail. + if(ossllib.SSL_CTX_set_keylog_callback) + ossllib.SSL_CTX_set_keylog_callback(ctx, func); + //else throw new Exception("SSL_CTX_keylog_cb_func not loaded"); + } - X509* d2i_X509(X509** a, const(ubyte*)* pp, c_long length) { - if(eallib.d2i_X509) - return eallib.d2i_X509(a, pp, length); - else throw new Exception("d2i_X509 not loaded"); - } - - X509* PEM_read_X509(FILE *fp, X509 **x, pem_password_cb *cb, void *u) { - if(eallib.PEM_read_X509) - return eallib.PEM_read_X509(fp, x, cb, u); - else throw new Exception("PEM_read_X509 not loaded"); - } - EVP_PKEY* PEM_read_PrivateKey(FILE *fp, EVP_PKEY **x, pem_password_cb *cb, void *u) { - if(eallib.PEM_read_PrivateKey) - return eallib.PEM_read_PrivateKey(fp, x, cb, u); - else throw new Exception("PEM_read_PrivateKey not loaded"); - } - - EVP_PKEY* d2i_PrivateKey_fp(FILE *fp, EVP_PKEY **a) { - if(eallib.d2i_PrivateKey_fp) - return eallib.d2i_PrivateKey_fp(fp, a); - else throw new Exception("d2i_PrivateKey_fp not loaded"); - } - X509* d2i_X509_fp(FILE *fp, X509 **x) { - if(eallib.d2i_X509_fp) - return eallib.d2i_X509_fp(fp, x); - else throw new Exception("d2i_X509_fp not loaded"); - } - - int SSL_connect(SSL* a) { - if(ossllib.SSL_connect) - return ossllib.SSL_connect(a); - else throw new Exception("SSL_connect not loaded"); - } - int SSL_write(SSL* a, const void* b, int c) { - if(ossllib.SSL_write) - return ossllib.SSL_write(a, b, c); - else throw new Exception("SSL_write not loaded"); - } - int SSL_read(SSL* a, void* b, int c) { - if(ossllib.SSL_read) - return ossllib.SSL_read(a, b, c); - else throw new Exception("SSL_read not loaded"); - } - @trusted nothrow @nogc int SSL_shutdown(SSL* a) { - if(ossllib.SSL_shutdown) - return ossllib.SSL_shutdown(a); - assert(0); - } - void SSL_free(SSL* a) { - if(ossllib.SSL_free) - return ossllib.SSL_free(a); - else throw new Exception("SSL_free not loaded"); - } - void SSL_CTX_free(SSL_CTX* a) { - if(ossllib.SSL_CTX_free) - return ossllib.SSL_CTX_free(a); - else throw new Exception("SSL_CTX_free not loaded"); - } - - int SSL_pending(const SSL* a) { - if(ossllib.SSL_pending) - return ossllib.SSL_pending(a); - else throw new Exception("SSL_pending not loaded"); - } - void SSL_set_verify(SSL* a, int b, void* c) { - if(ossllib.SSL_set_verify) - return ossllib.SSL_set_verify(a, b, c); - else throw new Exception("SSL_set_verify not loaded"); - } - void SSL_set_tlsext_host_name(SSL* a, const char* b) { - if(ossllib.SSL_ctrl) - return ossllib.SSL_ctrl(a, 55 /*SSL_CTRL_SET_TLSEXT_HOSTNAME*/, 0 /*TLSEXT_NAMETYPE_host_name*/, cast(void*) b); - else throw new Exception("SSL_set_tlsext_host_name not loaded"); - } - - SSL_METHOD* SSLv3_client_method() { - if(ossllib.SSLv3_client_method) - return ossllib.SSLv3_client_method(); - else throw new Exception("SSLv3_client_method not loaded"); - } - SSL_METHOD* TLS_client_method() { - if(ossllib.TLS_client_method) - return ossllib.TLS_client_method(); - else throw new Exception("TLS_client_method not loaded"); - } - void ERR_print_errors_cb(print_errors_cb cb, void* u) { - if(eallib.ERR_print_errors_cb) - return eallib.ERR_print_errors_cb(cb, u); - else throw new Exception("ERR_print_errors_cb not loaded"); - } - void X509_free(X509* x) { - if(eallib.X509_free) - return eallib.X509_free(x); - else throw new Exception("X509_free not loaded"); - } - int X509_STORE_add_cert(X509_STORE* s, X509* x) { - if(eallib.X509_STORE_add_cert) - return eallib.X509_STORE_add_cert(s, x); - else throw new Exception("X509_STORE_add_cert not loaded"); } extern(C) @@ -3024,15 +3265,6 @@ version(use_openssl) { return 0; } - extern(C) - void SSL_CTX_keylog_cb_func(SSL_CTX* ctx, void function(SSL*, char*) func) - { - // this isn't in openssl 1.0 and is non-essential, so it is allowed to fail. - if(ossllib.SSL_CTX_set_keylog_callback) - ossllib.SSL_CTX_set_keylog_callback(ctx, func); - //else throw new Exception("SSL_CTX_keylog_cb_func not loaded"); - } - private __gshared void* ossllib_handle; version(Windows) @@ -3054,29 +3286,58 @@ version(use_openssl) { return; synchronized(loadSslMutex) { - version(OSX) { - // newest box - ossllib_handle = dlopen("libssl.1.1.dylib", RTLD_NOW); - // other boxes - if(ossllib_handle is null) - ossllib_handle = dlopen("libssl.dylib", RTLD_NOW); - // old ones like my laptop test - if(ossllib_handle is null) - ossllib_handle = dlopen("/usr/local/opt/openssl/lib/libssl.1.0.0.dylib", RTLD_NOW); + version(Posix) { + version(OSX) { + static immutable string[] ossllibs = [ + "libssl.46.dylib", + "libssl.44.dylib", + "libssl.43.dylib", + "libssl.35.dylib", + "libssl.1.1.dylib", + "libssl.dylib", + "/usr/local/opt/openssl/lib/libssl.1.0.0.dylib", + ]; + } else { + static immutable string[] ossllibs = [ + "libssl.so.1.1", + "libssl.so.1.0.2", + "libssl.so.1.0.1", + "libssl.so.1.0.0", + "libssl.so", + ]; + } - } else version(Posix) { - ossllib_handle = dlopen("libssl.so.1.1", RTLD_NOW); - if(ossllib_handle is null) - ossllib_handle = dlopen("libssl.so", RTLD_NOW); + foreach(lib; ossllibs) { + ossllib_handle = dlopen(lib.ptr, RTLD_NOW); + if(ossllib_handle !is null) break; + } } else version(Windows) { - version(X86_64) + version(X86_64) { ossllib_handle = LoadLibraryW("libssl-1_1-x64.dll"w.ptr); - if(ossllib_handle is null) - ossllib_handle = LoadLibraryW("libssl32.dll"w.ptr); - version(X86_64) oeaylib_handle = LoadLibraryW("libcrypto-1_1-x64.dll"w.ptr); + } + + static immutable wstring[] ossllibs = [ + "libssl-1_1.dll"w, + "libssl32.dll"w, + ]; + + if(ossllib_handle is null) + foreach(lib; ossllibs) { + ossllib_handle = LoadLibraryW(lib.ptr); + if(ossllib_handle !is null) break; + } + + static immutable wstring[] eaylibs = [ + "libcrypto-1_1.dll"w, + "libeay32.dll", + ]; + if(oeaylib_handle is null) - oeaylib_handle = LoadLibraryW("libeay32.dll"w.ptr); + foreach(lib; eaylibs) { + oeaylib_handle = LoadLibraryW(lib.ptr); + if (oeaylib_handle !is null) break; + } if(ossllib_handle is null) { ossllib_handle = LoadLibraryW("ssleay32.dll"w.ptr); @@ -3160,7 +3421,7 @@ version(use_openssl) { string logfile = environment.get("SSLKEYLOGFILE"); if (logfile !is null) { - auto f = std.stdio.File("/tmp/keyfile", "a+"); + auto f = std.stdio.File(logfile, "a+"); f.writeln(fromStringz(line)); f.close(); } @@ -3170,28 +3431,33 @@ version(use_openssl) { private SSL* ssl; private SSL_CTX* ctx; private void initSsl(bool verifyPeer, string hostname) { - ctx = SSL_CTX_new(SSLv23_client_method()); + ctx = OpenSSL.SSL_CTX_new(OpenSSL.SSLv23_client_method()); assert(ctx !is null); - SSL_CTX_set_default_verify_paths(ctx); - version(Windows) - loadCertificatesFromRegistry(ctx); + debug OpenSSL.SSL_CTX_keylog_cb_func(ctx, &write_to_file); + ssl = OpenSSL.SSL_new(ctx); - debug SSL_CTX_keylog_cb_func(ctx, &write_to_file); - ssl = SSL_new(ctx); + if(hostname.length) { + OpenSSL.SSL_set_tlsext_host_name(ssl, toStringz(hostname)); + if(verifyPeer) + OpenSSL.X509_VERIFY_PARAM_set1_host(OpenSSL.SSL_get0_param(ssl), hostname.ptr, hostname.length); + } - if(hostname.length) - SSL_set_tlsext_host_name(ssl, toStringz(hostname)); + if(verifyPeer) { + OpenSSL.SSL_CTX_set_default_verify_paths(ctx); - if(verifyPeer) - SSL_set_verify(ssl, SSL_VERIFY_PEER, null); - else - SSL_set_verify(ssl, SSL_VERIFY_NONE, null); + version(Windows) { + loadCertificatesFromRegistry(ctx); + } - 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 + OpenSSL.SSL_set_verify(ssl, SSL_VERIFY_PEER, &verifyCertificateFromRegistryArsdHttp); + } else + OpenSSL.SSL_set_verify(ssl, SSL_VERIFY_NONE, null); + + 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 - SSL_CTX_set_client_cert_cb(ctx, &cb); + OpenSSL.SSL_CTX_set_client_cert_cb(ctx, &cb); } extern(C) @@ -3216,12 +3482,12 @@ version(use_openssl) { else goto case der; case pem: - *x509 = PEM_read_X509(fpCert, null, null, null); - *pkey = PEM_read_PrivateKey(fpKey, null, null, null); + *x509 = OpenSSL.PEM_read_X509(fpCert, null, null, null); + *pkey = OpenSSL.PEM_read_PrivateKey(fpKey, null, null, null); break; case der: - *x509 = d2i_X509_fp(fpCert, null); - *pkey = d2i_PrivateKey_fp(fpKey, null); + *x509 = OpenSSL.d2i_X509_fp(fpCert, null); + *pkey = OpenSSL.d2i_PrivateKey_fp(fpKey, null); break; } @@ -3232,7 +3498,7 @@ version(use_openssl) { } bool dataPending() { - return SSL_pending(ssl) > 0; + return OpenSSL.SSL_pending(ssl) > 0; } @trusted @@ -3243,14 +3509,14 @@ version(use_openssl) { @trusted void do_ssl_connect() { - if(SSL_connect(ssl) == -1) { + if(OpenSSL.SSL_connect(ssl) == -1) { string str; - ERR_print_errors_cb(&collectSslErrors, &str); + OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); int i; - auto err = SSL_get_verify_result(ssl); + auto err = OpenSSL.SSL_get_verify_result(ssl); //printf("wtf\n"); //scanf("%d\n", i); - throw new Exception("ssl connect failed " ~ str ~ " // " ~ to!string(err)); + throw new Exception("Secure connect failed: " ~ getOpenSslErrorCode(err)); } } @@ -3258,10 +3524,10 @@ version(use_openssl) { override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) { //import std.stdio;writeln(cast(string) buf); debug(arsd_http2_verbose) writeln("ssl writing ", buf.length); - auto retval = SSL_write(ssl, buf.ptr, cast(uint) buf.length); + auto retval = OpenSSL.SSL_write(ssl, buf.ptr, cast(uint) buf.length); if(retval == -1) { string str; - ERR_print_errors_cb(&collectSslErrors, &str); + OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); int i; //printf("wtf\n"); //scanf("%d\n", i); @@ -3277,11 +3543,11 @@ version(use_openssl) { override ptrdiff_t receive(scope void[] buf, SocketFlags flags) { debug(arsd_http2_verbose) writeln("ssl_read before"); - auto retval = SSL_read(ssl, buf.ptr, cast(int)buf.length); + auto retval = OpenSSL.SSL_read(ssl, buf.ptr, cast(int)buf.length); debug(arsd_http2_verbose) writeln("ssl_read after"); if(retval == -1) { string str; - ERR_print_errors_cb(&collectSslErrors, &str); + OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str); int i; //printf("wtf\n"); //scanf("%d\n", i); @@ -3299,7 +3565,7 @@ version(use_openssl) { } override void close() { - if(ssl) SSL_shutdown(ssl); + if(ssl) OpenSSL.SSL_shutdown(ssl); super.close(); } @@ -3311,8 +3577,8 @@ version(use_openssl) { void freeSsl() { if(ssl is null) return; - SSL_free(ssl); - SSL_CTX_free(ctx); + OpenSSL.SSL_free(ssl); + OpenSSL.SSL_CTX_free(ctx); ssl = null; } @@ -4466,9 +4732,9 @@ class WebSocket { } } - private bool loopExited; + private __gshared bool loopExited; /++ - + Exits the running [WebSocket.eventLoop]. You can call this from a signal handler or another thread. +/ void exitEventLoop() { loopExited = true; @@ -4803,11 +5069,62 @@ public { } } +private extern(C) +int verifyCertificateFromRegistryArsdHttp(int preverify_ok, X509_STORE_CTX* ctx) { + version(Windows) { + if(preverify_ok) + return 1; + + auto err_cert = OpenSSL.X509_STORE_CTX_get_current_cert(ctx); + auto err = OpenSSL.X509_STORE_CTX_get_error(ctx); + + if(err == 62) + return 0; // hostname mismatch is an error we can trust; that means OpenSSL already found the certificate and rejected it + + auto len = OpenSSL.i2d_X509(err_cert, null); + if(len == -1) + return 0; + ubyte[] buffer = new ubyte[](len); + auto ptr = buffer.ptr; + len = OpenSSL.i2d_X509(err_cert, &ptr); + if(len != buffer.length) + return 0; + + + CERT_CHAIN_PARA thing; + thing.cbSize = thing.sizeof; + auto context = CertCreateCertificateContext(X509_ASN_ENCODING, buffer.ptr, cast(int) buffer.length); + if(context is null) + return 0; + scope(exit) CertFreeCertificateContext(context); + + PCCERT_CHAIN_CONTEXT chain; + if(CertGetCertificateChain(null, context, null, null, &thing, 0, null, &chain)) { + scope(exit) + CertFreeCertificateChain(chain); + + DWORD errorStatus = chain.TrustStatus.dwErrorStatus; + + if(errorStatus == 0) + return 1; // Windows approved it, OK carry on + // otherwise, sustain OpenSSL's original ruling + } + + return 0; + } else { + return preverify_ok; + } +} + + version(Windows) { pragma(lib, "crypt32"); import core.sys.windows.wincrypt; - extern(Windows) + extern(Windows) { PCCERT_CONTEXT CertEnumCertificatesInStore(HCERTSTORE hCertStore, PCCERT_CONTEXT pPrevCertContext); + // BOOL CertGetCertificateChain(HCERTCHAINENGINE hChainEngine, PCCERT_CONTEXT pCertContext, LPFILETIME pTime, HCERTSTORE hAdditionalStore, PCERT_CHAIN_PARA pChainPara, DWORD dwFlags, LPVOID pvReserved, PCCERT_CHAIN_CONTEXT *ppChainContext); + PCCERT_CONTEXT CertCreateCertificateContext(DWORD dwCertEncodingType, const BYTE *pbCertEncoded, DWORD cbCertEncoded); + } void loadCertificatesFromRegistry(SSL_CTX* ctx) { auto store = CertOpenSystemStore(0, "ROOT"); @@ -4818,7 +5135,7 @@ version(Windows) { scope(exit) CertCloseStore(store, 0); - X509_STORE* ssl_store = SSL_CTX_get_cert_store(ctx); + X509_STORE* ssl_store = OpenSSL.SSL_CTX_get_cert_store(ctx); PCCERT_CONTEXT c; while((c = CertEnumCertificatesInStore(store, c)) !is null) { FILETIME na = c.pCertInfo.NotAfter; @@ -4839,14 +5156,20 @@ version(Windows) { } const(ubyte)* thing = c.pbCertEncoded; - auto x509 = d2i_X509(null, &thing, c.cbCertEncoded); + auto x509 = OpenSSL.d2i_X509(null, &thing, c.cbCertEncoded); if (x509) { - auto success = X509_STORE_add_cert(ssl_store, x509); - X509_free(x509); + auto success = OpenSSL.X509_STORE_add_cert(ssl_store, x509); + //if(!success) + //writeln("FAILED HERE"); + OpenSSL.X509_free(x509); + } else { + //writeln("FAILED"); } } CertFreeCertificateContext(c); + + // import core.stdc.stdio; printf("%s\n", OpenSSL.OpenSSL_version(0)); } diff --git a/joystick.d b/joystick.d index f73f1bb..9d183cd 100644 --- a/joystick.d +++ b/joystick.d @@ -199,7 +199,9 @@ version(linux) { break; else assert(0); // , to!string(fd) ~ " " ~ to!string(errno)); } - assert(r == event.sizeof); + if(r != event.sizeof) + throw new Exception("Read something weird off the joystick event fd"); + //import std.stdio; writeln(event); ptrdiff_t player = -1; foreach(i, f; joystickFds) @@ -230,6 +232,7 @@ version(linux) { } if(event.type & JS_EVENT_BUTTON) { joystickState[player].buttons[event.number] = event.value ? 255 : 0; + //writeln(player, " ", event.number, " ", event.value, " ", joystickState[player].buttons[event.number]);//, " != ", event.value ? 255 : 0); } } } diff --git a/minigui.d b/minigui.d index 73c125c..7a3e63a 100644 --- a/minigui.d +++ b/minigui.d @@ -1925,6 +1925,18 @@ class Widget : ReflectableProperties { return StyleInformation(this); } + int focusableWidgets(scope int delegate(Widget) dg) { + foreach(widget; WidgetStream(this)) { + if(widget.tabStop && !widget.hidden) { + int result = dg(widget); + if (result) + return result; + } + } + return 0; + } + + // FIXME: I kinda want to hide events from implementation widgets // so it just catches them all and stops propagation... // i guess i can do it with a event listener on star. @@ -7084,7 +7096,7 @@ class HorizontalLayout : Layout { } -version(Windows) +version(win32_widgets) private extern(Windows) LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { @@ -7944,6 +7956,12 @@ class Window : Widget { Window.newWindowCreated(this); } + version(custom_widgets) + override void defaultEventHandler_click(ClickEvent event) { + if(event.target && event.target.tabStop) + event.target.focus(); + } + private static void delegate(Window) newWindowCreated; version(win32_widgets) @@ -8082,17 +8100,63 @@ class Window : Widget { } win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); + static if(UsingSimpledisplayX11) { + ///+ + // for input proxy + auto display = XDisplayConnection.get; + auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); + XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask); + XMapWindow(display, inputProxy); + //import std.stdio; writefln("input proxy: 0x%0x", inputProxy); + this.inputProxy = new SimpleWindow(inputProxy); + + XEvent lastEvent; + this.inputProxy.handleNativeEvent = (XEvent ev) { + lastEvent = ev; + return 1; + }; + this.inputProxy.setEventHandlers( + (MouseEvent e) { + dispatchMouseEvent(e); + }, + (KeyEvent e) { + //import std.stdio; + //writefln("%x %s", cast(uint) e.key, e.key); + if(dispatchKeyEvent(e)) { + // FIXME: i should trap error + if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { + auto thing = nw.focusableWindow(); + if(thing && thing.window) { + lastEvent.xkey.window = thing.window; + // import std.stdio; writeln("sending event ", lastEvent.xkey); + trapXErrors( { + XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); + }); + } + } + } + }, + (dchar e) { + if(e == 13) e = 10; // hack? + if(e == 127) return; // linux sends this, windows doesn't. we don't want it. + dispatchCharEvent(e); + }, + ); + // done + //+/ + } + + + win.setRequestedInputFocus = &this.setRequestedInputFocus; this(win); } + SimpleWindow inputProxy; + private SimpleWindow setRequestedInputFocus() { - if(auto fw = cast(NestedChildWindowWidget) focusedWidget) { - // sdpyPrintDebugString("heaven"); - return fw.focusableWindow; - } - return win; + return inputProxy; } /// ditto @@ -8126,13 +8190,15 @@ class Window : Widget { event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; event.dispatch(); - return true; + return !event.propagationStopped; } + // returns true if propagation should continue into nested things.... prolly not a great thing to do. bool dispatchCharEvent(dchar ch) { if(focusedWidget) { auto event = new CharEvent(focusedWidget, ch); event.dispatch(); + return !event.propagationStopped; } return true; } @@ -8249,7 +8315,7 @@ class Window : Widget { } } - return true; + return true; // FIXME: the event default prevented? } /++ @@ -8295,18 +8361,29 @@ class Window : Widget { } static Widget getFirstFocusable(Widget start) { - if(start.tabStop && !start.hidden) - return start; + if(start is null) + return null; - if(!start.hidden) - foreach(child; start.children) { - auto f = getFirstFocusable(child); - if(f !is null) - return f; + foreach(widget; &start.focusableWidgets) { + return widget; } + return null; } + static Widget getLastFocusable(Widget start) { + if(start is null) + return null; + + Widget last; + foreach(widget; &start.focusableWidgets) { + last = widget; + } + + return last; + } + + mixin Emits!ClosingEvent; mixin Emits!ClosedEvent; } @@ -10409,9 +10486,10 @@ class Menu : Window { if(!menuParent.parentWindow.win.closed) { if(auto maw = cast(MouseActivatedWidget) menuParent) { maw.setDynamicState(DynamicState.depressed, false); + maw.setDynamicState(DynamicState.hover, false); maw.redraw(); } - menuParent.parentWindow.win.focus(); + // menuParent.parentWindow.win.focus(); } clickListener.disconnect(); } @@ -11285,7 +11363,7 @@ class ImageBox : Widget { if(this.parentWindow && this.parentWindow.win) { if(sprite) sprite.dispose(); - sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); + sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); } redraw(); } @@ -11319,7 +11397,7 @@ class ImageBox : Widget { private void updateSprite() { if(sprite is null && this.parentWindow && this.parentWindow.win) { - sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_)); + sprite = new Sprite(this.parentWindow.win, Image.fromMemoryImage(image_, true)); } } @@ -14177,6 +14255,117 @@ interface ReflectableProperties { } } +private struct Stack(T) { + this(int maxSize) { + internalLength = 0; + arr = initialBuffer[]; + } + + ///. + void push(T t) { + if(internalLength >= arr.length) { + auto oldarr = arr; + if(arr.length < 4096) + arr = new T[arr.length * 2]; + else + arr = new T[arr.length + 4096]; + arr[0 .. oldarr.length] = oldarr[]; + } + + arr[internalLength] = t; + internalLength++; + } + + ///. + T pop() { + assert(internalLength); + internalLength--; + return arr[internalLength]; + } + + ///. + T peek() { + assert(internalLength); + return arr[internalLength - 1]; + } + + ///. + @property bool empty() { + return internalLength ? false : true; + } + + ///. + private T[] arr; + private size_t internalLength; + private T[64] initialBuffer; + // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), + // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() + // function thanks to this, and push() was actually one of the slowest individual functions in the code! +} + +/// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. +private struct WidgetStream { + + ///. + @property Widget front() { + return current.widget; + } + + /// Use Widget.tree instead. + this(Widget start) { + current.widget = start; + current.childPosition = -1; + isEmpty = false; + stack = typeof(stack)(0); + } + + /* + Handle it + handle its children + + */ + + ///. + void popFront() { + more: + if(isEmpty) return; + + // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) + + current.childPosition++; + if(current.childPosition >= current.widget.children.length) { + if(stack.empty()) + isEmpty = true; + else { + current = stack.pop(); + goto more; + } + } else { + stack.push(current); + current.widget = current.widget.children[current.childPosition]; + current.childPosition = -1; + } + } + + ///. + @property bool empty() { + return isEmpty; + } + + private: + + struct Current { + Widget widget; + int childPosition; + } + + Current current; + + Stack!(Current) stack; + + bool isEmpty; +} + /+ diff --git a/minigui_addons/webview.d b/minigui_addons/webview.d index 71129d9..53b05de 100644 --- a/minigui_addons/webview.d +++ b/minigui_addons/webview.d @@ -360,7 +360,7 @@ class WebViewWidget_CEF : WebViewWidgetBase { //semaphore = new Semaphore; assert(CefApp.active); - this(new MiniguiCefClient(openNewWindow), parent); + this(new MiniguiCefClient(openNewWindow), parent, false); cef_window_info_t window_info; window_info.parent_window = containerWindow.nativeWindowHandle; @@ -398,7 +398,7 @@ class WebViewWidget_CEF : WebViewWidgetBase { .destroy(this); // but this is ok to do some memory management cleanup } - private this(MiniguiCefClient client, Widget parent) { + private this(MiniguiCefClient client, Widget parent, bool isDevTools) { super(parent); this.client = client; @@ -407,22 +407,56 @@ class WebViewWidget_CEF : WebViewWidgetBase { mapping[containerWindow.nativeWindowHandle()] = this; - - this.parentWindow.addEventListener((FocusEvent fe) { - if(!browserHandle) return; - //browserHandle.get_host.set_focus(true); - - executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));"); + this.addEventListener(delegate(KeyDownEvent ke) { + if(ke.key == Key.Tab) + ke.preventDefault(); }); - this.parentWindow.addEventListener((BlurEvent be) { + + this.addEventListener((FocusEvent fe) { if(!browserHandle) return; - executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));"); + XFocusChangeEvent ev; + ev.type = arsd.simpledisplay.EventType.FocusIn; + ev.display = XDisplayConnection.get; + ev.window = ozone; + ev.mode = NotifyModes.NotifyNormal; + ev.detail = NotifyDetail.NotifyVirtual; + + trapXErrors( { + XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev); + }); + + // this also works if the message is buggy and it avoids weirdness from raising window etc + //executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));"); + }); + this.addEventListener((BlurEvent be) { + if(!browserHandle) return; + + XFocusChangeEvent ev; + ev.type = arsd.simpledisplay.EventType.FocusOut; + ev.display = XDisplayConnection.get; + ev.window = ozone; + ev.mode = NotifyModes.NotifyNormal; + ev.detail = NotifyDetail.NotifyNonlinearVirtual; + + trapXErrors( { + XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev); + }); + + //executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));"); }); bool closeAttempted = false; + if(isDevTools) this.parentWindow.addEventListener((scope ClosingEvent ce) { + this.parentWindow.hide(); + ce.preventDefault(); + }); + else + this.parentWindow.addEventListener((scope ClosingEvent ce) { + if(devTools) + devTools.close(); if(!closeAttempted && browserHandle) { browserHandle.get_host.close_browser(true); ce.preventDefault(); @@ -450,11 +484,67 @@ class WebViewWidget_CEF : WebViewWidgetBase { } private NativeWindowHandle browserWindow; + private NativeWindowHandle ozone; private RC!cef_browser_t browserHandle; private static WebViewWidget[NativeWindowHandle] mapping; private static WebViewWidget[NativeWindowHandle] browserMapping; + private { + int findingIdent; + string findingText; + bool findingCase; + } + + // might not be stable, webview does this fully integrated + void findText(string text, bool forward = true, bool matchCase = false, bool findNext = false) { + if(browserHandle) { + auto host = browserHandle.get_host(); + + static ident = 0; + auto txt = cef_string_t(text); + host.find(++ident, &txt, forward, matchCase, findNext); + + findingIdent = ident; + findingText = text; + findingCase = matchCase; + } + } + + // ditto + void findPrevious() { + if(findingIdent == 0) + return; + if(!browserHandle) + return; + auto host = browserHandle.get_host(); + auto txt = cef_string_t(findingText); + host.find(findingIdent, &txt, 0, findingCase, 1); + } + + // ditto + void findNext() { + if(findingIdent == 0) + return; + if(!browserHandle) + return; + auto host = browserHandle.get_host(); + auto txt = cef_string_t(findingText); + host.find(findingIdent, &txt, 1, findingCase, 1); + } + + // ditto + void stopFind() { + if(findingIdent == 0) + return; + if(!browserHandle) + return; + auto host = browserHandle.get_host(); + host.stop_finding(1); + + findingIdent = 0; + } + override void refresh() { if(browserHandle) browserHandle.reload(); } override void back() { if(browserHandle) browserHandle.go_back(); } override void forward() { if(browserHandle) browserHandle.go_forward(); } @@ -475,9 +565,37 @@ class WebViewWidget_CEF : WebViewWidgetBase { browserHandle.get_main_frame.execute_java_script(&c, &u, line); } + private Window devTools; override void showDevTools() { if(!browserHandle) return; - browserHandle.get_host.show_dev_tools(null /* window info */, client.passable, null /* settings */, null /* inspect element at coordinates */); + + if(devTools is null) { + auto host = browserHandle.get_host; + + if(host.has_dev_tools()) { + host.close_dev_tools(); + return; + } + + cef_window_info_t windowinfo; + version(linux) { + auto sw = new Window("DevTools"); + //sw.win.beingOpenKeepsAppOpen = false; + devTools = sw; + + auto wv = new WebViewWidget_CEF(client, sw, true); + + sw.show(); + + windowinfo.parent_window = wv.containerWindow.nativeWindowHandle; + } + host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */); + } else { + if(devTools.hidden) + devTools.show(); + else + devTools.hide(); + } } // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. @@ -534,7 +652,7 @@ version(cef) { cef_dictionary_value_t** extra_info, int* no_javascript_access ) { - + sdpyPrintDebugString("on_before_popup"); if(this.client.openNewWindow is null) return 1; // new windows disabled @@ -548,10 +666,10 @@ version(cef) { runInGuiThread({ ret = 1; - scope WebViewWidget delegate(Widget, BrowserSettings) o = (parent, passed_settings) { + scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) { ret = 0; if(parent !is null) { - auto widget = new WebViewWidget_CEF(this.client, parent); + auto widget = new WebViewWidget_CEF(this.client, parent, false); (*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle; passed_settings.set(browser_settings); @@ -560,7 +678,7 @@ version(cef) { } return null; }; - this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, o)); + this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, accept)); return; }); @@ -589,10 +707,13 @@ version(cef) { import arsd.simpledisplay : Window; Window root; Window parent; + Window ozone; uint c = 0; auto display = XDisplayConnection.get; Window* children; XQueryTree(display, handle, &root, &parent, &children, &c); + if(c == 1) + ozone = children[0]; XFree(children); } else static assert(0); @@ -600,8 +721,9 @@ version(cef) { auto wv = *wvp; wv.browserWindow = handle; wv.browserHandle = RC!cef_browser_t(ptr); + wv.ozone = ozone ? ozone : handle; - wv.browserWindowWrapped = new SimpleWindow(wv.browserWindow); + wv.browserWindowWrapped = new SimpleWindow(wv.ozone); /+ XSelectInput(XDisplayConnection.get, handle, EventMask.FocusChangeMask); @@ -831,15 +953,70 @@ version(cef) { } } + class MiniguiRequestHandler : CEF!cef_request_handler_t { + override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow { + return 0; + } + override int on_open_urlfrom_tab(RC!(cef_browser_t), RC!(cef_frame_t), const(cef_string_utf16_t)*, cef_window_open_disposition_t, int) nothrow { + return 0; + } + override cef_resource_request_handler_t* get_resource_request_handler(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int, const(cef_string_utf16_t)*, int*) nothrow { + return null; + } + override int get_auth_credentials(RC!(cef_browser_t), const(cef_string_utf16_t)*, int, const(cef_string_utf16_t)*, int, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, RC!(cef_auth_callback_t)) nothrow { + // this is for http basic auth popup..... + return 0; + } + override int on_quota_request(RC!(cef_browser_t), const(cef_string_utf16_t)*, long, RC!(cef_callback_t)) nothrow { + return 0; + } + override int on_certificate_error(RC!(cef_browser_t), cef_errorcode_t, const(cef_string_utf16_t)*, RC!(cef_sslinfo_t), RC!(cef_callback_t)) nothrow { + return 0; + } + override int on_select_client_certificate(RC!(cef_browser_t), int, const(cef_string_utf16_t)*, int, ulong, cef_x509certificate_t**, RC!(cef_select_client_certificate_callback_t)) nothrow { + return 0; + } + override void on_plugin_crashed(RC!(cef_browser_t), const(cef_string_utf16_t)*) nothrow { + + } + override void on_render_view_ready(RC!(cef_browser_t) p) nothrow { + + } + override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow { + + } + override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow { + browser.runOnWebView(delegate(wv) { + wv.executeJavascript("console.log('here');"); + }); + + } + } + class MiniguiFocusHandler : CEF!cef_focus_handler_t { override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow { - // sdpyPrintDebugString("take"); + browser.runOnWebView(delegate(wv) { + Widget f; + if(next) { + f = Window.getFirstFocusable(wv.parentWindow); + } else { + foreach(w; &wv.parentWindow.focusableWidgets) { + if(w is wv) + break; + f = w; + } + } + if(f) + f.focus(); + }); } override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow { - //browser.runOnWebView((ev) { + /+ + browser.runOnWebView((ev) { + ev.focus(); // even this can steal focus from other parts of my application! + }); + +/ //sdpyPrintDebugString("setting"); - //ev.parentWindow.focusedWidget = ev; - //}); return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen. // seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent @@ -848,7 +1025,12 @@ version(cef) { // it also breaks its own pop up menus and drop down boxes to allow this! wtf } override void on_got_focus(RC!(cef_browser_t) browser) nothrow { - // sdpyPrintDebugString("got"); + browser.runOnWebView((ev) { + // this sometimes steals from the app too but it is relatively acceptable + // steals when i mouse in from the side of the window quickly, but still + // i want the minigui state to match so i'll allow it + ev.focus(); + }); } } @@ -863,6 +1045,7 @@ version(cef) { MiniguiDownloadHandler downloadHandler; MiniguiKeyboardHandler keyboardHandler; MiniguiFocusHandler focusHandler; + MiniguiRequestHandler requestHandler; this(void delegate(scope OpenNewWindowParams) openNewWindow) { this.openNewWindow = openNewWindow; lsh = new MiniguiCefLifeSpanHandler(this); @@ -872,6 +1055,7 @@ version(cef) { downloadHandler = new MiniguiDownloadHandler(); keyboardHandler = new MiniguiKeyboardHandler(); focusHandler = new MiniguiFocusHandler(); + requestHandler = new MiniguiRequestHandler(); } override cef_audio_handler_t* get_audio_handler() { @@ -915,10 +1099,12 @@ version(cef) { override cef_render_handler_t* get_render_handler() { // this thing might work for an off-screen thing // like to an image or to a video stream maybe + // + // might be useful to have it render here then send it over too for remote X sharing a process return null; } override cef_request_handler_t* get_request_handler() { - return null; + return requestHandler.returnable; } override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { return 0; // return 1 if you can actually handle the message diff --git a/postgres.d b/postgres.d index 593894f..d5f0d47 100644 --- a/postgres.d +++ b/postgres.d @@ -184,6 +184,19 @@ class PostgresResult : ResultSet { return row; } + int affectedRows() { + auto g = PQcmdTuples(res); + if(g is null) + return 0; + int num; + while(*g) { + num *= 10; + num += *g - '0'; + g++; + } + return num; + } + void popFront() { position++; if(position < numRows) @@ -308,6 +321,7 @@ extern(C) { int row_number, int column_number); + char* PQcmdTuples(PGresult *res); } diff --git a/simpledisplay.d b/simpledisplay.d index 00087a9..699f93b 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -12296,6 +12296,13 @@ version(X11) { if(width == 0 || height == 0) { XSetClipMask(display, gc, None); + if(xrenderPicturePainter) { + + XRectangle[1] rects; + rects[0] = XRectangle(short.min, short.min, short.max, short.max); + XRenderSetPictureClipRectangles(display, xrenderPicturePainter, 0, 0, rects.ptr, cast(int) rects.length); + } + version(with_xft) { if(xftFont is null || xftDraw is null) return; @@ -12306,6 +12313,9 @@ version(X11) { rects[0] = XRectangle(cast(short)(x), cast(short)(y), cast(short) width, cast(short) height); XSetClipRectangles(XDisplayConnection.get, gc, 0, 0, rects.ptr, 1, 0); + if(xrenderPicturePainter) + XRenderSetPictureClipRectangles(display, xrenderPicturePainter, 0, 0, rects.ptr, cast(int) rects.length); + version(with_xft) { if(xftFont is null || xftDraw is null) return; @@ -12572,6 +12582,12 @@ version(X11) { XRenderPictureAttributes attrs; // FIXME: I can prolly reuse this as long as the pixmap itself is valid. xrenderPicturePainter = XRenderCreatePicture(display, d, Sprite.RGB24, 0, &attrs); + + // need to initialize the clip + XRectangle[1] rects; + rects[0] = XRectangle(cast(short)(_clipRectangle.left), cast(short)(_clipRectangle.top), cast(short) _clipRectangle.width, cast(short) _clipRectangle.height); + + XRenderSetPictureClipRectangles(display, xrenderPicturePainter, 0, 0, rects.ptr, cast(int) rects.length); } XRenderComposite( @@ -13613,6 +13629,35 @@ mixin DynamicLoad!(XRandr, "Xrandr", 2, XRandrLibrarySuccessfullyLoaded) XRandrL } } + /++ + Platform-specific for X11. Traps errors for the duration of `dg`. Avoid calling this from inside a call to this. + + Please note that it returns + +/ + XErrorEvent[] trapXErrors(scope void delegate() dg) { + + static XErrorEvent[] errorBuffer; + + static extern(C) int handler (Display* dpy, XErrorEvent* evt) nothrow { + errorBuffer ~= *evt; + return 0; + } + + auto savedErrorHandler = XSetErrorHandler(&handler); + + try { + dg(); + } finally { + XSync(XDisplayConnection.get, 0/*False*/); + XSetErrorHandler(savedErrorHandler); + } + + auto bfr = errorBuffer; + errorBuffer = null; + + return bfr; + } + /// Platform-specific for X11. A singleton class (well, all its methods are actually static... so more like a namespace) wrapping a `Display*`. class XDisplayConnection { private __gshared Display* display; @@ -16986,6 +17031,9 @@ extern(C) { extern(C) alias XIOErrorHandler = int function (Display* display); } +extern(C) nothrow +alias XErrorHandler = int function(Display*, XErrorEvent*); + extern(C) nothrow @nogc { struct Screen{ XExtData *ext_data; /* hook for extension to hang data */ @@ -17190,8 +17238,6 @@ struct Visual byte pad; } - alias XErrorHandler = int function(Display*, XErrorEvent*); - struct XRectangle { short x; short y; diff --git a/terminal.d b/terminal.d index 1d5f215..ac0f56d 100644 --- a/terminal.d +++ b/terminal.d @@ -4875,7 +4875,7 @@ class LineGetter { it to `write` or `writeln`, you should return `["write", "writeln"]`. If you offer different tab complete in different places, you still - need to return the whole string. For example, a file competition of + need to return the whole string. For example, a file completion of a second argument, when the user writes `terminal.d term` and you want it to complete to an additional `terminal.d`, you should return `["terminal.d terminal.d"]`; in other words, `candidate ~ completion` @@ -6409,6 +6409,25 @@ class LineGetter { maybePositionCursor(); } + bool isSearchingHistory() { + return supplementalGetter !is null; + } + + /++ + Cancels an in-progress history search immediately, discarding the result, returning + to the normal prompt. + + If the user is not currently searching history (see [isSearchingHistory]), this + function does nothing. + +/ + void cancelHistorySearch() { + if(isSearchingHistory()) { + lastDrawLength = maximumDrawWidth - 1; + supplementalGetter = null; + redraw(); + } + } + /++ for integrating into another event loop you can pass individual events to this and @@ -7468,6 +7487,16 @@ struct ScrollbackBuffer { scrollbackPosition_++; } + /++ + Adds a line by components without affecting scrollback. + + History: + Added May 17, 2022 + +/ + void addLine(LineComponent[] components...) { + lines ~= Line(components.dup); + } + /++ Scrolling controls. diff --git a/webview.d b/webview.d index 8ee11f3..36602f2 100644 --- a/webview.d +++ b/webview.d @@ -109,6 +109,10 @@ struct RC(T) { object = null; } + bool opCast(T:bool)() nothrow { + return inner !is null; + } + void opAssign(T obj) { obj.AddRef(); if(object)