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 }