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