1 /** 2 * Support for implementing CGI scripts. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.net.http.cgi.script; 15 16 import core.runtime : Runtime; 17 18 import std.algorithm.searching : startsWith, canFind; 19 import std.conv : to, text; 20 import std.exception : enforce; 21 import std.path : baseName; 22 import std.process : environment; 23 import std.stdio : stdin, stdout, File; 24 25 import ae.net.http.cgi.common; 26 import ae.net.http.common; 27 import ae.net.ietf.headers : Headers, normalizeHeaderName; 28 import ae.sys.data : Data; 29 import ae.sys.dataset : DataVec; 30 import ae.sys.file : readExactly; 31 import ae.utils.text.ascii : toDec; 32 33 /// Return true if the current process was invoked as a CGI script. 34 bool inCGI() 35 { 36 return !!environment.get("GATEWAY_INTERFACE", null); 37 } 38 39 /// Return true if it seems likely that we are being invoked as an NPH 40 /// (non-parsed headers) script. 41 bool isNPH() 42 { 43 // https://www.htmlhelp.com/faq/cgifaq.2.html#8 44 return Runtime.args[0].baseName.startsWith("nph-"); 45 } 46 47 void prepareCGIFDs(File input = stdin, File output = stdout) 48 { 49 version (Posix) 50 { 51 import std.socket : Socket, AddressFamily, socket_t; 52 import std.typecons : scoped; 53 import core.sys.posix.unistd : dup; 54 55 auto stdinSocket = scoped!Socket(cast(socket_t)input.fileno.dup, AddressFamily.UNSPEC); 56 stdinSocket.blocking = true; 57 auto stdoutSocket = scoped!Socket(cast(socket_t)output.fileno.dup, AddressFamily.UNSPEC); 58 stdoutSocket.blocking = true; 59 } 60 } 61 62 /// Load the CGI request from the environment / standard input. 63 CGIRequest readCGIRequest( 64 string[string] env = environment.toAA(), 65 File input = stdin, 66 ) 67 { 68 auto request = CGIRequest.fromAA(env); 69 70 if (request.vars.contentLength) 71 { 72 auto contentLength = request.vars.contentLength.to!size_t; 73 if (contentLength) 74 { 75 auto data = Data(contentLength); 76 data.asDataOf!ubyte.enter((scope contents) { 77 input.readExactly(contents) 78 .enforce("EOF while reading content data"); 79 }); 80 request.data = DataVec(data); 81 } 82 } 83 84 return request; 85 } 86 87 private struct FileWriter 88 { 89 File f; 90 void put(T...)(auto ref T args) { f.write(args); } 91 } 92 93 /// Write the response headers from a HTTP response in CGI format. 94 void writeCGIHeaders(Writer)(HttpResponse r, ref Writer writer) 95 { 96 auto headers = r.headers; 97 if (r.status) 98 headers.require("Status", text(ushort(r.status), " ", r.statusMessage)); 99 100 static immutable string[] headerOrder = ["Location", "Content-Type", "Status"]; 101 foreach (name; headerOrder) 102 if (auto p = name in headers) 103 writer.put(name, ": ", *p, "\n"); 104 105 foreach (name, value; headers) 106 if (!headerOrder.canFind(name.normalizeHeaderName)) 107 writer.put(name, ": ", value, "\n"); 108 writer.put("\n"); 109 } 110 111 /// Write the response headers from a HTTP response in CGI NPH format. 112 void writeNPHHeaders(Writer)(HttpResponse r, ref Writer writer) 113 { 114 char[5] statusBuf; 115 writer.put("HTTP/1.0 ", toDec(ushort(r.status), statusBuf), " ", r.statusMessage, "\n"); 116 foreach (string header, string value; r.headers) 117 writer.put(header, ": ", value, "\n"); 118 writer.put("\n"); 119 } 120 121 /// Write a HTTP response in CGI format. 122 void writeCGIResponse(HttpResponse r) 123 { 124 auto writer = FileWriter(stdout); 125 writeCGIHeaders(r, writer); 126 127 foreach (datum; r.data) 128 datum.enter((scope contents) { 129 stdout.rawWrite(contents); 130 }); 131 } 132 133 /// Write a HTTP response in CGI NPH format. 134 void writeNPHResponse(HttpResponse r) 135 { 136 auto writer = FileWriter(stdout); 137 writeNPHHeaders(r, writer); 138 139 foreach (datum; r.data) 140 datum.enter((scope contents) { 141 stdout.rawWrite(contents); 142 }); 143 }