1 /+
2 MIT License
3
4 Copyright (c) 2020 2night SpA
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 +/
24
25 module dcgi;
26
27 /// UDA for your handler
28 struct MaxRequestBodyLength { size_t length; }
29
30 ///
31 enum DisplayExceptions;
32
33 /// Remember to mixin this template inside your main file.
34 template DCGI(alias handler = null)
35 {
36 void main()
37 {
38 static if (is(typeof(handler) == typeof(null)))
39 {
40 static if (!__traits(compiles, cgi(Request.init, Output.init))) static assert(0, "You must define: void cgi(Request r, Output o) { ... }");
41 else cgiMain!cgi();
42 }
43 else
44 {
45 static if (!__traits(compiles, handler(Request.init, Output.init))) static assert(0, "You must define: void " ~ handler.stringof ~ "(Request r, Output o) { ... }");
46 else cgiMain!handler();
47 }
48 }
49 }
50
51 /// You're supposed not to call this function. It is called from the mixed-in code
52 void cgiMain(alias handler)()
53 {
54 import std.traits : hasUDA, getUDAs;
55 import std.file : readText;
56
57 // Redirecting stdout to avoid mistakes using writeln & c.
58 import std.stdio : stdout, stderr;
59 auto stream = stdout;
60 stdout = stderr;
61
62 import std.format : format;
63 import std.conv : to;
64 import std.process : environment;
65 import std..string : toUpper;
66 import std.algorithm : map;
67 import std.array : assocArray;
68 import std.typecons : tuple;
69
70 string[string] headers =
71 environment
72 .toAA
73 .byKeyValue
74 .map!(t => tuple(t.key.toUpper,t.value))
75 .assocArray;
76
77 char[] buffer;
78
79 Request request;
80 Output output;
81 bool showExceptions;
82 size_t maxRequestBodyLength = 1024*4;
83
84 try {
85 output = new Output(stream);
86 showExceptions = hasUDA!(handler, DisplayExceptions);
87
88 static if (hasUDA!(handler, MaxRequestBodyLength))
89 maxRequestBodyLength = getUDAs!(handler, MaxRequestBodyLength)[0].length;
90
91 {
92 // Read body data
93 import std.stdio : stdin;
94 import core.sys.posix.sys.select;
95
96 // Is there anything on stdin?
97 timeval tv = timeval(0, 1);
98 fd_set fds;
99 FD_ZERO(&fds);
100 FD_SET(0, &fds);
101 select(0+1, &fds, null, null, &tv);
102
103 bool hasData = FD_ISSET(0, &fds);
104 if (hasData)
105 {
106 buffer.length = maxRequestBodyLength + 1;
107 auto requestData = stdin.rawRead(buffer);
108
109 if (requestData.length > maxRequestBodyLength)
110 throw new Exception("Request body too large");
111
112 buffer.length = requestData.length;
113 }
114 }
115
116 // Init request. Call handler
117 request = new Request(headers, buffer);
118 handler(request, output);
119 }
120
121 // Unhandled Exception escape from user code
122 catch (Exception e)
123 {
124 if (!output.headersSent)
125 output.status = 501;
126
127 cgiLog(format("Unhandled exception: %s", e.msg));
128 cgiLog(e.to!string);
129
130 if (showExceptions)
131 output(format("<pre>\n%s\n</pre>", e.to!string));
132 }
133
134 // Even worst.
135 catch (Throwable t)
136 {
137 // I know I'm not supposed to catch Throwable and continue with execution
138 // but it just tries to write exception and exit.
139
140 if (!output.headersSent)
141 output.status = 501;
142
143 cgiLog(format("Throwable: %s", t.msg));
144 cgiLog(t.to!string);
145
146 if (showExceptions)
147 output(format("<pre>\n%s\n</pre>", t.to!string));
148 }
149
150 // No reply so far?
151 if (!output.headersSent)
152 {
153 // Send and empty response
154 output("");
155 }
156 }
157
158 /// Write a formatted log
159 void cgiLog(T...)(T params)
160 {
161 import std.datetime : SysTime, Clock;
162 import std.conv : to;
163 import std.stdio : write, writeln, stderr;
164
165 SysTime t = Clock.currTime;
166
167 stderr.writef(
168 "%04d/%02d/%02d %02d:%02d:%02d.%s >>> ",
169 t.year, t.month, t.day, t.hour,t.minute,t.second,t.fracSecs.split!"msecs".msecs
170 );
171
172 foreach(p; params)
173 stderr.write(p.to!string, " ");
174
175 stderr.writeln;
176 stderr.flush;
177 }
178
179 /// A cookie
180 private import std.datetime : DateTime;
181 struct Cookie
182 {
183 string name; /// Cookie data
184 string value; /// ditto
185 string path; /// ditto
186 string domain; /// ditto
187
188 DateTime expire; /// ditto
189
190 bool session = true; /// ditto
191 bool secure = false; /// ditto
192 bool httpOnly = false; /// ditto
193
194 /// Invalidate cookie
195 public void invalidate()
196 {
197 expire = DateTime(1970,1,1,0,0,0);
198 }
199 }
200
201 /// A request from user
202 class Request
203 {
204
205 /// HTTP methods
206 public enum Method
207 {
208 Get, ///
209 Post, ///
210 Head, ///
211 Delete, ///
212 Put, ///
213 Unknown = -1 ///
214 }
215
216 @disable this();
217
218 private this(string[string] headers, char[] requestData)
219 {
220 import std.regex : match, ctRegex;
221 import std.uri : decodeComponent;
222 import std..string : translate, split, toLower;
223
224 // Reset values
225 _header = headers;
226 _get = (typeof(_get)).init;
227 _post = (typeof(_post)).init;
228 _cookie = (typeof(_cookie)).init;
229 _data = requestData;
230
231 // Read get params
232 if ("QUERY_STRING" in _header)
233 foreach(m; match(_header["QUERY_STRING"], ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g")))
234 _get[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent;
235
236 // Read post params
237 if ("REQUEST_METHOD" in _header && _header["REQUEST_METHOD"] == "POST")
238 if(_data.length > 0 && split(_header["CONTENT_TYPE"].toLower(),";")[0] == "application/x-www-form-urlencoded")
239 foreach(m; match(_data, ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g")))
240 _post[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent;
241
242 // Read cookies
243 if ("HTTP_COOKIE" in _header)
244 foreach(m; match(_header["HTTP_COOKIE"], ctRegex!("([^=]+)=([^;]+);? ?", "g")))
245 _cookie[m.captures[1].decodeComponent] = m.captures[2].decodeComponent;
246
247 }
248
249 ///
250 @nogc @property nothrow public const(char[]) data() const { return _data; }
251
252 ///
253 @nogc @property nothrow public const(string[string]) get() const { return _get; }
254
255 ///
256 @nogc @property nothrow public const(string[string]) post() const { return _post; }
257
258 ///
259 @nogc @property nothrow public const(string[string]) header() const { return _header; }
260
261 ///
262 @nogc @property nothrow public const(string[string]) cookie() const { return _cookie; }
263
264 ///
265 @property public Method method() const
266 {
267 import std..string : toLower;
268 switch(_header["REQUEST_METHOD"].toLower())
269 {
270 case "get": return Method.Get;
271 case "post": return Method.Post;
272 case "head": return Method.Head;
273 case "put": return Method.Put;
274 case "delete": return Method.Delete;
275 default: return Method.Unknown;
276 }
277 }
278
279 private char[] _data;
280 private string[string] _get;
281 private string[string] _post;
282 private string[string] _header;
283 private string[string] _cookie;
284
285 }
286
287 /// Your reply.
288 class Output
289 {
290 private import std.stdio : File;
291
292 private struct KeyValue
293 {
294 this (in string key, in string value) { this.key = key; this.value = value; }
295 string key;
296 string value;
297 }
298
299 /// You can add a http header. But you can't if body is already sent.
300 public void addHeader(in string key, in string value)
301 {
302 if (_headersSent)
303 throw new Exception("Can't add/edit headers. Too late. Just sent.");
304
305 _headers ~= KeyValue(key, value);
306 }
307
308 @disable this();
309
310 private this(File stream)
311 {
312 _stream = stream;
313 }
314
315 /// Force sending of headers.
316 public void sendHeaders()
317 {
318 import std.format : format;
319
320 if (_headersSent)
321 throw new Exception("Can't resend headers. Too late. Just sent.");
322
323 import std.uri : encode;
324
325 bool has_content_type = false;
326 _stream.write(format("Status: %s\r\n", _status));
327
328 // send user-defined headers
329 foreach(header; _headers)
330 {
331 import std..string : toLower;
332 _stream.write(format("%s: %s\r\n", header.key, header.value));
333 if (header.key.toLower() == "content-type") has_content_type = true;
334 }
335
336 // Default content-type is text/html if not defined by user
337 if (!has_content_type)
338 _stream.write(format("Content-Type: text/html; charset=utf-8\r\n"));
339
340 // If required, I add headers to write cookies
341 foreach(Cookie c; _cookies)
342 {
343
344 _stream.write(format("Set-Cookie: %s=%s", c.name.encode(), c.value.encode()));
345
346 if (!c.session)
347 {
348 string[] mm = ["", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
349 string[] dd = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
350
351 string data = format("%s, %s %s %s %s:%s:%s GMT",
352 dd[c.expire.dayOfWeek], c.expire.day, mm[c.expire.month], c.expire.year,
353 c.expire.hour, c.expire.minute, c.expire.second
354 );
355
356 _stream.write(format("; Expires=%s", data));
357 }
358
359 if (!c.path.length == 0) _stream.write(format("; path=%s", c.path));
360 if (!c.domain.length == 0) _stream.write(format("; domain=%s", c.domain));
361
362 if (c.secure) _stream.write(format("; Secure"));
363 if (c.httpOnly) _stream.write(format("; HttpOnly"));
364
365 _stream.write("\r\n");
366 }
367
368 _stream.write("\r\n");
369 _headersSent = true;
370 }
371
372 /// You can set a cookie. But you can't if body is already sent.
373 public void setCookie(Cookie c)
374 {
375 if (_headersSent)
376 throw new Exception("Can't set cookies. Too late. Just sent.");
377
378 _cookies ~= c;
379 }
380
381 /// Retrieve all cookies
382 @nogc @property nothrow public Cookie[] cookies() { return _cookies; }
383
384 /// Output status
385 @nogc @property nothrow public ulong status() { return _status; }
386
387 /// Set response status. Default is 200 (OK)
388 @property public void status(ulong status)
389 {
390 if (_headersSent)
391 throw new Exception("Can't set status. Too late. Just sent.");
392
393 _status = status;
394 }
395
396 /**
397 * Syntax sugar to write data
398 * Example:
399 * --------------------
400 * output("Hello world", "!");
401 * --------------------
402 */
403 public void opCall(T...)(T params)
404 {
405 import std.conv : to;
406
407 foreach(p; params)
408 write(p);
409 }
410
411 /// Write data
412 public void write(string data) { import std..string : representation; write(data.representation); }
413
414 /// Ditto
415 public void write(in void[] data)
416 {
417 import std.stdio : stdout;
418
419 if (!_headersSent)
420 sendHeaders();
421
422 _stream.rawWrite(data);
423 }
424
425 /// Are headers already sent?
426 @nogc nothrow public bool headersSent() { return _headersSent; }
427
428 private bool _headersSent = false;
429 private Cookie[] _cookies;
430 private KeyValue[] _headers;
431 private ulong _status = 200;
432 private File _stream;
433 }