1 /++
2  + A simple way to run your D scripts/applications on a webserver
3  + ---
4  + import std.stdio;
5  + import reserved.reserved;
6  +
7  + @ReservedResponse
8  + void response(Request req, Output output)
9  + {
10  +   // A really useful application.
11  +   output ~= "Hello ";
12  +   if ("name" in req.get) output ~= req.get["name"];
13  +   else output ~= "World";
14  + }
15  +
16  + mixin Reserved!"awesome_d_webservice";
17  + ---
18  +/
19  
20 module reserved;
21 
22 private import std.datetime      : DateTime; // For cookies
23 private import std..string        : toLower, format, empty;
24 private import core.sync.mutex   : Mutex;
25 private import std.exception;
26 private import std.socket;
27 
28 __gshared Mutex lock;
29 
30 /// UDA. Annotate a function ```bool your_function()``` or ```bool your_function(string[] args)``` to execute it once before the first request.
31 public enum ReservedInit;
32 
33 /// UDA. Annotate a function ```void response(Request req, Output output)``` that will be called for each request.     
34 public enum ReservedResponse; 
35 
36 /** Remember to mixin this on your main file.
37  * Params:
38  *      serviceName = A UNIX socket will be created inside /tmp/run/serviceName dir. 
39  */
40 template Reserved(string serviceName)
41 {
42    // This will catch term signals
43    extern(C)
44    void exit_gracefully(int value)
45    {
46       import std.c.stdlib : exit;
47       lock.lock();
48       exit(0);
49    }
50 
51    int main(string[] args)
52    {
53       import core.sys.posix.unistd;
54       import core.sys.posix.signal;
55       import core.sync.mutex;
56       import std.traits : hasUDA, ReturnType;
57       import std.file : mkdirRecurse, write, readText, exists, remove;
58       
59       ulong listenerId = 0;
60 
61       // Command line parsing
62       {
63          import std.getopt;
64       
65          auto parsedArgs = getopt
66          (
67             args,
68             "listenerId|i", "Id used by listener, default = 0",  &listenerId
69          );
70    
71          if (parsedArgs.helpWanted)
72          {
73             defaultGetoptPrinter("Reserved listener for " ~ serviceName, parsedArgs.options);
74             return 0;
75          }
76       }
77 
78       synchronized { lock = new Mutex(); }
79 
80       // Catching signal
81       sigset(SIGINT, &exit_gracefully);
82       sigset(SIGTERM, &exit_gracefully);
83 
84       // Search for basic functions
85       auto getReservedFunction(alias T)()
86       {
87          foreach(name;  __traits(allMembers,  __traits(parent, main)))
88             static if (hasUDA!(__traits(getMember, __traits(parent, main), name), T))
89                return &__traits(getMember, __traits(parent, main), name);
90       }
91 
92       // Check if init function is good for us.
93       static if (!is(ReturnType!(getReservedFunction!ReservedInit) : void))
94       {
95          auto init = getReservedFunction!ReservedInit;
96          static if (!is(ReturnType!init == bool)) static assert(0, "@ReservedInit musts return a bool");
97          static if (__traits(compiles, init(args))) enum callInitWithParams = true;
98          else enum callInitWithParams = false;
99       }
100       else // Init function is optional
101       {
102          auto init = function() { return true; };
103          enum callInitWithParams = false;
104       } 
105 
106       // Check if handler is good for us too.
107       static if (!is(ReturnType!(getReservedFunction!ReservedResponse) : void)) 
108          auto handler = getReservedFunction!ReservedResponse;
109       else static assert(0, "You must define a @ReservedResponse function");
110 
111       static if (!__traits(compiles, handler(Request.init, Output.init))) static assert(0, "@ReservedResponse musts accept Request and Output as params");
112       else {
113 
114          // Call init function
115          bool initResult;
116 
117          static if (callInitWithParams) initResult = init(args);
118          else initResult = init();
119          
120          if (!initResult)
121          {
122             reservedLog("Init failed, exit.");
123             return -1;
124          }
125 
126          // Our socket
127          import std.format : format;
128          immutable basePath = "/tmp/run/" ~ serviceName;
129          immutable socketFile = format("%s/listener.%s.sock", basePath, listenerId);
130          
131          // Create dir if not exists
132          mkdirRecurse(basePath);
133 
134          // Is anyone else using our socket?
135          {
136             import std.process : executeShell;
137             import core.thread : Thread;
138             import core.time : dur;
139 
140             // Kill it gently
141             executeShell("touch " ~ socketFile ~ "; fuser -k -SIGTERM " ~ socketFile);
142             Thread.sleep(dur!"msecs"(200));
143 
144             // Kill it!
145             executeShell("touch " ~ socketFile ~ "; fuser -k -SIGKILL " ~ socketFile);
146          }
147 
148          if (exists(socketFile)) 
149             remove(socketFile);
150 
151        __reservedImpl!serviceName(handler, socketFile);
152       }
153       
154       return 0;
155    }	
156 }
157 
158 void __reservedImpl(string serviceName, H)(H handler, string socketFile)
159 {
160    import std.range : chunks, array;
161    import std.conv : to;
162    
163    // Let's listen using a brand new socket
164    UnixAddress address = new UnixAddress(socketFile);
165    
166    Socket receiver;
167    Socket listener = new Socket(AddressFamily.UNIX, SocketType.STREAM);
168    listener.blocking = true;
169    listener.bind(address);
170    listener.listen(3);
171 
172    reservedLog("Ok, ready. Socket:", socketFile);
173 
174    // Buffer we use to read request from server
175    char[] buffer;
176    buffer.length = 4096;
177    
178    char[] requestData;
179    requestData.reserve = 4096*10;
180 
181    while(true)
182    {
183       requestData.length = 0;
184 
185       string[string] headers;
186       bool headersCompleted = false;
187       bool requestCompleted = false;
188 
189       // We accept request from webserver            
190       receiver = listener.accept();
191 
192       // We use lock to prevent shutting down during operation
193       lock.lock();
194 
195             
196       while(!requestCompleted)
197       {
198          import std.ascii : isDigit;
199 
200          // Read some data from server
201          auto received = receiver.receive(buffer);
202          
203          // Something goes wrong?
204          if (received < 0)
205          {
206             requestData.length = 0;
207             break;
208          }
209 
210          // Append data
211          requestData ~= buffer[0..received];
212 
213          if (!headersCompleted)
214          {
215             // Check if request is complete
216             foreach(idx,c; requestData)
217             {
218                if (!(c.isDigit || c == ':')) 
219                   break;
220                
221                if (c == ':')
222                {
223                   size_t headersSize = requestData[0..idx].to!size_t;
224                   if (requestData.length >= headersSize + idx)
225                   {
226                      headersCompleted = true;
227 
228                      // Parse headers
229                      import std.algorithm : splitter;
230                      foreach(pair; requestData[idx+1..idx+1+headersSize].to!string.splitter('\0').chunks(2))
231                      {
232                         auto p = pair.array;
233                         
234                         if (p[0].empty) 
235                            continue;
236 
237                         headers[p[0]] = p[1];
238                      }
239 
240                      requestData = requestData[idx+1+headersSize+1..$];
241                      break;
242                   }
243                }
244             }
245          }
246          
247          if (headersCompleted && requestData.length >= headers["CONTENT_LENGTH"].to!size_t)
248             requestCompleted = true;
249       }
250 
251       if (!requestCompleted)
252       {
253          // TODO: Bad request;
254          lock.unlock();
255          receiver.close();
256          continue;
257       }
258 
259       // Start request
260       Request request = new Request(headers, requestData);
261       Output  output = new Output(receiver);
262       
263       bool exit = false;
264 
265       try { handler(request, output); }
266       
267       // Unhandled Exception escape from user code
268       catch (Exception e) 
269       { 
270          if (!output.headersSent) 
271             output.status = 501; 
272          
273          reservedLog(format("Uncatched exception: %s", e.msg)); 
274       }
275 
276       // Even worst.
277       catch (Throwable t) 
278       { 
279          if (!output.headersSent) 
280             output.status = 501; 
281           
282           reservedLog(format("Throwable: %s", t.msg)); 
283           exit = true;
284       }
285       finally 
286       { 
287          receiver.close();
288       }
289       
290       lock.unlock();
291 
292       if(exit == true) break;
293    }
294 }
295 
296 /// Write a formatted log
297 void reservedLog(T...)(T params)
298 {
299    import std.datetime : SysTime, Clock;
300    import std.conv : to;
301    import std.process : thisProcessID;
302    import std.stdio : write, writef, writeln, stdout;
303 
304    SysTime t = Clock.currTime;
305 
306    writef(
307       "%04d/%02d/%02d %02d:%02d:%02d.%s [%s] >>> ", 
308       t.year, t.month, t.day, t.hour,t.minute,t.second,t.fracSecs.split!"msecs".msecs, thisProcessID()
309    );
310 
311    foreach (p; params)
312       write(p.to!string, " ");
313    
314    writeln;
315    stdout.flush;
316 }
317 
318 /// A cookie
319 struct Cookie
320 {
321    string      name;       /// Cookie data
322    string      value;      /// ditto
323    string      path;       /// ditto
324    string      domain;     /// ditto
325 
326    DateTime    expire;     /// ditto
327 
328    bool        session     = true;  /// ditto  
329    bool        secure      = false; /// ditto
330    bool        httpOnly    = false; /// ditto
331 
332    /// Invalidate cookie
333    public void invalidate()
334    {
335       expire = DateTime(1970,1,1,0,0,0);
336    }
337 }
338 
339 /// A request from user
340 class Request 
341 { 
342 
343    /// HTTP methods
344    public enum Method
345 	{
346 		Get, ///
347       Post, ///
348       Head, ///
349       Delete, ///
350       Put, ///
351       Unknown = -1 ///
352 	}
353 	
354    @disable this();
355 
356    private this(string[string] headers, char[] requestData) 
357    {
358       import std.regex : match, ctRegex;
359       import std.uri : decodeComponent;
360 		import std..string : translate, split;
361 
362       // Reset values
363 		_header  = headers;
364       _get 	   = (typeof(_get)).init;
365       _post 	= (typeof(_post)).init;
366       _cookie  = (typeof(_cookie)).init;
367       _data 	= requestData;
368 
369 		// Read get params
370       if ("QUERY_STRING" in _header)
371          foreach(m; match(_header["QUERY_STRING"], ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g")))
372             _get[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent;
373 
374       // Read post params
375       if ("REQUEST_METHOD" in _header && _header["REQUEST_METHOD"] == "POST")
376          if(_data.length > 0 && split(_header["CONTENT_TYPE"].toLower(),";")[0] == "application/x-www-form-urlencoded")
377             foreach(m; match(_data, ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g")))
378                _post[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent;
379 
380       // Read cookies
381       if ("HTTP_COOKIE" in _header)
382          foreach(m; match(_header["HTTP_COOKIE"], ctRegex!("([^=]+)=([^;]+);? ?", "g")))
383             _cookie[m.captures[1].decodeComponent] = m.captures[2].decodeComponent;
384 
385    }
386 
387    ///
388    @nogc @property nothrow public const(char[]) data() const  { return _data; } 
389 	
390    ///
391    @nogc @property nothrow public const(string[string]) get() const { return _get; }
392    
393    ///
394    @nogc @property nothrow public const(string[string]) post()  const { return _post; }
395    
396    ///
397    @nogc @property nothrow public const(string[string]) header() const { return _header; } 
398    
399    ///
400    @nogc @property nothrow public const(string[string]) cookie() const { return _cookie; }  
401    
402 	///
403    @property public Method method() const
404 	{
405 		switch(_header["REQUEST_METHOD"].toLower())
406 		{
407 			case "get": return Method.Get;
408 			case "post": return Method.Post;
409 			case "head": return Method.Head;
410 			case "put": return Method.Put;
411 			case "delete": return Method.Delete;
412          default: return Method.Unknown;  
413 		}      
414 	}
415 
416    private char[] _data;
417    private string[string]  _get;
418    private string[string]  _post;
419    private string[string]  _header;
420 	private string[string]  _cookie;
421 }
422 
423 /// Your reply.
424 class Output
425 {
426 	private struct KeyValue
427 	{
428 		this (in string key, in string value) { this.key = key; this.value = value; }
429 		string key;
430 		string value;
431 	}
432 	
433    @disable this();
434 
435    private this(Socket socket)
436    {
437       _socket        = socket;
438       _status		   = 200;
439 		_headersSent   = false;
440    }
441 
442    /// You can add a http header. But you can't if body is already sent.
443 	public void addHeader(in string key, in string value) 
444    {
445       if (_headersSent) 
446          throw new Exception("Can't add/edit headers. Too late. Just sent.");
447 
448       _headers ~= KeyValue(key, value); 
449    }
450 
451 	/// Force sending of headers.
452 	public void sendHeaders()
453    {
454       if (_headersSent) 
455          throw new Exception("Can't resend headers. Too late. Just sent.");
456 
457       import std.uri : encode;
458 
459       bool has_content_type = false;
460       _socket.send(format("Status: %s\r\n", _status));
461 
462       // send user-defined headers
463       foreach(header; _headers)
464       {
465          _socket.send(format("%s: %s\r\n", header.key, header.value));
466          if (header.key.toLower() == "content-type") has_content_type = true;
467       }
468 
469       // Default content-type is text/html if not defined by user
470       if (!has_content_type)
471          _socket.send(format("content-type: text/html; charset=utf-8\r\n"));
472       
473       // If required, I add headers to write cookies
474       foreach(Cookie c; _cookies)
475       {
476 
477          _socket.send(format("Set-Cookie: %s=%s", c.name.encode(), c.value.encode()));
478    
479          if (!c.session)
480          {
481             string[] mm = ["", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
482             string[] dd = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
483 
484             string data = format("%s, %s %s %s %s:%s:%s GMT",
485                dd[c.expire.dayOfWeek], c.expire.day, mm[c.expire.month], c.expire.year, 
486                c.expire.hour, c.expire.minute, c.expire.second
487             );
488 
489             _socket.send(format("; Expires: %s", data));
490          }
491 
492          if (!c.path.length == 0) _socket.send(format("; path=%s", c.path));
493          if (!c.domain.length == 0) _socket.send(format("; domain=%s", c.domain));
494 
495          if (c.secure) _socket.send(format("; Secure"));
496          if (c.httpOnly) _socket.send(format("; HttpOnly"));
497 
498          _socket.send("\r\n");
499       }
500    
501       _socket.send("\r\n");
502       _headersSent = true;
503    }
504 	
505    /// You can set a cookie.  But you can't if body is already sent.
506    public void setCookie(Cookie c)
507    {
508       if (_headersSent) 
509          throw new Exception("Can't set cookies. Too late. Just sent.");
510       
511       _cookies ~= c;
512    }
513 	
514    /// Retrieve all cookies
515    @nogc @property nothrow public Cookie[]  cookies() 				{ return _cookies; }
516 	
517    /// Output status
518    @nogc @property nothrow public ulong 		status() 				{ return _status; }
519 	
520    /// Set response status. Default is 200 (OK)
521    @property public void 		               status(ulong status) 
522    {
523       if (_headersSent) 
524          throw new Exception("Can't set status. Too late. Just sent.");
525 
526       _status = status; 
527    }
528 
529    /**
530    * Syntax sugar to write data
531    * Example:
532    * --------------------
533    * output ~= "Hello world";
534    * --------------------
535    */ 
536 	public void opOpAssign(string op, T)(T data) if (op == "~")  { import std.conv : to; write(data.to!string); }
537 
538    /// Write data
539    public void write(string data) { import std..string : representation; write(data.representation); }
540    
541    /// Ditto
542    public void write(in void[] data) 
543    {
544       if (!_headersSent) 
545          sendHeaders(); 
546       
547       _socket.send(data); 
548    }
549    
550    /// Are headers already sent?
551    @nogc nothrow public bool headersSent() { return _headersSent; }
552 
553 	private bool			   _headersSent = false;
554 	private Cookie[]      	_cookies;
555 	private KeyValue[]  	   _headers;
556    private ulong           _status = 200;
557 		
558 	private Socket          _socket;
559 }