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, DataVec;
29 import ae.sys.file : readExactly;
30 import ae.utils.text.ascii : toDec;
31 
32 /// Return true if the current process was invoked as a CGI script.
33 bool inCGI()
34 {
35 	return !!environment.get("GATEWAY_INTERFACE", null);
36 }
37 
38 /// Return true if it seems likely that we are being invoked as an NPH
39 /// (non-parsed headers) script.
40 bool isNPH()
41 {
42 	// https://www.htmlhelp.com/faq/cgifaq.2.html#8
43 	return Runtime.args[0].baseName.startsWith("nph-");
44 }
45 
46 /// Load the CGI request from the environment / standard input.
47 CGIRequest readCGIRequest(
48 	string[string] env = environment.toAA(),
49 	File input = stdin,
50 )
51 {
52 	auto request = CGIRequest.fromAA(env);
53 
54 	if (request.vars.contentLength)
55 	{
56 		auto contentLength = request.vars.contentLength.to!size_t;
57 		if (contentLength)
58 		{
59 			auto data = Data(contentLength);
60 			input.readExactly(cast(ubyte[])data.contents)
61 				.enforce("EOF while reading content data");
62 			request.data = DataVec(data);
63 		}
64 	}
65 
66 	return request;
67 }
68 
69 private struct FileWriter
70 {
71 	File f;
72 	void put(T...)(auto ref T args) { f.write(args); }
73 }
74 
75 /// Write the response headers from a HTTP response in CGI format.
76 void writeCGIHeaders(Writer)(HttpResponse r, ref Writer writer)
77 {
78 	auto headers = r.headers;
79 	if (r.status)
80 		headers.require("Status", text(ushort(r.status), " ", r.statusMessage));
81 
82 	static immutable string[] headerOrder = ["Location", "Content-Type", "Status"];
83 	foreach (name; headerOrder)
84 		if (auto p = name in headers)
85 			writer.put(name, ": ", *p, "\n");
86 
87 	foreach (name, value; headers)
88 		if (!headerOrder.canFind(name.normalizeHeaderName))
89 			writer.put(name, ": ", value, "\n");
90 	writer.put("\n");
91 }
92 
93 /// Write the response headers from a HTTP response in CGI NPH format.
94 void writeNPHHeaders(Writer)(HttpResponse r, ref Writer writer)
95 {
96 	char[5] statusBuf;
97 	writer.put("HTTP/1.0 ", toDec(ushort(r.status), statusBuf), " ", r.statusMessage, "\n");
98 	foreach (string header, string value; r.headers)
99 		writer.put(header, ": ", value, "\n");
100 	writer.put("\n");
101 }
102 
103 /// Write a HTTP response in CGI format.
104 void writeCGIResponse(HttpResponse r)
105 {
106 	auto writer = FileWriter(stdout);
107 	writeCGIHeaders(r, writer);
108 
109 	foreach (datum; r.data)
110 		stdout.rawWrite(datum.contents);
111 }
112 
113 /// Write a HTTP response in CGI NPH format.
114 void writeNPHResponse(HttpResponse r)
115 {
116 	auto writer = FileWriter(stdout);
117 	writeNPHHeaders(r, writer);
118 
119 	foreach (datum; r.data)
120 		stdout.rawWrite(datum.contents);
121 }