diff options
Diffstat (limited to 'src/ext_depends/arsd/cgi.d')
-rw-r--r-- | src/ext_depends/arsd/cgi.d | 451 |
1 files changed, 362 insertions, 89 deletions
diff --git a/src/ext_depends/arsd/cgi.d b/src/ext_depends/arsd/cgi.d index 844a411..0497eb2 100644 --- a/src/ext_depends/arsd/cgi.d +++ b/src/ext_depends/arsd/cgi.d @@ -131,7 +131,7 @@ void main() { to change versions. The possible options for `VALUE_HERE` are: $(LIST - * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default for dub builds. You can run the program then connect directly to it from your browser. + * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default for dub builds. You can run the program then connect directly to it from your browser. Note: prior to version 11, this would be embedded_httpd_processes on Linux and embedded_httpd_threads everywhere else. It now means embedded_httpd_hybrid everywhere supported and embedded_httpd_threads everywhere else. * `cgi` for traditional cgi binaries. These are run by an outside web server as-needed to handle requests. * `fastcgi` for FastCGI builds. FastCGI is managed from an outside helper, there's one built into Microsoft IIS, Apache httpd, and Lighttpd, and a generic program you can use with nginx called `spawn-fcgi`. If you don't already know how to use it, I suggest you use one of the other modes. * `scgi` for SCGI builds. SCGI is a simplified form of FastCGI, where you run the server as an application service which is proxied by your outside webserver. @@ -617,29 +617,12 @@ void cloexec(Socket s) { } } -version(embedded_httpd_hybrid) { - version=embedded_httpd_threads; - version(cgi_no_fork) {} else version(Posix) - version=cgi_use_fork; - version=cgi_use_fiber; -} - -version(cgi_use_fork) - enum cgi_use_fork_default = true; -else - enum cgi_use_fork_default = false; - // the servers must know about the connections to talk to them; the interfaces are vital version(with_addon_servers) version=with_addon_servers_connections; version(embedded_httpd) { - version(linux) - version=embedded_httpd_processes; - else { - version=embedded_httpd_threads; - } - + version=embedded_httpd_hybrid; /* version(with_openssl) { pragma(lib, "crypto"); @@ -648,6 +631,18 @@ version(embedded_httpd) { */ } +version(embedded_httpd_hybrid) { + version=embedded_httpd_threads; + version(cgi_no_fork) {} else version(Posix) + version=cgi_use_fork; + version=cgi_use_fiber; +} + +version(cgi_use_fork) + enum cgi_use_fork_default = true; +else + enum cgi_use_fork_default = false; + version(embedded_httpd_processes) version=embedded_httpd_processes_accept_after_fork; // I am getting much better average performance on this, so just keeping it. But the other way MIGHT help keep the variation down so i wanna keep the code to play with later @@ -1435,7 +1430,7 @@ class Cgi { string contentFilename; /// the file where we dumped the content, if contentInMemory == false. Note that if you want to keep it, you MUST move the file, since otherwise it is considered garbage when cgi is disposed. /// - ulong fileSize() { + ulong fileSize() const { if(contentInMemory) return content.length; import std.file; @@ -1985,6 +1980,12 @@ class Cgi { if(headerNumber == 1) { // request line auto parts = al.splitter(header, " "); + if(parts.front == "PRI") { + // this is an HTTP/2.0 line - "PRI * HTTP/2.0" - which indicates their payload will follow + // we're going to immediately refuse this, im not interested in implementing http2 (it is unlikely + // to bring me benefit) + throw new HttpVersionNotSupportedException(); + } requestMethod = to!RequestMethod(parts.front); parts.popFront(); requestUri = parts.front; @@ -3639,8 +3640,8 @@ string plainHttpError(bool isCgi, string type, Throwable t) { auto message = messageFromException(t); message = simpleHtmlEncode(message); - return format("%s %s\r\nContent-Length: %s\r\n\r\n%s", - isCgi ? "Status:" : "HTTP/1.0", + return format("%s %s\r\nContent-Length: %s\r\nConnection: close\r\n\r\n%s", + isCgi ? "Status:" : "HTTP/1.1", type, message.length, message); } @@ -3753,17 +3754,101 @@ bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(Custom } /++ - A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] to an example of how you can use it. + A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] for a complete, up-to-date, example of how it is used internally. + + As of version 11 (released August 2023), you can also make things like this: + + --- + // listens on both a unix domain socket called `foo` and on the loopback interfaces port 8080 + RequestServer server = RequestServer(["http://unix:foo", "http://localhost:8080"]); + + // can also: + // RequestServer server = RequestServer(0); // listen on an OS-provided port on all interfaces + + // NOT IMPLEMENTED YET + // server.initialize(); // explicit initialization will populate any "any port" things and throw if a bind failed + + foreach(listenSpec; server.listenSpecs) { + // you can check what it actually bound to here and see your assigned ports + } + + // NOT IMPLEMENTED YET + // server.start!handler(); // starts and runs in the arsd.core event loop + + server.serve!handler(); // blocks the thread until the server exits + --- History: Added Sept 26, 2020 (release version 8.5). + + The `listenSpec` member was added July 31, 2023. +/ struct RequestServer { - /// + /++ + Sets the host and port the server will listen on. This is semi-deprecated; the new (as of July 31, 2023) [listenSpec] parameter obsoletes these. You cannot use both together; the listeningHost and listeningPort are ONLY used if listenSpec is null. + +/ string listeningHost = defaultListeningHost(); - /// + /// ditto ushort listeningPort = defaultListeningPort(); + static struct ListenSpec { + enum Protocol { + http, + https, + scgi + } + Protocol protocol; + + enum AddressType { + ip, + unix, + abstract_ + } + AddressType addressType; + + string address; + ushort port; + } + + /++ + The array of addresses you want to listen on. The format looks like a url but has a few differences. + + This ONLY works on embedded_httpd_threads, embedded_httpd_hybrid, and scgi builds at this time. + + `http://localhost:8080` + + `http://unix:filename/here` + + `scgi://abstract:/name/here` + + `http://[::1]:4444` + + Note that IPv6 addresses must be enclosed in brackets. If you want to listen on an interface called `unix` or `abstract`, contact me, that is not supported but I could add some kind of escape mechanism. + + If you leave off the protocol, it assumes the default based on compile flags. If you only give a number, it is assumed to be a port on any tcp interface. + + `localhost:8080` serves the default protocol. + + `8080` or `:8080` assumes default protocol on localhost. + + The protocols can be `http:`, `https:`, and `scgi:`. Original `cgi` is not supported with this, since it is transactional with a single process. + + Valid hosts are an IPv4 address (with a mandatory port), an IPv6 address (with a mandatory port), just a port alone, `unix:/path/to/unix/socket` (which may be a relative path without a leading slash), or `abstract:/path/to/linux/abstract/namespace`. + + `http://unix:foo` will serve http over the unix domain socket named `foo` in the current working directory. + + $(PITFALL + If you set this to anything non-null (including a non-null, zero-length array) any `listenSpec` entries, [listeningHost] and [listeningPort] are ignored. + ) + + Bugs: + The implementation currently ignores the protocol spec in favor of the default compiled in option. + + History: + Added July 31, 2023 (dub v11.0) + +/ + string[] listenSpec; + /++ Uses a fork() call, if available, to provide additional crash resiliency and possibly improved performance. On the other hand, if you fork, you must not assume any memory is shared between requests (you shouldn't be anyway though! But @@ -3786,13 +3871,23 @@ struct RequestServer { +/ int numberOfThreads = 0; - /// + /++ + Creates a server configured to listen to multiple URLs. + + History: + Added July 31, 2023 (dub v11.0) + +/ + this(string[] listenTo) { + this.listenSpec = listenTo; + } + + /// Creates a server object configured to listen on a single host and port. this(string defaultHost, ushort defaultPort) { this.listeningHost = defaultHost; this.listeningPort = defaultPort; } - /// + /// ditto this(ushort defaultPort) { listeningPort = defaultPort; } @@ -3800,29 +3895,45 @@ struct RequestServer { /++ Reads the command line arguments into the values here. - Possible arguments are `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. + Possible arguments are `--listen` (can appear multiple times), `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. + + Please note you cannot combine `--listen` with `--listening-host` or `--listening-port` / `--port`. Use one or the other style. +/ void configureFromCommandLine(string[] args) { + bool portOrHostFound = false; + bool foundPort = false; bool foundHost = false; bool foundUid = false; bool foundGid = false; + bool foundListen = false; foreach(arg; args) { if(foundPort) { listeningPort = to!ushort(arg); + portOrHostFound = true; foundPort = false; + continue; } if(foundHost) { listeningHost = arg; + portOrHostFound = true; foundHost = false; + continue; } if(foundUid) { privilegesDropToUid = to!uid_t(arg); foundUid = false; + continue; } if(foundGid) { privilegesDropToGid = to!gid_t(arg); foundGid = false; + continue; + } + if(foundListen) { + this.listenSpec ~= arg; + foundListen = false; + continue; } if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host") foundHost = true; @@ -3832,6 +3943,12 @@ struct RequestServer { foundUid = true; else if(arg == "--gid") foundGid = true; + else if(arg == "--listen") + foundListen = true; + } + + if(portOrHostFound && listenSpec.length) { + throw new Exception("You passed both a --listening-host or --listening-port and a --listen argument. You should fix your script to ONLY use --listen arguments."); } } @@ -3950,7 +4067,9 @@ struct RequestServer { __traits(child, _this, fun)(cgi); else static assert(0, "Not implemented in your compiler version!"); } - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads); + auto manager = this.listenSpec is null ? + new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads) : + new ListeningConnectionManager(this.listenSpec, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads); manager.listen(); } @@ -3959,7 +4078,9 @@ struct RequestServer { +/ void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { globalStopFlag = false; - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads); + auto manager = this.listenSpec is null ? + new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads) : + new ListeningConnectionManager(this.listenSpec, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads); manager.listen(); } @@ -4167,6 +4288,10 @@ void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer param if(processPoolSize <= 1) closeConnection = true; //cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection); + } catch(HttpVersionNotSupportedException he) { + sendAll(ir.source, plainHttpError(false, "505 HTTP Version Not Supported", he)); + closeConnection = true; + break; } catch(Throwable t) { // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P // anyway let's kill the connection @@ -4914,6 +5039,10 @@ void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection // broken pipe or something, just abort the connection closeConnection = true; break; + } catch(HttpVersionNotSupportedException ve) { + sendAll(connection, plainHttpError(false, "505 HTTP Version Not Supported", ve)); + closeConnection = true; + break; } catch(Throwable t) { // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P // anyway let's kill the connection @@ -5530,10 +5659,17 @@ class ListeningConnectionManager { import core.sys.posix.sys.select; fd_set read_fds; FD_ZERO(&read_fds); - FD_SET(listener.handle, &read_fds); - if(cancelfd != -1) + int max = 0; + foreach(listener; listeners) { + FD_SET(listener.handle, &read_fds); + if(listener.handle > max) + max = listener.handle; + } + if(cancelfd != -1) { FD_SET(cancelfd, &read_fds); - auto max = listener.handle > cancelfd ? listener.handle : cancelfd; + if(cancelfd > max) + max = cancelfd; + } auto ret = select(max + 1, &read_fds, null, null, null); if(ret == -1) { import core.stdc.errno; @@ -5547,24 +5683,27 @@ class ListeningConnectionManager { return null; } - if(FD_ISSET(listener.handle, &read_fds)) - return listener.accept(); + foreach(listener; listeners) { + if(FD_ISSET(listener.handle, &read_fds)) + return listener.accept(); + } return null; } else { - Socket socket = listener; - auto check = new SocketSet(); keep_looping: check.reset(); - check.add(socket); + foreach(listener; listeners) + check.add(listener); // just to check the stop flag on a kinda busy loop. i hate this FIXME auto got = Socket.select(check, null, null, 3.seconds); if(got > 0) - return listener.accept(); + foreach(listener; listeners) + if(check.isSet(listener)) + return listener.accept(); if(globalStopFlag) return null; else @@ -5575,7 +5714,7 @@ class ListeningConnectionManager { int defaultNumberOfThreads() { import std.parallelism; version(cgi_use_fiber) { - return totalCPUs * 1 + 1; + return totalCPUs * 2 + 1; // still chance some will be pointlessly blocked anyway } else { // I times 4 here because there's a good chance some will be blocked on i/o. return totalCPUs * 4; @@ -5625,7 +5764,9 @@ class ListeningConnectionManager { version(cgi_use_fiber) { version(Windows) { - listener.accept(); + // please note these are overlapped sockets! so the accept just kicks things off + foreach(listener; listeners) + listener.accept(); } WorkerThread[] threads = new WorkerThread[](numberOfThreads); @@ -5692,12 +5833,12 @@ class ListeningConnectionManager { void existing_connection_new_data() { // wait until a slot opens up - //int waited = 0; + // int waited = 0; while(queueLength >= queue.length) { Thread.sleep(1.msecs); - //waited ++; + // waited ++; } - //if(waited) {import std.stdio; writeln(waited);} + // if(waited) {import std.stdio; writeln(waited);} synchronized(this) { queue[nextIndexBack] = sn; nextIndexBack++; @@ -5739,27 +5880,105 @@ class ListeningConnectionManager { private void dg_handler(Socket s) { fhandler(s); } + + + this(string[] listenSpec, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { + fhandler = handler; + this(listenSpec, &dg_handler, dropPrivs, useFork, numberOfThreads); + } + this(string[] listenSpec, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { + string[] host; + ushort[] port; + + foreach(spec; listenSpec) { + /+ + The format: + + protocol:// + address_spec + + Protocol is optional. Must be http, https, scgi, or fastcgi. + + address_spec is either: + ipv4 address : port + [ipv6 address] : port + unix:filename + abstract:name + port <which is tcp but on any interface> + +/ + + string protocol; + string address_spec; + + auto protocolIdx = spec.indexOf("://"); + if(protocolIdx != -1) { + protocol = spec[0 .. protocolIdx]; + address_spec = spec[protocolIdx + "://".length .. $]; + } else { + address_spec = spec; + } + + if(address_spec.startsWith("unix:") || address_spec.startsWith("abstract:")) { + host ~= address_spec; + port ~= 0; + } else { + auto idx = address_spec.lastIndexOf(":"); + if(idx == -1) { + host ~= null; + } else { + auto as = address_spec[0 .. idx]; + if(as.length >= 3 && as[0] == '[' && as[$-1] == ']') + as = as[1 .. $-1]; + host ~= as; + } + port ~= address_spec[idx + 1 .. $].to!ushort; + } + + } + + this(host, port, handler, dropPrivs, useFork, numberOfThreads); + } + this(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { + this([host], [port], handler, dropPrivs, useFork, numberOfThreads); + } + this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { + this([host], [port], handler, dropPrivs, useFork, numberOfThreads); + } + + this(string[] host, ushort[] port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { fhandler = handler; this(host, port, &dg_handler, dropPrivs, useFork, numberOfThreads); } - this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { + this(string[] host, ushort[] port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { + assert(host.length == port.length); + this.handler = handler; this.useFork = useFork; this.numberOfThreads = numberOfThreads ? numberOfThreads : defaultNumberOfThreads(); - listener = startListening(host, port, tcp, cleanup, 128, dropPrivs); + listeners.reserve(host.length); + + foreach(i; 0 .. host.length) + if(host[i] == "localhost") { + listeners ~= startListening("127.0.0.1", port[i], tcp, cleanup, 128, dropPrivs); + listeners ~= startListening("::1", port[i], tcp, cleanup, 128, dropPrivs); + } else { + listeners ~= startListening(host[i], port[i], tcp, cleanup, 128, dropPrivs); + } version(cgi_use_fiber) - if(useFork) - listener.blocking = false; + if(useFork) { + foreach(listener; listeners) + listener.blocking = false; + } // this is the UI control thread and thus gets more priority Thread.getThis.priority = Thread.PRIORITY_MAX; } - Socket listener; + Socket[] listeners; void delegate(Socket) handler; immutable bool useFork; @@ -5795,17 +6014,20 @@ Socket startListening(string host, ushort port, ref bool tcp, ref void delegate( throw new Exception("abstract unix sockets not supported on this system"); } } else { + auto address = host.length ? parseAddress(host, port) : new InternetAddress(port); version(cgi_use_fiber) { version(Windows) listener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); else - listener = new TcpSocket(); + listener = new Socket(address.addressFamily, SocketType.STREAM); } else { - listener = new TcpSocket(); + listener = new Socket(address.addressFamily, SocketType.STREAM); } cloexec(listener); listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); - listener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port)); + if(address.addressFamily == AddressFamily.INET6) + listener.setOption(SocketOptionLevel.IPV6, SocketOption.IPV6_V6ONLY, true); + listener.bind(address); cleanup = delegate() { listener.close(); }; @@ -5851,6 +6073,12 @@ class ConnectionException : Exception { } } +class HttpVersionNotSupportedException : Exception { + this(string file = __FILE__, size_t line = __LINE__) { + super("HTTP Version Not Supported", file, line); + } +} + alias void delegate(Socket) CMT; import core.thread; @@ -6027,11 +6255,13 @@ class WorkerThread : Thread { epoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev); } - epoll_event ev; - ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. - ev.data.fd = lcm.listener.handle; - if(epoll_ctl(epfd, EPOLL_CTL_ADD, lcm.listener.handle, &ev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); + foreach(listener; lcm.listeners) { + epoll_event ev; + ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. + ev.data.fd = listener.handle; + if(epoll_ctl(epfd, EPOLL_CTL_ADD, listener.handle, &ev) == -1) + throw new Exception("epoll_ctl " ~ to!string(errno)); + } @@ -6046,37 +6276,44 @@ class WorkerThread : Thread { throw new Exception("epoll_wait " ~ to!string(errno)); } - foreach(idx; 0 .. nfds) { + outer: foreach(idx; 0 .. nfds) { auto flags = events[idx].events; if(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) { globalStopFlag = true; //import std.stdio; writeln("exit heard"); break; - } else if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) { - //import std.stdio; writeln(myThreadNumber, " woken up ", flags); - // this try/catch is because it is set to non-blocking mode - // and Phobos' stupid api throws an exception instead of returning - // if it would block. Why would it block? because a forked process - // might have beat us to it, but the wakeup event thundered our herds. - try - sn = lcm.listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better - catch(SocketAcceptException e) { continue; } - - cloexec(sn); - if(lcm.tcp) { - // disable Nagle's algorithm to avoid a 40ms delay when we send/recv - // on the socket because we do some buffering internally. I think this helps, - // certainly does for small requests, and I think it does for larger ones too - sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); - - sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); + } else { + foreach(listener; lcm.listeners) { + if(cast(size_t) events[idx].data.ptr == cast(size_t) listener.handle) { + //import std.stdio; writeln(myThreadNumber, " woken up ", flags); + // this try/catch is because it is set to non-blocking mode + // and Phobos' stupid api throws an exception instead of returning + // if it would block. Why would it block? because a forked process + // might have beat us to it, but the wakeup event thundered our herds. + try + sn = listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better + catch(SocketAcceptException e) { continue outer; } + + cloexec(sn); + if(lcm.tcp) { + // disable Nagle's algorithm to avoid a 40ms delay when we send/recv + // on the socket because we do some buffering internally. I think this helps, + // certainly does for small requests, and I think it does for larger ones too + sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); + + sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); + } + + dg(sn); + continue outer; + } else { + // writeln(events[idx].data.ptr); + } } - dg(sn); - } else { if(cast(size_t) events[idx].data.ptr < 1024) { - throw new Exception("this doesn't look like a fiber pointer..."); + throw arsd.core.ArsdException!"this doesn't look like a fiber pointer... "(cast(size_t) events[idx].data.ptr); } auto fiber = cast(CgiFiber) events[idx].data.ptr; fiber.proceed(); @@ -6307,7 +6544,7 @@ ByChunkRange byChunk(BufferedInputRange ir, size_t atMost) { version(cgi_with_websocket) { // http://tools.ietf.org/html/rfc6455 - /** + /++ WEBSOCKET SUPPORT: Full example: @@ -6335,12 +6572,14 @@ version(cgi_with_websocket) { } websocket.close(); - } else assert(0, "i want a web socket!"); + } else { + cgi.write("You are loading the websocket endpoint in a browser instead of a websocket client. Use a websocket client on this url instead.\n", true); + } } mixin GenericMain!websocketEcho; --- - */ + +/ class WebSocket { Cgi cgi; @@ -6703,6 +6942,11 @@ version(cgi_with_websocket) { } + /++ + Returns true if the request headers are asking for a websocket upgrade. + + If this returns true, and you want to accept it, call [acceptWebsocket]. + +/ bool websocketRequested(Cgi cgi) { return "sec-websocket-key" in cgi.requestHeaders @@ -6715,6 +6959,9 @@ version(cgi_with_websocket) { ; } + /++ + If [websocketRequested], you can call this to accept it and upgrade the connection. It returns the new [WebSocket] object you use for future communication on this connection; the `cgi` object should no longer be used. + +/ WebSocket acceptWebsocket(Cgi cgi) { assert(!cgi.closed); assert(!cgi.outputtedResponseData); @@ -8592,11 +8839,7 @@ void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoS void newConnection() { // on edge triggering, it is important that we get it all while(true) { - version(Android) { - auto size = cast(int) addr.sizeof; - } else { - auto size = cast(uint) addr.sizeof; - } + auto size = cast(socklen_t) addr.sizeof; auto ns = accept(sock, cast(sockaddr*) &addr, &size); if(ns == -1) { if(errno == EAGAIN || errno == EWOULDBLOCK) { @@ -9203,6 +9446,8 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) { case idents[idx]: static if(is(param == Cgi.UploadedFile)) { params[idx] = cgi.files[name]; + } else static if(is(param : const Cgi.UploadedFile[])) { + (cast() params[idx]) = cgi.filesArray[name]; } else { setVariable(name, paramName, ¶ms[idx], value); } @@ -9544,6 +9789,7 @@ css"; <script>document.documentElement.classList.remove("no-script");</script> <style>.no-script requires-script { display: none; }</style> <title>D Application</title> + <meta name="viewport" content="initial-scale=1, width=device-width" /> <link rel="stylesheet" href="style.css" /> </head> <body> @@ -9561,7 +9807,7 @@ html", true, true); return document.requireSelector("main"); } - /// Renders a response as an HTTP error + /// Renders a response as an HTTP error with associated html body void renderBasicError(Cgi cgi, int httpErrorCode) { cgi.setResponseStatus(getHttpCodeText(httpErrorCode)); auto c = htmlContainer(); @@ -9670,10 +9916,24 @@ html", true, true); case "html": presentExceptionAsHtml(cgi, t, meta); break; + case "json": + presentExceptionAsJsonImpl(cgi, t); + break; default: } } + private void presentExceptionAsJsonImpl()(Cgi cgi, Throwable t) { + cgi.setResponseStatus("500 Internal Server Error"); + cgi.setResponseContentType("application/json"); + import arsd.jsvar; + var v = var.emptyObject; + v.type = typeid(t).toString; + v.msg = t.msg; + v.fullString = t.toString(); + cgi.write(v.toJson(), true); + } + /++ If you override this, you will need to cast the exception type `t` dynamically, @@ -9796,7 +10056,20 @@ html", true, true); auto div = Element.make("div"); div.addClass("form-field"); - static if(is(T == Cgi.UploadedFile)) { + static if(is(T : const Cgi.UploadedFile)) { + Element lbl; + if(displayName !is null) { + lbl = div.addChild("label"); + lbl.addChild("span", displayName, "label-text"); + lbl.appendText(" "); + } else { + lbl = div; + } + auto i = lbl.addChild("input", name); + i.attrs.name = name; + i.attrs.type = "file"; + i.attrs.multiple = "multiple"; + } else static if(is(T == Cgi.UploadedFile)) { Element lbl; if(displayName !is null) { lbl = div.addChild("label"); |