diff --git a/README.md b/README.md index ac2c342..2ffc46f 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,23 @@ I have [a patreon](https://www.patreon.com/adam_d_ruppe) and my (almost) [weekly Thanks go to Nick Sabalausky, Trass3r, Stanislav Blinov, ketmar, maartenvd, and many others over the years for input and patches. Several of the modules are also ports of other C code, see the comments in those files for their original authors. + +# Conventions + +Many http-based functions in the lib also support unix sockets as an alternative to tcp. + +With cgi.d, use + + --host unix:/path/here + +or, on Linux: + + --host abstract:/path/here + +after compiling with `-version=embedded_httpd_thread` to serve http on the given socket. (`abstract:` does a unix socket in the Linux-specific abstract namespace). + +With http2.d, use + + Uri("http://whatever_host/path?args").viaUnixSocket("/path/here") + +any time you are constructing a client. Note that `navigateTo` may lose the unix socket unless you specify it again. diff --git a/cgi.d b/cgi.d index dcc1dc9..cf97eee 100644 --- a/cgi.d +++ b/cgi.d @@ -1590,7 +1590,11 @@ class Cgi { sendAll(ir.source, d); } - this(ir, ir.source.remoteAddress().toString(), 80 /* FIXME */, 0, false, &rdo, null, closeConnection); + auto ira = ir.source.remoteAddress(); + + // that check for UnixAddress is to work around a Phobos bug + // see: https://github.com/dlang/phobos/pull/7383 + this(ir, cast(UnixAddress) ira ? "unix:" : ira.toString(), 80 /* FIXME */, 0, false, &rdo, null, closeConnection); } /** @@ -4219,6 +4223,7 @@ class ListeningConnectionManager { version(linux) { listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); string filename = "\0" ~ host["abstract:".length .. $]; + import std.stdio; stderr.writeln("Listening to abstract unix domain socket: ", host["abstract:".length .. $]); listener.bind(new UnixAddress(filename)); tcp = false; } else { diff --git a/dub.json b/dub.json index a6992b9..3d8ac3a 100644 --- a/dub.json +++ b/dub.json @@ -116,7 +116,17 @@ }, "sourceFiles": ["png.d"] }, - + { + "name": "bmp", + "description": "BMP file format support", + "importPaths": ["."], + "targetType": "library", + "dflags": ["-mv=arsd.bmp=bmp.d"], + "dependencies": { + "arsd-official:color_base":"*" + }, + "sourceFiles": ["bmp.d"] + }, { "name": "htmltotext", "description": "HTML to plain text conversion", @@ -300,6 +310,25 @@ "sourceFiles": ["database.d"], "importPaths": ["."], "dflags": ["-mv=arsd.database=database.d"] + }, + { + "name": "libssh2_bindings", + "description": "My bindings to libssh2", + "targetType": "library", + "sourceFiles": ["libssh2.d"], + "importPaths": ["."], + "libs": ["ssh2"], + "dflags": ["-mv=arsd.libssh2=libssh2.d"] + }, + { + "name": "eventloop", + "description": "My DEPRECATED event loop. Do NOT use in new programs, only offered here because I use it in a lot of old programs.", + "targetType": "sourceLibrary", + "versions": ["with_eventloop"], + "sourceFiles": ["eventloop.d"], + "importPaths": ["."], + "dflags": ["-mv=arsd.eventloop=eventloop.d"] } + ] } diff --git a/http2.d b/http2.d index 4bd2ada..c1d95d8 100644 --- a/http2.d +++ b/http2.d @@ -36,6 +36,11 @@ version(arsd_http_winhttp_implementation) { pragma(lib, "winhttp") import core.sys.windows.winhttp; // FIXME: alter the dub package file too + + // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpreaddata + // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpsendrequest + // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpopenrequest + // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpconnect } @@ -318,8 +323,25 @@ import std.conv; import std.range; +private AddressFamily family(string unixSocketPath) { + if(unixSocketPath.length) + return AddressFamily.UNIX; + else // FIXME: what about ipv6? + return AddressFamily.INET; +} -// Copy pasta from cgi.d, then stripped down +version(Windows) +private class UnixAddress : Address { + this(string) { + throw new Exception("No unix address support on this system in lib yet :("); + } + override sockaddr* name() { assert(0); } + override const(sockaddr)* name() const { assert(0); } + override int nameLen() const { assert(0); } +} + + +// Copy pasta from cgi.d, then stripped down. unix path thing added tho /// struct Uri { alias toString this; // blargh idk a url really is a string, but should it be implicit? @@ -339,6 +361,22 @@ struct Uri { reparse(uri); } + private string unixSocketPath = null; + /// Indicates it should be accessed through a unix socket instead of regular tcp. Returns new version without modifying this object. + Uri viaUnixSocket(string path) const { + Uri copy = this; + copy.unixSocketPath = path; + return copy; + } + + /// Goes through a unix socket in the abstract namespace (linux only). Returns new version without modifying this object. + version(linux) + Uri viaAbstractSocket(string path) const { + Uri copy = this; + copy.unixSocketPath = "\0" ~ path; + return copy; + } + private void reparse(string uri) { // from RFC 3986 // the ctRegex triples the compile time and makes ugly errors for no real benefit @@ -545,6 +583,11 @@ struct Uri { n.removeDots(); + // if still basically talking to the same thing, we should inherit the unix path + // too since basically the unix path is saying for this service, always use this override. + if(n.host == baseUrl.host && n.scheme == baseUrl.scheme && n.port == baseUrl.port) + n.unixSocketPath = baseUrl.unixSocketPath; + return n; } @@ -630,6 +673,7 @@ class HttpRequest { auto parts = where; finalUrl = where.toString(); requestParameters.method = method; + requestParameters.unixSocketPath = where.unixSocketPath; requestParameters.host = parts.host; requestParameters.port = cast(ushort) parts.port; requestParameters.ssl = parts.scheme == "https"; @@ -812,18 +856,26 @@ class HttpRequest { } } - Socket getOpenSocketOnHost(string host, ushort port, bool ssl) { + Socket getOpenSocketOnHost(string host, ushort port, bool ssl, string unixSocketPath) { + Socket openNewConnection() { Socket socket; if(ssl) { version(with_openssl) - socket = new SslClientSocket(AddressFamily.INET, SocketType.STREAM); + socket = new SslClientSocket(family(unixSocketPath), SocketType.STREAM); else throw new Exception("SSL not compiled in"); } else - socket = new Socket(AddressFamily.INET, SocketType.STREAM); + socket = new Socket(family(unixSocketPath), SocketType.STREAM); + + if(unixSocketPath) { + import std.stdio; writeln(cast(ubyte[]) unixSocketPath); + socket.connect(new UnixAddress(unixSocketPath)); + } else { + // FIXME: i should prolly do ipv6 if available too. + socket.connect(new InternetAddress(host, port)); + } - socket.connect(new InternetAddress(host, port)); debug(arsd_http2) writeln("opening to ", host, ":", port, " ", cast(void*) socket); assert(socket.handle() !is socket_t.init); return socket; @@ -903,7 +955,7 @@ class HttpRequest { continue; } - auto socket = getOpenSocketOnHost(pc.requestParameters.host, pc.requestParameters.port, pc.requestParameters.ssl); + auto socket = getOpenSocketOnHost(pc.requestParameters.host, pc.requestParameters.port, pc.requestParameters.ssl, pc.requestParameters.unixSocketPath); if(socket !is null) { activeRequestOnSocket[socket] = pc; @@ -1384,6 +1436,8 @@ struct HttpRequestParameters { string contentType; /// ubyte[] bodyData; /// + + string unixSocketPath; } interface IHttpClient { @@ -2209,11 +2263,11 @@ wss://echo.websocket.org if(ssl) { version(with_openssl) - socket = new SslClientSocket(AddressFamily.INET, SocketType.STREAM); + socket = new SslClientSocket(family(uri.unixSocketPath), SocketType.STREAM); else throw new Exception("SSL not compiled in"); } else - socket = new Socket(AddressFamily.INET, SocketType.STREAM); + socket = new Socket(family(uri.unixSocketPath), SocketType.STREAM); } @@ -2222,7 +2276,10 @@ wss://echo.websocket.org +/ /// Group: foundational void connect() { - socket.connect(new InternetAddress(host, port)); // FIXME: ipv6 support... + if(uri.unixSocketPath) + socket.connect(new UnixAddress(uri.unixSocketPath)); + else + socket.connect(new InternetAddress(host, port)); // FIXME: ipv6 support... // FIXME: websocket handshake could and really should be async too. auto uri = this.uri.path.length ? this.uri.path : "/"; @@ -2924,8 +2981,8 @@ private { return needsMoreData(); } - msg.data = d[0 .. msg.realLength]; - d = d[msg.realLength .. $]; + msg.data = d[0 .. cast(size_t) msg.realLength]; + d = d[cast(size_t) msg.realLength .. $]; if(msg.masked) { // let's just unmask it now diff --git a/simpledisplay.d b/simpledisplay.d index e4f711f..33a94fb 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -1991,7 +1991,7 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon { XChangeProperty( display, impl.window, - GetAtom!"_NET_WM_ICON"(display), + GetAtom!("_NET_WM_ICON", true)(display), GetAtom!"CARDINAL"(display), 32 /* bits */, 0 /*PropModeReplace*/, @@ -4217,7 +4217,7 @@ class Timer { } version(Windows) -/// Lets you add HANDLEs to the event loop. Not meant to be used for async I/O per se, but for other handles (it can only handle a few handles at a time.) +/// Lets you add HANDLEs to the event loop. Not meant to be used for async I/O per se, but for other handles (it can only handle a few handles at a time.) Only works on certain types of handles! see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjectsex class WindowsHandleReader { /// this(void delegate() onReady, HANDLE handle) { @@ -6618,10 +6618,9 @@ struct ScreenPainter { impl.drawArc(upperLeft.x, upperLeft.y, width, height, start, finish); } - //this function draws a circle using the drawArc() function above, it requires you to pass the point it - //will be drawn at as a Point struct and the radius as an int + //this function draws a circle with the drawEllipse() function above, it requires the upper left point and the radius void drawCircle(Point upperLeft, int radius) { - this.drawArc(upperLeft, radius, radius, 0, 0); + drawEllipse(upperLeft, Point(upperLeft.x + radius, upperLeft.y + radius)); } /// . @@ -9382,7 +9381,15 @@ version(X11) { if(!xshmQueryCompleted) { int i1, i2, i3; xshmQueryCompleted = true; - _xshmAvailable = XQueryExtension(XDisplayConnection.get(), "MIT-SHM", &i1, &i2, &i3) != 0; + + auto str = XDisplayConnection.get().display_name; + // only if we are actually on the same machine does this + // have any hope, and the query extension only asks if + // the server can in theory, not in practice. + if(str is null || str[0] != ':') + _xshmAvailable = false; + else + _xshmAvailable = XQueryExtension(XDisplayConnection.get(), "MIT-SHM", &i1, &i2, &i3) != 0; } return _xshmAvailable; } @@ -14309,51 +14316,55 @@ void demandAttention(Window window, bool needs = true) { /// X-specific TrueColorImage getWindowNetWmIcon(Window window) { - auto display = XDisplayConnection.get; + try { + auto display = XDisplayConnection.get; - auto data = getX11PropertyData (window, GetAtom!"_NET_WM_ICON"(display), XA_CARDINAL); + auto data = getX11PropertyData (window, GetAtom!"_NET_WM_ICON"(display), XA_CARDINAL); - if (data.length > arch_ulong.sizeof * 2) { - auto meta = cast(arch_ulong[]) (data[0 .. arch_ulong.sizeof * 2]); - // these are an array of rgba images that we have to convert into pixmaps ourself + if (data.length > arch_ulong.sizeof * 2) { + auto meta = cast(arch_ulong[]) (data[0 .. arch_ulong.sizeof * 2]); + // these are an array of rgba images that we have to convert into pixmaps ourself - int width = cast(int) meta[0]; - int height = cast(int) meta[1]; + int width = cast(int) meta[0]; + int height = cast(int) meta[1]; - auto bytes = cast(ubyte[]) (data[arch_ulong.sizeof * 2 .. $]); + auto bytes = cast(ubyte[]) (data[arch_ulong.sizeof * 2 .. $]); - static if(arch_ulong.sizeof == 4) { - bytes = bytes[0 .. width * height * 4]; - alias imageData = bytes; - } else static if(arch_ulong.sizeof == 8) { - bytes = bytes[0 .. width * height * 8]; - auto imageData = new ubyte[](4 * width * height); - } else static assert(0); + static if(arch_ulong.sizeof == 4) { + bytes = bytes[0 .. width * height * 4]; + alias imageData = bytes; + } else static if(arch_ulong.sizeof == 8) { + bytes = bytes[0 .. width * height * 8]; + auto imageData = new ubyte[](4 * width * height); + } else static assert(0); - // this returns ARGB. Remember it is little-endian so - // we have BGRA - // our thing uses RGBA, which in little endian, is ABGR - for(int idx = 0, idx2 = 0; idx < bytes.length; idx += arch_ulong.sizeof, idx2 += 4) { - auto r = bytes[idx + 2]; - auto g = bytes[idx + 1]; - auto b = bytes[idx + 0]; - auto a = bytes[idx + 3]; + // this returns ARGB. Remember it is little-endian so + // we have BGRA + // our thing uses RGBA, which in little endian, is ABGR + for(int idx = 0, idx2 = 0; idx < bytes.length; idx += arch_ulong.sizeof, idx2 += 4) { + auto r = bytes[idx + 2]; + auto g = bytes[idx + 1]; + auto b = bytes[idx + 0]; + auto a = bytes[idx + 3]; - imageData[idx2 + 0] = r; - imageData[idx2 + 1] = g; - imageData[idx2 + 2] = b; - imageData[idx2 + 3] = a; + imageData[idx2 + 0] = r; + imageData[idx2 + 1] = g; + imageData[idx2 + 2] = b; + imageData[idx2 + 3] = a; + } + + return new TrueColorImage(width, height, imageData); } - return new TrueColorImage(width, height, imageData); + return null; + } catch(Exception e) { + return null; } - - return null; } -} +} /* UsingSimpledisplayX11 */ void loadBinNameToWindowClassName () { diff --git a/terminal.d b/terminal.d index 9c31a6d..a8bbe1b 100644 --- a/terminal.d +++ b/terminal.d @@ -63,7 +63,7 @@ module arsd.terminal; // FIXME: needs to support VT output on Windows too in certain situations -// FIXME: paste on Windows and alt+NNNN codes in getline function +// detect VT on windows by trying to set the flag. if this succeeds, ask it for caps. if this replies with my code we good to do extended output. /++ $(H3 Get Line) @@ -190,7 +190,6 @@ version(Windows) version(Win32Console) { import core.sys.windows.windows; - import std.string : toStringz; private { enum RED_BIT = 4; enum GREEN_BIT = 2; @@ -294,7 +293,7 @@ vt|vt100|DEC vt100 compatible:\ # Entry for an xterm. Insert mode has been disabled. -vs|xterm|screen|screen.xterm|screen.xterm-256color|xterm-color|xterm-256color|vs100|xterm terminal emulator (X Window System):\ +vs|xterm|tmux|tmux-256color|screen|screen.xterm|screen.xterm-256color|xterm-color|xterm-256color|vs100|xterm terminal emulator (X Window System):\ :am:bs:mi@:km:co#80:li#55:\ :im@:ei@:\ :cl=\E[H\E[J:\ @@ -560,6 +559,8 @@ struct Terminal { // work the way they're advertised. I just have to best-guess hack and hope it // doesn't break anything else. (If you know a better way, let me know!) bool isMacTerminal() { + // it gives 1,2 in getTerminalCapabilities... + // FIXME import std.process; import std.string; auto term = environment.get("TERM"); @@ -853,13 +854,16 @@ struct Terminal { return; } + auto info = getTerminalCapabilities(fdIn, fdOut); + //writeln(info); + if(type == ConsoleOutputType.cellular) { doTermcap("ti"); clear(); moveTo(0, 0, ForceOption.alwaysSend); // we need to know where the cursor is for some features to work, and moving it is easier than querying it } - if(terminalInFamily("xterm", "rxvt", "screen")) { + if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it) } } @@ -941,7 +945,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as if(type == ConsoleOutputType.cellular) { doTermcap("te"); } - if(terminalInFamily("xterm", "rxvt", "screen")) { + if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) { writeStringRaw("\033[23;0t"); // restore window title from the stack } cursor = TerminalCursor.DEFAULT; @@ -1267,11 +1271,19 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as /// Changes the terminal's title void setTitle(string t) { version(Win32Console) { - // FIXME: use the W version - SetConsoleTitleA(toStringz(t)); + wchar[256] buffer; + size_t bufferLength; + foreach(wchar ch; t) + if(bufferLength < buffer.length) + buffer[bufferLength++] = ch; + if(bufferLength < buffer.length) + buffer[bufferLength++] = 0; + else + buffer[$-1] = 0; + SetConsoleTitleW(buffer.ptr); } else { import std.string; - if(terminalInFamily("xterm", "rxvt", "screen")) + if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) writeStringRaw(format("\033]0;%s\007", t)); } } @@ -1619,6 +1631,8 @@ struct RealTimeConsoleInput { if(!(flags & ConsoleInputFlags.echo)) f |= ECHO; + // \033Z or \033[c + n.c_lflag &= ~f; tcsetattr(fdIn, TCSANOW, &n); } @@ -1672,20 +1686,20 @@ struct RealTimeConsoleInput { // this is vt200 mouse with full motion tracking, supported by xterm terminal.writeStringRaw("\033[?1003h"); destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; - } else if(terminal.terminalInFamily("rxvt", "screen") || environment.get("MOUSE_HACK") == "1002") { + } else if(terminal.terminalInFamily("rxvt", "screen", "tmux") || environment.get("MOUSE_HACK") == "1002") { terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; } } if(flags & ConsoleInputFlags.paste) { - if(terminal.terminalInFamily("xterm", "rxvt", "screen")) { + if(terminal.terminalInFamily("xterm", "rxvt", "screen", "tmux")) { terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; } } // try to ensure the terminal is in UTF-8 mode - if(terminal.terminalInFamily("xterm", "screen", "linux") && !terminal.isMacTerminal()) { + if(terminal.terminalInFamily("xterm", "screen", "linux", "tmux") && !terminal.isMacTerminal()) { terminal.writeStringRaw("\033%G"); } @@ -2416,7 +2430,7 @@ struct RealTimeConsoleInput { default: // screen doesn't actually do the modifiers, but // it uses the same format so this branch still works fine. - if(terminal.terminalInFamily("xterm", "screen")) { + if(terminal.terminalInFamily("xterm", "screen", "tmux")) { import std.conv, std.string; auto terminator = sequence[$ - 1]; auto parts = sequence[2 .. $ - 1].split(";"); @@ -2490,6 +2504,7 @@ struct RealTimeConsoleInput { // starting at 70 i do some magic for like shift+enter etc. // this only happens on my own terminal emulator. + case "70": return keyPressAndRelease(NonCharacterKeyEvent.Key.ScrollLock, modifierState); case "78": return keyPressAndRelease2('\b', modifierState); case "79": return keyPressAndRelease2('\t', modifierState); case "83": return keyPressAndRelease2('\n', modifierState); @@ -2610,6 +2625,7 @@ struct KeyboardEvent { End = 0x23 + 0xF0000, /// . PageUp = 0x21 + 0xF0000, /// . PageDown = 0x22 + 0xF0000, /// . + ScrollLock = 0x91 + 0xF0000, /// unlikely to work outside my custom terminal emulator } @@ -2665,6 +2681,7 @@ struct NonCharacterKeyEvent { End = 0x23, /// . PageUp = 0x21, /// . PageDown = 0x22, /// . + ScrollLock = 0x91, /// unlikely to work outside my terminal emulator } Key key; /// . @@ -3011,6 +3028,127 @@ void main() { } } +version(Posix) +private int[] getTerminalCapabilities(int fdIn, int fdOut) { + if(fdIn == -1 || fdOut == -1) + return null; + + import std.conv; + import core.stdc.errno; + import core.sys.posix.unistd; + + ubyte[128] hack2; + termios old; + ubyte[128] hack; + tcgetattr(fdIn, &old); + auto n = old; + n.c_lflag &= ~(ICANON | ECHO); + tcsetattr(fdIn, TCSANOW, &n); + scope(exit) + tcsetattr(fdIn, TCSANOW, &old); + + // drain the buffer? meh + + string cmd = "\033[c"; + auto err = write(fdOut, cmd.ptr, cmd.length); + if(err != cmd.length) { + throw new Exception("couldn't ask terminal for ID"); + } + + // reading directly to bypass any buffering + int retries = 16; + int len; + ubyte[96] buffer; + try_again: + + + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 10 * 1000; + + fd_set fs; + FD_ZERO(&fs); + + FD_SET(fdIn, &fs); + if(select(fdIn + 1, &fs, null, null, &tv) == -1) { + goto try_again; + } + + if(FD_ISSET(fdIn, &fs)) { + auto len2 = read(fdIn, &buffer[len], buffer.length - len); + if(len2 <= 0) { + retries--; + if(retries > 0) + goto try_again; + throw new Exception("can't get terminal id"); + } else { + len += len2; + } + } else { + // no data... assume terminal doesn't support giving an answer + return null; + } + + ubyte[] answer; + bool hasAnswer(ubyte[] data) { + if(data.length < 4) + return false; + answer = null; + size_t start; + int position = 0; + foreach(idx, ch; data) { + switch(position) { + case 0: + if(ch == '\033') { + start = idx; + position++; + } + break; + case 1: + if(ch == '[') + position++; + else + position = 0; + break; + case 2: + if(ch == '?') + position++; + else + position = 0; + break; + case 3: + // body + if(ch == 'c') { + answer = data[start .. idx + 1]; + return true; + } else if(ch == ';' || (ch >= '0' && ch <= '9')) { + // good, keep going + } else { + // invalid, drop it + position = 0; + } + break; + default: assert(0); + } + } + return false; + } + + auto got = buffer[0 .. len]; + if(!hasAnswer(got)) { + goto try_again; + } + auto gots = cast(char[]) answer[3 .. $-1]; + + import std.string; + + auto pieces = split(gots, ";"); + int[] ret; + foreach(p; pieces) + ret ~= p.to!int; + return ret; +} + /** FIXME: support lines that wrap FIXME: better controls maybe