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 }