1 /**
2  * Support for implementing SCGI application servers.
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.scgi.app;
15 
16 import std.algorithm.searching : findSplit;
17 import std.conv : to;
18 import std.exception;
19 import std.string;
20 
21 import ae.net.asockets;
22 import ae.net.http.cgi.common;
23 import ae.net.http.cgi.script;
24 import ae.net.http.common;
25 import ae.sys.dataset : DataVec;
26 import ae.sys.log;
27 import ae.utils.array;
28 
29 /// Implements the SCGI protocol over an abstract connection.
30 final class SCGIConnection
31 {
32 	IConnection connection;  /// Connection used to construct this object.
33 	Logger log;              /// Optional logger.
34 	bool nph;                /// Whether to operate in Non-Parsed Headers mode.
35 
36 	/// Constructor.
37 	/// Params:
38 	///  connection = Abstract connection used for communication.
39 	this(IConnection connection)
40 	{
41 		this.connection = connection;
42 		connection.handleReadData = &onReadData;
43 	}
44 
45 	private Data buffer;
46 
47 	protected void onReadData(Data data)
48 	{
49 		buffer ~= data;
50 
51 		while (true)
52 			try
53 			{
54 				buffer.asDataOf!char.enter((scope bufferStr) {
55 					auto colonIdx = bufferStr.indexOf(':');
56 					if (colonIdx < 0)
57 						return;
58 
59 					auto headerLenStr = bufferStr[0 .. colonIdx];
60 					auto headerLen = headerLenStr.to!size_t;
61 					auto headerEnd = headerLenStr.length + 1 /*:*/ + headerLen + 1 /*,*/;
62 					if (buffer.length < headerEnd)
63 						return;
64 					enforce(bufferStr[headerEnd - 1] == ',', "Expected ','");
65 
66 					auto headersStr = bufferStr[headerLenStr.length + 1 .. headerEnd - 1];
67 					enum CONTENT_LENGTH = "CONTENT_LENGTH";
68 					enforce(headersStr.startsWith(CONTENT_LENGTH ~ "\0"), "Expected first header to be " ~ CONTENT_LENGTH);
69 					auto contentLength = headersStr[CONTENT_LENGTH.length + 1 .. $].findSplit("\0")[0].to!size_t;
70 					if (buffer.length < headerEnd + contentLength)
71 						return;
72 
73 					// We now know we have all the data in the request
74 
75 					auto headers = parseHeaders(headersStr.idup);
76 					enforce(headers.get("SCGI", null) == "1", "Unknown SCGI version");
77 					CGIRequest request;
78 					request.vars = CGIVars.fromAA(headers);
79 					request.headers = CGIRequest.decodeHeaders(headers, request.vars.serverProtocol ? request.vars.serverProtocol : "HTTP");
80 					request.data = DataVec(buffer[headerEnd .. headerEnd + contentLength]);
81 					buffer = buffer[headerEnd + contentLength .. $];
82 					handleRequest(request);
83 				});
84 			}
85 			catch (Exception e)
86 			{
87 				if (log) log("Error handling request: " ~ e.toString());
88 				connection.disconnect(e.msg);
89 				return;
90 			}
91 	}
92 
93 	/// Parse SCGI-formatted headers.
94 	static string[string] parseHeaders(string s)
95 	{
96 		string[string] headers;
97 		while (s.length)
98 		{
99 			auto name = s.skipUntil('\0').enforce("Unterminated header name");
100 			auto value = s.skipUntil('\0').enforce("Unterminated header value");
101 			headers[name] = value;
102 		}
103 		return headers;
104 	}
105 
106 	/// Write a response.
107 	void sendResponse(HttpResponse r)
108 	{
109 		FastAppender!char headers;
110 		if (nph)
111 			writeNPHHeaders(r, headers);
112 		else
113 			writeCGIHeaders(r, headers);
114 		connection.send(Data(headers.get().asBytes));
115 		connection.send(r.data[]);
116 		connection.disconnect("Response sent");
117 	}
118 
119 	/// User-supplied callback for handling incoming requests.
120 	void delegate(ref CGIRequest) handleRequest;
121 }