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 }