1 module fastcgi;
2 
3 import std.socket;
4 import std.conv;
5 import std.algorithm;
6 import std.range;
7 import std.experimental.logger;
8 import std.random;
9 import std.datetime;
10 import std.utf;
11 import std.typecons;
12 
13 struct FastCGIHeader {
14     ubyte _version;
15     ubyte type;
16     ushort requestId;
17     ushort contentLength;
18     ubyte paddingLength;
19     ubyte reserved;
20     string content;
21 }
22 
23 final class FastCGIClient {
24     private {
25         static immutable FCGI_VERSION = 1;
26         static immutable FCGI_ROLE_RESPONDER = 1;
27         static immutable FCGI_ROLE_AUTHORIZER = 2;
28         static immutable FCGI_ROLE_FILTER = 3;
29         static immutable FCGI_TYPE_BEGIN = 1;
30         static immutable FCGI_TYPE_ABORT = 2;
31         static immutable FCGI_TYPE_END = 3;
32         static immutable FCGI_TYPE_PARAMS = 4;
33         static immutable FCGI_TYPE_STDIN = 5;
34         static immutable FCGI_TYPE_STDOUT = 6;
35         static immutable FCGI_TYPE_STDERR = 7;
36         static immutable FCGI_TYPE_DATA = 8;
37         static immutable FCGI_TYPE_GETVALUES = 9;
38         static immutable FCGI_TYPE_GETVALUES_RESULT = 10;
39         static immutable FCGI_TYPE_UNKOWNTYPE = 11;
40         static immutable FCGI_HEADER_SIZE = 8;
41     }
42 
43     enum FCGI_STATE_SEND = 1;
44     enum FCGI_STATE_ERROR = 2;
45     enum FCGI_STATE_SUCCESS = 3;
46 
47     private {
48         string host;
49         ushort port;
50         Duration timeout;
51         int keepalive;
52         Socket sock;
53         string[string][ushort] requests;
54     }
55 
56     this(string host, ushort port, int timeout, bool keepalive) {
57         this.host = host;
58         this.port = port;
59         this.timeout = seconds(timeout);
60         this.keepalive = keepalive ? 1 : 0;
61         this.sock = null;
62     }
63 
64     bool connect() {
65        this.sock = new Socket(AddressFamily.INET, SocketType.STREAM);
66        
67 	   this.sock.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, this.timeout);
68         try {
69             this.sock.connect(new InternetAddress(this.host, this.port));
70         } catch (SocketException msg) {
71             error(msg.toString());
72             return false;
73         }
74         return true;
75     }
76 
77     bool ensureConnection() {
78         if (!this.isConnected()) {
79             if (!this.connect()) {
80                 error("Failed to establish connection!");
81                 return false;
82             }
83         }
84         return true;
85     }
86 
87     bool isConnected() {
88         return this.sock !is null;
89     }
90 
91     void closeConnection() {
92         if (this.isConnected()) {
93             this.sock.close();
94             this.sock = null;
95         }
96     }
97 
98     ubyte[] encodeFastCGIRecord(ubyte fcgi_type, ubyte[] content, ushort requestid) {
99         auto length = content.length;
100         return [
101             cast(ubyte) FCGI_VERSION,
102             cast(ubyte) fcgi_type,
103             cast(ubyte) ((requestid >> 8) & 0xFF),
104             cast(ubyte) (requestid & 0xFF),
105             cast(ubyte) ((length >> 8) & 0xFF),
106             cast(ubyte) (length & 0xFF),
107             cast(ubyte) 0,
108             cast(ubyte) 0
109         ] ~ content;
110     }
111 
112     ubyte[] encodeNameValueParams(string name, string value) {
113         auto nLen = name.length;
114         auto vLen = value.length;
115         ubyte[] record;
116         if (nLen < 128) {
117             record ~= cast(ubyte) nLen;
118         } else {
119             record ~= cast(ubyte) (((nLen >> 24) & 0xFF) | 0x80);
120             record ~= cast(ubyte) ((nLen >> 16) & 0xFF);
121             record ~= cast(ubyte) ((nLen >> 8) & 0xFF);
122             record ~= cast(ubyte) (nLen & 0xFF);
123         }
124         if (vLen < 128) {
125             record ~= cast(ubyte) vLen;
126         } else {
127             record ~= cast(ubyte) (((vLen >> 24) & 0xFF) | 0x80);
128             record ~= cast(ubyte) ((vLen >> 16) & 0xFF);
129             record ~= cast(ubyte) ((vLen >> 8) & 0xFF);
130             record ~= cast(ubyte) (vLen & 0xFF);
131         }
132         return record ~ cast(ubyte[]) name.toUTF8 ~ cast(ubyte[]) value.toUTF8;
133     }
134 
135     FastCGIHeader decodeFastCGIHeader(ubyte[] stream) {
136         FastCGIHeader header;
137 
138         header._version = stream[0];
139         header.type = stream[1];
140         header.requestId = (cast(ushort) stream[2] << 8) | stream[3];
141         header.contentLength = (cast(ushort) stream[4] << 8) | stream[5];
142         header.paddingLength = stream[6];
143         header.reserved = stream[7];
144 
145         return header;
146     }
147 
148     Nullable!FastCGIHeader decodeFastCGIRecord() {
149 		Nullable!FastCGIHeader ret;
150 		
151 		ubyte[FCGI_HEADER_SIZE] headerBuff;
152 		auto headerLen = this.sock.receive(headerBuff[]);
153 		
154 		if (headerLen != FCGI_HEADER_SIZE) {
155 			return ret;
156 		}
157 
158 		auto header = decodeFastCGIHeader(headerBuff[]);
159 		if (header.contentLength > 0) {
160 			ubyte[] buffer; buffer.length = header.contentLength;
161 			auto bytesRead = this.sock.receive(buffer);
162 			if (bytesRead != header.contentLength) {
163 				error("Failed to read the expected content length.");
164 				return ret;
165 			}
166 			header.content = cast(string)buffer;
167 		}
168 
169 		if (header.paddingLength > 0) {
170 			auto dummyBuffer = new ubyte[](header.paddingLength);
171 			auto skipped = this.sock.receive(dummyBuffer);
172 		}
173 
174 		return Nullable!FastCGIHeader(header);
175 	}
176 
177 
178     string request(string[string] nameValuePairs, string post) {
179 
180         if (!this.ensureConnection()) {
181             error("connect failure! please check your fasctcgi-server !!");
182             return "";
183         }
184 
185         auto requestId = cast(ushort) uniform(1, (1 << 16) - 1);
186         this.requests[requestId] = ["state": FCGI_STATE_SEND.to!string, "response": ""];
187         ubyte[] request;
188         ubyte[] beginFCGIRecordContent = [
189             cast(ubyte) 0,
190             cast(ubyte) FCGI_ROLE_RESPONDER,
191             cast(ubyte) this.keepalive,
192             cast(ubyte) 0,
193             cast(ubyte) 0,
194             cast(ubyte) 0,
195             cast(ubyte) 0,
196             cast(ubyte) 0
197         ];
198         request ~= this.encodeFastCGIRecord(FCGI_TYPE_BEGIN, beginFCGIRecordContent, requestId);
199         ubyte[] paramsRecord;
200         if (!nameValuePairs.empty) {
201             foreach (name, value; nameValuePairs) {
202                 paramsRecord ~= this.encodeNameValueParams(name, value);
203             }
204         }
205 
206         if (!paramsRecord.empty) {
207             request ~= this.encodeFastCGIRecord(FCGI_TYPE_PARAMS, paramsRecord, requestId);
208         }
209         request ~= this.encodeFastCGIRecord(FCGI_TYPE_PARAMS, null, requestId);
210 
211         if (!post.empty) {
212             request ~= this.encodeFastCGIRecord(FCGI_TYPE_STDIN.to!ubyte, cast(ubyte[])post.toUTF8, requestId);
213         }
214         request ~= this.encodeFastCGIRecord(FCGI_TYPE_STDIN, null, requestId);
215 
216         this.sock.send(request);
217         this.requests[requestId]["state"] = FCGI_STATE_SEND.to!string;
218         return this.waitForResponse(requestId);
219     }
220 
221     string waitForResponse(ushort requestId) {
222         while (true) {
223             auto response = this.decodeFastCGIRecord();
224             if (response.isNull) {
225                 break;
226             }
227             if (response.get.type == FCGI_TYPE_STDOUT || response.get.type == FCGI_TYPE_STDERR) {
228                 if (response.get.type == FCGI_TYPE_STDERR) {
229                     this.requests[requestId]["state"] = FCGI_STATE_ERROR.to!string;
230                 }
231                 if (requestId == response.get.requestId) {
232                     this.requests[requestId]["response"] ~= response.get.content;
233                 }
234             }
235             if (response.get.type == FCGI_STATE_SUCCESS) {
236                 // 
237             }
238         }
239 		this.closeConnection();
240         return this.requests[requestId]["response"];
241     }
242 
243     override string toString() {
244         return "fastcgi connect host:" ~ this.host ~ " port:" ~ to!string(this.port);
245     }
246 }