diff --git a/src/arsd/cgi.d b/src/arsd/cgi.d index 56fe83ad..fb8ddabd 100644 --- a/src/arsd/cgi.d +++ b/src/arsd/cgi.d @@ -85,6 +85,8 @@ void main() { # now you can go to http://localhost:8080/?name=whatever ) + Please note: the default port for http is 8085 and for cgi is 4000. I recommend you set your own by the command line argument in a startup script instead of relying on any hard coded defaults. It is possible though to hard code your own with [RequestServer]. + Compile_versions: @@ -150,7 +152,7 @@ void main() { ) Compile_and_run: - + For CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory. For FastCGI: `dmd yourfile.d cgi.d -version=fastcgi` and run it. spawn-fcgi helps on nginx. You can put the file in the directory for Apache. On IIS, run it with a port on the command line (this causes it to call FCGX_OpenSocket, which can work on nginx too). @@ -328,7 +330,7 @@ void main() { web applications. For working with json, try [arsd.jsvar]. - + [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in accessing databases. @@ -336,7 +338,7 @@ void main() { Copyright: - cgi.d copyright 2008-2021, Adam D. Ruppe. Provided under the Boost Software License. + cgi.d copyright 2008-2022, Adam D. Ruppe. Provided under the Boost Software License. Yes, this file is old, and yes, it is still actively maintained and used. +/ @@ -370,6 +372,15 @@ version(Posix) { } } +version(Windows) { + version(minimal) { + + } else { + // not too concerned about gdc here since the mingw version is fairly new as well + version=with_breaking_cgi_features; + } +} + void cloexec(int fd) { version(Posix) { import core.sys.posix.fcntl; @@ -386,11 +397,16 @@ void cloexec(Socket s) { version(embedded_httpd_hybrid) { version=embedded_httpd_threads; - version(cgi_no_fork) {} else + 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; @@ -532,7 +548,7 @@ class ConnectionClosedException : Exception { } } - + version(Windows) { // FIXME: ugly hack to solve stdin exception problems on Windows: // reading stdin results in StdioException (Bad file descriptor) @@ -564,14 +580,6 @@ private struct stdin { import core.sys.windows.windows; static: - static this() { - // Set stdin to binary mode - version(Win64) - _setmode(std.stdio.stdin.fileno(), 0x8000); - else - setmode(std.stdio.stdin.fileno(), 0x8000); - } - T[] rawRead(T)(T[] buf) { uint bytesRead; auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null); @@ -808,7 +816,7 @@ class Cgi { } } else { // it is an argument of some sort - if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT) { + if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { auto parts = breakUp(arg); _post[parts[0]] ~= parts[1]; allPostNamesInOrder ~= parts[0]; @@ -949,7 +957,7 @@ class Cgi { { import core.runtime; auto sfn = getenv("SCRIPT_FILENAME"); - scriptFileName = sfn.length ? sfn : Runtime.args[0]; + scriptFileName = sfn.length ? sfn : (Runtime.args.length ? Runtime.args[0] : null); } bool iis = false; @@ -1017,7 +1025,7 @@ class Cgi { // FIXME: DOCUMENT_ROOT? // FIXME: what about PUT? - if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT) { + if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { version(preserveData) // a hack to make forwarding simpler immutable(ubyte)[] data; size_t amountReceived = 0; @@ -1028,7 +1036,8 @@ class Cgi { // to be slow if they did that. The spec says it is always there though. // And it has worked reliably for me all year in the live environment, // but some servers might be different. - auto contentLength = to!size_t(getenv("CONTENT_LENGTH")); + auto cls = getenv("CONTENT_LENGTH"); + auto contentLength = to!size_t(cls.length ? cls : "0"); immutable originalContentLength = contentLength; if(contentLength) { @@ -1345,7 +1354,7 @@ class Cgi { } /* - stderr.writeln("RECEIVED: ", pps.piece.name, "=", + stderr.writeln("RECEIVED: ", pps.piece.name, "=", pps.piece.content.length < 1000 ? to!string(pps.piece.content) @@ -1612,7 +1621,7 @@ class Cgi { /// My idea here was so you can output a progress bar or /// something to a cooperative client (see arsd.rtud for a potential helper) /// - /// The default is to do nothing. Subclass cgi and use the + /// The default is to do nothing. Subclass cgi and use the /// CustomCgiMain mixin to do something here. void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const { // This space intentionally left blank. @@ -1742,7 +1751,7 @@ class Cgi { { import core.runtime; - scriptFileName = Runtime.args[0]; + scriptFileName = Runtime.args.length ? Runtime.args[0] : null; } @@ -1857,6 +1866,8 @@ class Cgi { // FIXME: if size is > max content length it should // also fail at this point. _rawDataOutput(cast(ubyte[]) "HTTP/1.1 100 Continue\r\n\r\n"); + + // FIXME: let the user write out 103 early hints too } } // else @@ -1964,8 +1975,8 @@ class Cgi { /// application. Either use Apache's built in methods for basic authentication, or add /// something along these lines to your server configuration: /// - /// RewriteEngine On - /// RewriteCond %{HTTP:Authorization} ^(.*) + /// RewriteEngine On + /// RewriteCond %{HTTP:Authorization} ^(.*) /// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] /// /// To ensure the necessary data is available to cgi.d. @@ -2004,10 +2015,13 @@ class Cgi { uri ~= "s"; uri ~= "://"; uri ~= host; + /+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right hting now + version(none) if(!(!port || port == defaultPort)) { uri ~= ":"; uri ~= to!string(port); } + +/ uri ~= requestUri; return uri; } @@ -2190,6 +2204,17 @@ class Cgi { customHeaders ~= h; } + /++ + I named the original function `header` after PHP, but this pattern more fits + the rest of the Cgi object. + + Either name are allowed. + + History: + Alias added June 17, 2022. + +/ + alias setResponseHeader = header; + private string[] customHeaders; private bool websocketMode; @@ -2420,7 +2445,7 @@ class Cgi { /++ Gets a request variable as a specific type, or the default value of it isn't there or isn't convertible to the request type. - + Checks both GET and POST variables, preferring the POST variable, if available. A nice trick is using the default value to choose the type: @@ -2555,7 +2580,7 @@ class Cgi { immutable(char[]) referrer; immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : ""); - immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.) + immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.) immutable bool https; /// Was the request encrypted via https? immutable int port; /// On what TCP port number did the server receive the request? @@ -2568,7 +2593,7 @@ class Cgi { /** Represents user uploaded files. - + When making a file upload form, be sure to follow the standard: set method="POST" and enctype="multipart/form-data" in your html
`); } - container.appendChild(obj.toHtml()); + container.appendChild(obj.toHtml(presenter)); cgi.write(container.parentDocument.toString, true); } } @@ -10121,7 +10884,7 @@ struct DUMMY {} struct SetOfFields(T) { private void[0][string] storage; void set(string what) { - //storage[what] = + //storage[what] = } void unset(string what) {} void setAll() {} @@ -10145,7 +10908,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); } @@ -10165,9 +10928,37 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); } +/++ + Serves static data. To be used with [dispatcher]. + + History: + Added October 31, 2021 ++/ +auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { + assert(urlPrefix[0] == '/'); + if(contentType is null) { + contentType = contentTypeFromFileExtension(urlPrefix); + } + + static struct DispatcherDetails { + immutable(void)[] data; + string contentType; + } + + static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { + cgi.setCache(true); + cgi.setResponseContentType(details.contentType); + cgi.write(details.data, true); + return true; + } + return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); +} + string contentTypeFromFileExtension(string filename) { if(filename.endsWith(".png")) return "image/png"; + if(filename.endsWith(".apng")) + return "image/apng"; if(filename.endsWith(".svg")) return "image/svg+xml"; if(filename.endsWith(".jpg")) @@ -10201,7 +10992,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; @@ -10428,27 +11219,58 @@ private static string getHttpCodeText(int code) pure nothrow @nogc { case 203: return "203 Non-Authoritative Information"; case 204: return "204 No Content"; case 205: return "205 Reset Content"; + case 206: return "206 Partial Content"; // case 300: return "300 Multiple Choices"; case 301: return "301 Moved Permanently"; case 302: return "302 Found"; case 303: return "303 See Other"; + case 304: return "304 Not Modified"; + case 305: return "305 Use Proxy"; case 307: return "307 Temporary Redirect"; case 308: return "308 Permanent Redirect"; + // - // FIXME: add more common 400 ones cgi.d might return too case 400: return "400 Bad Request"; + case 401: return "401 Unauthorized"; + case 402: return "402 Payment Required"; case 403: return "403 Forbidden"; case 404: return "404 Not Found"; case 405: return "405 Method Not Allowed"; case 406: return "406 Not Acceptable"; + case 407: return "407 Proxy Authentication Required"; + case 408: return "408 Request Timeout"; case 409: return "409 Conflict"; case 410: return "410 Gone"; - // + case 411: return "411 Length Required"; + case 412: return "412 Precondition Failed"; + case 413: return "413 Payload Too Large"; + case 414: return "414 URI Too Long"; + case 415: return "415 Unsupported Media Type"; + case 416: return "416 Range Not Satisfiable"; + case 417: return "417 Expectation Failed"; + case 418: return "418 I'm a teapot"; + case 421: return "421 Misdirected Request"; + case 422: return "422 Unprocessable Entity (WebDAV)"; + case 423: return "423 Locked (WebDAV)"; + case 424: return "424 Failed Dependency (WebDAV)"; + case 425: return "425 Too Early"; + case 426: return "426 Upgrade Required"; + case 428: return "428 Precondition Required"; + case 431: return "431 Request Header Fields Too Large"; + case 451: return "451 Unavailable For Legal Reasons"; + case 500: return "500 Internal Server Error"; case 501: return "501 Not Implemented"; case 502: return "502 Bad Gateway"; case 503: return "503 Service Unavailable"; + case 504: return "504 Gateway Timeout"; + case 505: return "505 HTTP Version Not Supported"; + case 506: return "506 Variant Also Negotiates"; + case 507: return "507 Insufficient Storage (WebDAV)"; + case 508: return "508 Loop Detected (WebDAV)"; + case 510: return "510 Not Extended"; + case 511: return "511 Network Authentication Required"; // default: assert(0, "Unsupported http code"); } @@ -10476,12 +11298,14 @@ bool apiDispatcher()(Cgi cgi) { import arsd.dom; } +/ +version(linux) +private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; /* -Copyright: Adam D. Ruppe, 2008 - 2021 +Copyright: Adam D. Ruppe, 2008 - 2022 License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. Authors: Adam D. Ruppe - Copyright Adam D. Ruppe 2008 - 2021. + Copyright Adam D. Ruppe 2008 - 2022. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)