1 /*
2 Copyright (c) 2023-2024 Andrea Fontana
3 
4 Permission is hereby granted, free of charge, to any person
5 obtaining a copy of this software and associated documentation
6 files (the "Software"), to deal in the Software without
7 restriction, including without limitation the rights to use,
8 copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the
10 Software is furnished to do so, subject to the following
11 conditions:
12 
13 The above copyright notice and this permission notice shall be
14 included in all copies or substantial portions of the Software.
15 
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 OTHER DEALINGS IN THE SOFTWARE.
24 */
25 
26 /// Everything you need to communicate.
27 module serverino.interfaces;
28 
29 import std.conv : to;
30 import std.string : format, representation, indexOf, lastIndexOf, toLower, toStringz, strip;
31 import std.range : empty, assumeSorted;
32 import std.algorithm : map, canFind, splitter, startsWith;
33 import core.thread : Thread;
34 import std.datetime : SysTime, Clock, dur, Duration, DateTime;
35 import std.experimental.logger : log, warning, fatal, critical;
36 import std.socket : Address, Socket, SocketShutdown, socket_t, SocketOptionLevel, SocketOption, Linger, AddressFamily;
37 
38 import serverino.databuffer;
39 import serverino.common;
40 import core.stdc.ctype;
41 
42 /++ A cookie. Use `Cookie("key", "value")` to create a cookie. You can chain methods.
43 + ---
44 + auto cookie = Cookie("name", "value").path("/").domain("example.com").secure().maxAge(1.days);
45 + output.setCookie(cookie);
46 + ---
47 +/
48 struct Cookie
49 {
50    /// Cookie SameSite flag
51    enum SameSite
52    {
53       NotSet = "NotSet",   /// SameSite flag will not be set.
54       Strict = "Strict",   /// Strict value. Cookie will be sent only if the request is from the same site.
55       Lax = "Lax",         /// Lax value. Cookie will be sent only if the request is from the same site, except for links from external sites.
56       None = "None"        /// None value. Cookie will be sent always. Secure flag will be set.
57    }
58 
59    @disable this();
60 
61    /// Build a cookie with name and value
62    @safe @nogc nothrow this(string name, string value) { _name = name; _value = value; _valid = true; }
63 
64    /// Set cookie path
65    @safe @nogc nothrow @property ref Cookie path(string path) scope return { _path = path; return this; }
66 
67    /// Set cookie domain
68    @safe @nogc nothrow @property ref Cookie domain(string domain) scope return { _domain = domain; return this; }
69 
70    /// Set cookie secure flag. This cookie will be sent only thru https.
71    @safe @nogc nothrow @property ref Cookie secure(bool secure = true) scope return { _secure = secure; return this; }
72 
73    /// Set cookie httpOnly flag. This cookie will not be accessible from javascript.
74    @safe @nogc nothrow @property ref Cookie httpOnly(bool httpOnly = true) scope return { _httpOnly = httpOnly; return this; }
75 
76    /// Set cookie expire time. It overrides maxAge.
77    @safe @nogc nothrow @property ref Cookie expire(SysTime expire) scope return { _maxAge = Duration.zero; _expire = expire; return this; }
78 
79    /// Set cookie max age. It overrides expire.
80    @safe @nogc nothrow @property ref Cookie maxAge(Duration maxAge) scope return { _expire = SysTime.init; _maxAge = maxAge; return this; }
81 
82    /// Set cookie SameSite flag
83    @safe @nogc nothrow @property ref Cookie sameSite(SameSite sameSite) scope return { _sameSite = sameSite; return this; }
84 
85    /// Invalidate cookie. It will be deleted from browser on output.setCookie() request.
86    @safe @nogc nothrow @property ref Cookie invalidate() scope return
87    {
88       _value = string.init;
89       _expire = SysTime.init;
90       _maxAge = Duration.min;
91       return this;
92    }
93 
94    private:
95 
96    string   _name;
97    string   _value;
98    string   _path;
99    string   _domain;
100    bool     _secure     = false;
101    bool     _httpOnly   = false;
102    SysTime  _expire     = SysTime.init;
103    Duration _maxAge     = Duration.zero;
104    SameSite _sameSite   = SameSite.NotSet;
105 
106    bool _valid = false;
107 }
108 
109 /// HTTP version used in request
110 enum HttpVersion
111 {
112    HTTP10 = "HTTP/1.0",
113    HTTP11 = "HTTP/1.1"
114 }
115 
116 /++ A request from user. Do not store ref to this struct anywhere.
117 + ---
118 + void handler(Request request, Output output)
119 + {
120 +    info("You asked for ", request.uri, " with method ", request.method, " and params ", request.get.data);
121 + }
122 + ---
123 +/
124 struct Request
125 {
126    /// Print request data
127    @safe string dump()(bool html = true) const
128    {
129       import std.string : replace;
130       string d = toString();
131 
132       if (html) d = `<img src="data:image/png;base64,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" alt="" style="display:flex;margin-right:auto; text-align: center;margin-bottom:10px;"><pre style="width:auto;border-radius:4px;padding:10px;background:#eff1ecff;overflow-x:auto">` ~ d.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") ~ "</pre>";
133 
134       return d;
135    }
136 
137    /// ditto
138    @safe string toString()() const
139    {
140 
141       string output;
142       output ~= format("Serverino %s.%s.%s\n\n", SERVERINO_MAJOR, SERVERINO_MINOR, SERVERINO_REVISION);
143       output ~= format("Worker: #%s\n", worker.to!string);
144       output ~= format("Build ID: %s\n", buildId);
145       output ~= "\n";
146       output ~= "Request:\n";
147       output ~= format(" • Method: %s\n", method.to!string);
148       output ~= format(" • Uri: %s\n", uri);
149 
150       if (!_internal._user.empty)
151          output ~= format(" • Authorization: user => `%s` password => `%s`\n",_internal._user,_internal._password.map!(x=>'*'));
152 
153       if (!get.data.empty)
154       {
155          output ~= "\nQuery Params:\n";
156          foreach(k,v; get.data)
157          {
158             output ~= format(" • %s => %s\n", k, v);
159          }
160       }
161 
162       if (!body.data.empty)
163          output ~= "\nContent-type: " ~ body.contentType ~ " (size: %s bytes)\n".format(body.data.length);
164 
165       if (!post.data.empty)
166       {
167          output ~= "\nPost Params:\n";
168          foreach(k,v; post.data)
169          {
170             output ~= format(" • %s => %s\n", k, v);
171          }
172       }
173 
174       if (!form.data.empty)
175       {
176          output ~= "\nForm Data:\n";
177          foreach(k,v; form.data)
178          {
179             import std.file : getSize;
180 
181             if (v.isFile) output ~= format(" • `%s` (content-type: %s, size: %s bytes, path: %s)\n", k, v.contentType, getSize(v.path), v.path);
182             else output ~= format(" • `%s` (content-type: %s, size: %s bytes)\n", k, v.contentType, v.data.length);
183          }
184       }
185 
186       if (!cookie.data.empty)
187       {
188          output ~= "\nCookies:\n";
189          foreach(k,v; cookie.data)
190          {
191             output ~= format(" • %s => %s\n", k, v);
192          }
193       }
194 
195       output ~= "\nHeaders:\n";
196       foreach(k,v; header.data)
197       {
198          output ~= format(" • %s => %s\n", k, v);
199       }
200 
201       return output;
202    }
203 
204    /// HTTP methods
205    public enum Method
206 	{
207 		Get, /// GET
208       Post, /// POST
209       Head, /// HEAD
210       Put, /// PUT
211       Delete, /// DELETE
212       Connect, /// CONNECT
213       Options, /// OPTIONS
214       Patch, /// PATCH
215       Trace, /// TRACE
216       Unknown = -1 /// Unknown method
217 	}
218 
219    /++ Params from query string
220     + ---
221     + request.get.has("name"); // true for http://localhost:8000/page?name=hello
222     + request.get.read("name", "blah") // returns "Karen" for http://localhost:8000/page?name=Karen
223     + request.get.read("name", "blah") // returns "blah" for http://localhost:8000/page?test=123
224     + ---
225     +/
226    @safe @nogc @property nothrow public auto get() const { return SafeAccess!string(_internal._get); }
227 
228    /++ Params from post if content-type is "application/x-www-form-urlencoded"
229    + ---
230    + request.post.has("name");
231    + request.post.read("name", "Anonymous") // returns "Anonymous" if name was not posted
232    + ---
233    +/
234    @safe @nogc @property nothrow public auto post()  const { return SafeAccess!string(_internal._post); }
235 
236    /++
237     + The fields from a form. Only if content-type is "multipart/form-data".
238     + ---
239     + FormData fd = request.form.read("form_id");
240     + if (fd.isFile)
241     + {
242     +   // We have a file attached
243     +   info("File name: ", fd.filename);
244     +   info("File path: ", fd.path);
245     + }
246     + else
247     + {
248     +   // We have data inlined
249     +   into("Content-Type: ", fd.contentType, " Size: ", fd.data.length, " bytes")
250     +   info("Data: ", fd.data);
251     + }
252     ---
253     +/
254    @safe @nogc @property nothrow public auto form() const { return SafeAccess!FormData(_internal._form); }
255 
256    /++ Raw posted data
257    ---
258    import std.experimental.logger;
259    info("Content-Type: ", request.body.contentType, " Size: ", request.body.data.length, " bytes");
260    ---
261    +/
262    @safe @nogc @property nothrow public auto body() const { import std.typecons: tuple; return tuple!("data", "contentType")(_internal._data,_internal._postDataContentType); }
263 
264    /++
265    Http headers, always lowercase
266    ---
267    request.header.read("user-agent");
268    ---
269    +/
270    @safe @nogc @property nothrow public auto header() const { return SafeAccess!(string)(_internal._header); }
271 
272    /// Cookies received from user
273    @safe @nogc @property nothrow public auto cookie() const { return SafeAccess!string(_internal._cookie); }
274 
275    /// The uri requested by user
276    @safe @nogc @property nothrow public const(string) uri() const { return _internal._uri; }
277 
278    /// Which worker is processing this request?
279    @safe @nogc @property nothrow public auto worker() const { return _internal._worker; }
280 
281    /// The host that received the request
282    @safe @nogc @property nothrow public auto host() const { return _internal._host; }
283 
284    /// Use at your own risk! Raw data from user.
285    @safe @nogc @property nothrow public auto requestLine() const { return _internal._rawRequestLine; }
286 
287    /// Basic http authentication user. Safe only if sent thru https!
288    @safe @nogc @property nothrow public auto user() const { return _internal._user; }
289 
290    /// Basic http authentication password. Safe only if sent thru https!
291    @safe @nogc @property nothrow public auto password() const { return _internal._password; }
292 
293    /// The sequence of endpoints called so far
294    @safe @nogc @property nothrow public auto route() const { return _internal._route; }
295 
296    static package string simpleNotSecureCompileTimeHash(string seed = "")
297    {
298       // Definetely not a secure hash function
299       // Created just to give a unique ID to each build.
300 
301       char[16] h = "SimpleNotSecure!";
302       char[32] h2;
303 
304       auto  s = (seed ~ "_" ~  __TIMESTAMP__).representation;
305       static immutable hex = "0123456789abcdef".representation;
306 
307       ulong sc = 104_059;
308 
309       foreach_reverse(idx, c; s)
310       {
311          sc += 1+(cast(ushort)c);
312          sc *= 79_193;
313          h[15-idx%16] ^= cast(char)(sc%255);
314       }
315 
316       foreach(idx, c; h)
317       {
318          sc += 1+(cast(ushort)c);
319          sc *= 96_911;
320          h2[idx*2] = hex[(sc%256)/16];
321          h2[idx*2+1]= hex[(sc%256)%16];
322       }
323 
324       return h2.dup;
325    }
326 
327    /// Every time you compile the app this value will change
328    enum buildId = simpleNotSecureCompileTimeHash();
329 
330 	/// HTTP method
331    @safe @property @nogc nothrow public Method method() const
332 	{
333 		switch(_internal._method)
334 		{
335 			case "GET": return Method.Get; /// GET
336 			case "POST": return Method.Post; /// POST
337 			case "HEAD": return Method.Head; /// HEAD
338 			case "PUT": return Method.Put; /// PUT
339 			case "DELETE": return Method.Delete; /// DELETE
340          case "CONNECT": return Method.Connect; /// CONNECT
341          case "OPTIONS": return Method.Options; /// OPTIONS
342          case "PATCH": return Method.Patch; /// PATCH
343          case "TRACE": return Method.Trace; /// TRACE
344          default: return Method.Unknown; /// Unknown
345 		}
346 	}
347 
348 
349    package enum ParsingStatus
350    {
351       OK = 0,                 ///
352       MaxUploadSizeExceeded,  ///
353       InvalidBody,            ///
354       InvalidRequest          ///
355    }
356 
357    /++ Simple structure to safely access data from an associative array.
358    + ---
359    + // request.cookie returns a SafeAccess!string
360    + // get a cookie named "user", default to "anonymous"
361    + auto user = request.cookie.read("user", "anonymous");
362    +
363    + // Access the underlying AA
364    + auto data = request.cookie.data;
365    + foreach(k,v; data) info(k, " => ", v);
366    + ---
367    +/
368    struct SafeAccess(T)
369    {
370       public:
371 
372       /++
373          Read a value. Return defaultValue if k does not exist.
374          ---------
375          request.cookie.read("user", "anonymous");
376          ---------
377       +/
378       @safe @nogc nothrow auto read(string key, T defaultValue = T.init) const
379       {
380          auto v = key in _data;
381 
382          if (v == null) return defaultValue;
383          return *v;
384       }
385 
386       /// Check if value exists
387       @safe @nogc nothrow bool has(string key) const
388       {
389          return (key in _data) != null;
390       }
391 
392       /// Return the underlying AA
393       @safe @nogc nothrow @property auto data() const { return _data; }
394 
395       auto toString() const { return _data.to!string; }
396 
397       private:
398 
399       @safe @nogc nothrow private this(const ref T[string] data) { _data = data; }
400       const T[string] _data;
401    }
402 
403    /++ Data sent through multipart/form-data.
404    +/
405    public struct FormData
406    {
407       string name;         /// Form field name
408 
409       string contentType;  /// Content type
410       char[] data;         /// Data, if inlined (empty if `isFile() == true`)
411 
412       string filename;     /// We have a file attached. Its name.
413       string path;         /// If we have a file attached, here it is saved.
414 
415       /// Is it a file or is data inlined?
416       @safe @nogc nothrow @property bool isFile() const { return !filename.empty; }
417    }
418 
419    package struct RequestImpl
420    {
421       void process()
422       {
423          import std.algorithm : splitter;
424          import std.regex : match, ctRegex;
425          import std.uri : decodeComponent;
426          import std.string : translate, split, strip;
427          import std.process : thisProcessID;
428 
429          static string myPID;
430          if (myPID.length == 0) myPID = thisProcessID().to!string;
431 
432          foreach(ref h; _rawHeaders.splitter("\r\n"))
433          {
434             auto first = h.indexOf(":");
435             if (first < 0) continue;
436             _header[h[0..first]] = h[first+1..$];
437          }
438 
439          _worker = myPID;
440 
441          if ("host" in _header) _host = _header["host"];
442 
443          // Read get params
444          if (!_rawQueryString.empty)
445             foreach(m; match(_rawQueryString, ctRegex!("([^=&]*)(?:=([^&]*))?&?", "g")))
446                _get[m.captures[1].decodeComponent] = translate(m.captures[2],['+':' ']).decodeComponent;
447 
448          // Read post params
449          try
450          {
451             import std.algorithm : filter, endsWith, countUntil;
452             import std.range : drop, takeOne;
453             import std.array : array;
454 
455             if (_method == "POST")
456             {
457                auto contentType = "application/octet-stream";
458 
459                if ("content-type" in _header && !_header["content-type"].empty)
460                   contentType = _header["content-type"];
461 
462                auto cSplitted = contentType.splitter(";");
463 
464                _postDataContentType = cSplitted.front.toLower.strip;
465                cSplitted.popFront();
466 
467                if (_postDataContentType == "application/x-www-form-urlencoded")
468                {
469                   import std.stdio;
470 
471                   // Ok that's easy...
472                   foreach(m; match(_data, ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g")))
473                      _post[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent;
474                }
475                else if (_postDataContentType == "multipart/form-data")
476                {
477                   // The hard way
478                   string boundary;
479 
480                   // Usually they declare the boundary
481                   if (!cSplitted.empty)
482                   {
483                      boundary = cSplitted.front.strip;
484 
485                      if (boundary.startsWith("boundary=")) boundary = boundary[9..$].strip().strip(`"`);
486                      else boundary = string.init;
487                   }
488 
489                   // Sometimes they write it on the first line.
490                   if (boundary.empty)
491                   {
492                      auto lines = _data.splitter("\r\n").filter!(x => !x.empty).takeOne;
493 
494                      if (!lines.empty)
495                      {
496                         auto firstLine = lines.front;
497 
498                         if (firstLine.length < 512 && firstLine.startsWith("--"))
499                            boundary = firstLine[2..$].to!string;
500                      }
501                   }
502 
503                   if (!boundary.empty)
504                   {
505                      bool lastBoundary = false;
506                      foreach(chunk; _data.splitter("--" ~ boundary))
507                      {
508                         // All chunks must end with \r\n
509                         if (!chunk.endsWith("\r\n"))
510                            continue;
511 
512                         // The last one is --boundary--
513                         chunk = chunk[0..$-2];
514                         if (chunk.length == 2 && chunk == "--")
515                         {
516                            lastBoundary = true;
517                            break;
518                         }
519 
520                         // All chunks must start with \r\n
521                         if (!chunk.startsWith("\r\n"))
522                            continue;
523 
524                         chunk = chunk[2..$];
525 
526                         bool done = false;
527                         string content_disposition;
528                         string content_type = "text/plain";
529 
530                         // "Headers"
531                         while(true)
532                         {
533 
534                            auto nextLine = chunk.countUntil("\n");
535                            auto line = chunk[0..nextLine];
536 
537                            // All lines must end with \r\n
538                            if (!line.endsWith("\r"))
539                               break;
540 
541                            line = line[0..$-1];
542 
543                            if (line == "\r")
544                            {
545                               done = true;
546                               break;
547                            }
548                            else if (line.toLower.startsWith("content-disposition:"))
549                               content_disposition = line.to!string;
550                            else if (line.toLower.startsWith("content-type:"))
551                               content_type = line.to!string["content-type:".length..$].strip;
552                            else break;
553 
554                            if (nextLine + 1 >= chunk.length)
555                               break;
556 
557                            chunk = chunk[nextLine+1 .. $];
558                         }
559 
560                         // All chunks must start with \r\n
561                         if (!chunk.startsWith("\r\n")) continue;
562                         chunk = chunk[2..$];
563 
564                         if (content_disposition.empty) continue;
565 
566                         // content-disposition fields
567                         auto form_data_raw = content_disposition.splitter(";").drop(1).map!(x=>x.split("=")).array;
568                         string[string] form_data;
569 
570                         foreach(f; form_data_raw)
571                         {
572                            if (f.length > 1)
573                            {
574                               auto k = f[0].strip;
575                               auto v = f[1].strip;
576 
577                               if (v.length < 2) continue;
578                               form_data[k] = v[1..$-1];
579                            }
580                         }
581 
582                         if ("name" !in form_data) continue;
583 
584                         FormData fd;
585                         fd.name = form_data["name"];
586                         fd.contentType = content_type;
587 
588                         if ("filename" !in form_data) fd.data = chunk.to!(char[]);
589                         else
590                         {
591                            fd.filename = form_data["filename"];
592                            import core.atomic : atomicFetchAdd;
593                            import std.path : extension, buildPath;
594                            import std.file : tempDir;
595 
596 
597                            string now = Clock.currTime.toUnixTime.to!string;
598                            string uploadId = "%05d".format(atomicFetchAdd(_uploadId, 1));
599                            string path = tempDir.buildPath("upload_%s_%s_%s%s".format(now, myPID, uploadId, extension(fd.filename)));
600 
601                            fd.path = path;
602 
603                            import std.file : write;
604                            write(path, chunk);
605                         }
606 
607                         _form[fd.name] = fd;
608 
609                      }
610 
611                      if (!lastBoundary)
612                      {
613                         debug warning("Can't parse multipart/form-data content");
614                         _parsingStatus = ParsingStatus.InvalidBody;
615 
616                         // Something went wrong with parsing, we ignore data.
617                         clearFiles();
618                         _post = typeof(_post).init;
619                         _form = typeof(_form).init;
620                      }
621 
622                   }
623                }
624             }
625          }
626          catch (Exception e) { _parsingStatus = ParsingStatus.InvalidBody; }
627 
628          // Read cookies
629          if ("cookie" in _header)
630             foreach(m; match(_header["cookie"], ctRegex!("([^=]+)=([^;]+)?;? ?", "g")))
631                _cookie[m.captures[1].decodeComponent] = m.captures[2].decodeComponent;
632 
633          if ("authorization" in _header)
634          {
635             import std.base64 : Base64, Base64Exception;
636             import std.string : indexOf;
637             auto auth = _header["authorization"];
638 
639             if (auth.length > 6 && auth[0..6].toLower == "basic ")
640             {
641                try
642                {
643                   auth = (cast(char[])Base64.decode(auth[6..$])).to!string;
644                   auto delim = auth.indexOf(":");
645 
646                      if (delim < 0) _user = auth;
647                      else
648                      {
649                         _user = auth[0..delim];
650 
651                         if (delim < auth.length-1)
652                            _password = auth[delim+1..$];
653                      }
654 
655                }
656                catch(Base64Exception e)
657                {
658                   _user = string.init;
659                   _password = string.init;
660                   debug warning("Authorization header ignored. Error decoding base64.");
661                }
662             }
663          }
664 
665 
666       }
667 
668       void clearFiles() {
669          import std.file : remove, exists;
670          foreach(f; _form)
671             try {if (exists(f.path)) remove(f.path); } catch(Exception e) { }
672       }
673 
674       ~this() { clearFiles(); }
675 
676 
677       char[] _data;
678       string[string]  _get;
679       string[string]  _post;
680       string[string]  _header;
681       string[string]  _cookie;
682 
683       string _uri;
684       string _method;
685       string _host;
686       string _postDataContentType;
687       string _worker;
688       string _user;
689       string _password;
690       size_t _uploadId;
691 
692       string _rawQueryString;
693       string _rawHeaders;
694       string _rawRequestLine;
695 
696       string[]  _route;
697 
698       HttpVersion _httpVersion;
699 
700       FormData[string]   _form;
701       ParsingStatus      _parsingStatus = ParsingStatus.OK;
702 
703       size_t    _requestId;
704 
705       void clear()
706       {
707          clearFiles();
708 
709          _form    = null;
710          _data    = null;
711          _get     = null;
712          _post    = null;
713          _header  = null;
714          _cookie  = null;
715          _uri     = string.init;
716 
717          _method        = string.init;
718          _host          = string.init;
719          _user          = string.init;
720          _password      = string.init;
721          _httpVersion   = HttpVersion.HTTP10;
722 
723          _rawQueryString   = string.init;
724          _rawHeaders       = string.init;
725          _rawRequestLine   = string.init;
726 
727          _postDataContentType = string.init;
728 
729          _parsingStatus = ParsingStatus.OK;
730 
731          _route.length = 0;
732          _route.reserve(10);
733       }
734    }
735 
736    package RequestImpl* _internal;
737 }
738 
739 /++ A response to user. Default content-type is "text/html".
740 + ---
741 + // Set status code to 404
742 + output.status = 404
743 +
744 + // Send a response. Same as: output.write("Sorry, page not found.");
745 + output ~= "Sorry, page not found.";
746 + ---
747 +/
748 struct Output
749 {
750 
751 	public:
752 
753    /// Override timeout for this request
754    @safe void setTimeout(Duration max) {  _internal._timeout = max; }
755 
756    /++
757    + Add a http header.
758    + You can't set `content-length`, `status` or `transfer-encoding` headers. They are managed by serverino internally.
759    + ---
760    + // Set content-type to json, default is text/html
761    + output.addHeader("content-type", "application/json");
762    + output.addHeader("expire", 3.days);
763    + ---
764    +/
765 	@safe void addHeader(in string key, in string value)
766    {
767       string k = key.toLower;
768 
769       debug if (["content-length", "status", "transfer-encoding"].canFind(k))
770       {
771          warning("You can't set `", key, "` header. It's managed by serverino internally.");
772          if (k == "status") warning("Use `output.status = XXX` instead.");
773          return;
774       }
775 
776       _internal._dirty = true;
777       _internal._headers ~= KeyValue(k, value);
778    }
779 
780    /// Ditto
781    @safe void addHeader(in string key, in Duration dur) { addHeader(key, Clock.currTime + dur); }
782 
783    /// Ditto
784    @safe void addHeader(in string key, in SysTime time) { addHeader(key, toHTTPDate(time)); }
785 
786    /// You can reply with a file. Automagical mime-type detection.
787    bool serveFile(const string path, bool guessMime = true)
788    {
789       _internal._dirty = true;
790 
791       import std.file : exists, getSize, isFile;
792       import std.path : extension, baseName;
793       import std.stdio : File;
794 
795       if (!exists(path) || !isFile(path))
796       {
797          warning("Trying to serve `", baseName(path) ,"`, but it doesn't exists.");
798          return false;
799       }
800 
801       size_t fs = path.getSize().to!size_t;
802       _internal._headers ~= KeyValue("content-length", fs.to!string);
803 
804       if (!_internal._headers.canFind!(x=>x.key == "content-type"))
805       {
806          string header = "application/octet-stream";
807 
808          if (guessMime)
809          {
810             static immutable mimes =
811             [
812                ".html" : "text/html", ".htm" : "text/html", ".shtml" : "text/html", ".css" : "text/css", ".xml" : "text/xml",
813                ".gif" : "image/gif", ".jpeg" : "image/jpeg", ".jpg" : "image/jpeg", ".js" : "application/javascript",
814                ".atom" : "application/atom+xml", ".rss" : "application/rss+xml", ".mml" : "text/mathml", ".txt" : "text/plain",
815                ".jad" : "text/vnd.sun.j2me.app-descriptor", ".wml" : "text/vnd.wap.wml", ".htc" : "text/x-component",
816                ".png" : "image/png", ".tif" : "image/tiff", ".tiff" : "image/tiff", ".wbmp" : "image/vnd.wap.wbmp",
817                ".ico" : "image/x-icon", ".jng" : "image/x-jng", ".bmp" : "image/x-ms-bmp", ".svg" : "image/svg+xml",
818                ".svgz" : "image/svg+xml", ".webp" : "image/webp", ".woff" : "application/font-woff",
819                ".jar" : "application/java-archive", ".war" : "application/java-archive", ".ear" : "application/java-archive",
820                ".json" : "application/json", ".hqx" : "application/mac-binhex40", ".doc" : "application/msword",
821                ".pdf" : "application/pdf", ".ps" : "application/postscript", ".eps" : "application/postscript",
822                ".ai" : "application/postscript", ".rtf" : "application/rtf", ".m3u8" : "application/vnd.apple.mpegurl",
823                ".xls" : "application/vnd.ms-excel", ".eot" : "application/vnd.ms-fontobject",
824                ".ppt" : "application/vnd.ms-powerpoint", ".wmlc" : "application/vnd.wap.wmlc",
825                ".kml" : "application/vnd.google-earth.kml+xml", ".kmz" : "application/vnd.google-earth.kmz",
826                ".7z" : "application/x-7z-compressed", ".cco" : "application/x-cocoa",
827                ".jardiff" : "application/x-java-archive-diff", ".jnlp" : "application/x-java-jnlp-file",
828                ".run" : "application/x-makeself", ".pl" : "application/x-perl", ".pm" : "application/x-perl",
829                ".prc" : "application/x-pilot", ".pdb" : "application/x-pilot", ".rar" : "application/x-rar-compressed",
830                ".rpm" : "application/x-redhat-package-manager", ".sea" : "application/x-sea",
831                ".swf" : "application/x-shockwave-flash", ".sit" : "application/x-stuffit", ".tcl" : "application/x-tcl",
832                ".tk" : "application/x-tcl", ".der" : "application/x-x509-ca-cert", ".pem" : "application/x-x509-ca-cert",
833                ".crt" : "application/x-x509-ca-cert", ".xpi" : "application/x-xpinstall", ".xhtml" : "application/xhtml+xml",
834                ".xspf" : "application/xspf+xml", ".zip" : "application/zip", ".bin" : "application/octet-stream",
835                ".exe" : "application/octet-stream", ".dll" : "application/octet-stream", ".deb" : "application/octet-stream",
836                ".dmg" : "application/octet-stream", ".iso" : "application/octet-stream", ".img" : "application/octet-stream",
837                ".msi" : "application/octet-stream", ".msp" : "application/octet-stream", ".msm" : "application/octet-stream",
838                ".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
839                ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
840                ".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".mid" : "audio/midi",
841                ".midi" : "audio/midi", ".kar" : "audio/midi", ".mp3" : "audio/mpeg", ".ogg" : "audio/ogg", ".m4a" : "audio/x-m4a",
842                ".ra" : "audio/x-realaudio", ".3gpp" : "video/3gpp", ".3gp" : "video/3gpp", ".ts" : "video/mp2t", ".mp4" : "video/mp4",
843                ".mpeg" : "video/mpeg", ".mpg" : "video/mpeg", ".mov" : "video/quicktime", ".webm" : "video/webm", ".flv" : "video/x-flv",
844                ".m4v" : "video/x-m4v", ".mng" : "video/x-mng", ".asx" : "video/x-ms-asf", ".asf" : "video/x-ms-asf",
845                ".wmv" : "video/x-ms-wmv", ".avi" : "video/x-msvideo"
846             ];
847 
848             if (path.extension in mimes)
849                header = mimes[path.extension];
850          }
851 
852          addHeader("content-type", header);
853       }
854 
855       ubyte[] buffer;
856       buffer.length = fs;
857       File toSend = File(path, "r");
858 
859       auto bytesRead = toSend.rawRead(buffer);
860 
861       if (bytesRead.length != fs)
862       {
863          sendData("HTTP/1.0 500 Internal server error\r\n");
864          return false;
865       }
866 
867       //sendHeaders();
868       sendData(bytesRead);
869       return true;
870     }
871 
872 
873    /++ Add or edit a cookie.
874    + To delete a cookie, use cookie.invalidate() and then setCookie(cookie)
875    +/
876    @safe void setCookie(Cookie c)
877    {
878       _internal._dirty = true;
879 
880       if (!c._valid)
881          throw new Exception("Invalid cookie. Please use Cookie(name, value) to create a valid cookie.");
882 
883      _internal._cookies ~= c;
884    }
885 
886 
887    /// Read status.
888    @safe @nogc @property nothrow ushort status() 	{ return _internal._status; }
889 
890    /// Set response status. 200 by default.
891    @safe @property void status(ushort status)
892    {
893       _internal._dirty = true;
894      _internal._status = status;
895    }
896 
897    /**
898    * Syntax sugar. Easier way to write output.
899    * Example:
900    * --------------------
901    * output ~= "Hello world";
902    * --------------------
903    */
904 	void opOpAssign(string op, T)(T data) if (op == "~")  { write(data.to!string); }
905 
906    /**
907    * Mute/unmute output. If false, serverino will not send any data to user.
908    * --------------------
909    * output = false; // Mute the output.
910    * output ~= "Hello world"; // Serverino will not send this to user.
911    * --------------------
912    */
913    void opAssign(in bool v) {
914       _internal._sendBody = v;
915    }
916 
917    /// Write data to output. You can write as many times as you want.
918    @safe void write(string data = string.init) { write(data.representation); }
919 
920    /// Ditto
921    @safe void write(in void[] data)
922    {
923       _internal._dirty = true;
924       sendData(data);
925    }
926 
927    struct KeyValue
928 	{
929 		@safe this (in string key, in string value) { this.key = key; this.value = value; }
930 		string key;
931 		string value;
932 	}
933 
934    package:
935 
936    @safe void sendData(const string data) { sendData(data.representation); }
937    @safe void sendData(bool force = false)(const void[] data)
938    {
939       _internal._dirty = true;
940       _internal._sendBuffer.append(cast(const char[])data);
941    }
942 
943    @safe static string toHTTPDate(SysTime t) {
944       string[] mm = ["", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
945       string[] dd = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
946 
947       SysTime gmt = t.toUTC();
948 
949       return format("%s, %s %s %s %s:%s:%s GMT",
950          dd[gmt.dayOfWeek], gmt.day, mm[gmt.month], gmt.year,
951          gmt.hour, gmt.minute, gmt.second
952       );
953    }
954 
955    struct OutputImpl
956    {
957       Cookie[]        _cookies;
958       KeyValue[]  	 _headers;
959       bool            _keepAlive;
960       string          _httpVersion;
961       ushort          _status;
962       Duration        _timeout;
963       bool            _dirty;
964       size_t          _requestId;
965       DataBuffer!char _sendBuffer;
966       DataBuffer!char _headersBuffer;
967       string          _buffer;
968       Socket          _channel;
969       bool            _flushed;
970       bool            _sendBody;
971 
972 
973       @safe void buildHeaders()
974       {
975          import std.uri : encodeComponent;
976          import std.array : appender;
977 
978          _headersBuffer.reserve(1024, true);
979          _headersBuffer.clear();
980 
981          immutable string[short] StatusCode =
982          [
983             200: "OK", 201 : "Created", 202 : "Accepted", 203 : "Non-Authoritative Information", 204 : "No Content", 205 : "Reset Content", 206 : "Partial Content",
984 
985             300 : "Multiple Choices", 301 : "Moved Permanently", 302 : "Found", 303 : "See Other", 304 : "Not Modified", 305 : "Use Proxy", 307 : "Temporary Redirect",
986 
987             400 : "Bad Request", 401 : "Unauthorized", 402 : "Payment Required", 403 : "Forbidden", 404 : "Not Found", 405 : "Method Not Allowed",
988             406 : "Not Acceptable", 407 : "Proxy Authentication Required", 408 : "Request Timeout", 409 : "Conflict", 410 : "Gone",
989             411 : "Lenght Required", 412 : "Precondition Failed", 413 : "Request Entity Too Large", 414 : "Request-URI Too Long", 415 : "Unsupported Media Type",
990             416 : "Requested Range Not Satisfable", 417 : "Expectation Failed", 422 : "Unprocessable Content",
991 
992             500 : "Internal Server Error", 501 : "Not Implemented", 502 : "Bad Gateway", 503 : "Service Unavailable", 504 : "Gateway Timeout", 505 : "HTTP Version Not Supported"
993          ];
994 
995          string statusDescription;
996 
997          immutable item = _status in StatusCode;
998          if (item != null) statusDescription = *item;
999          else statusDescription = "Unknown";
1000 
1001          bool has_content_type = false;
1002          _headersBuffer.append(format("%s %s %s\r\n", _httpVersion, _status, statusDescription));
1003 
1004          if (!_keepAlive) _headersBuffer.append("connection: close\r\n");
1005          else _headersBuffer.append("connection: keep-alive\r\n");
1006 
1007          // send user-defined headers
1008          foreach(const ref header;_headers)
1009          {
1010             if (!_sendBody && (header.key == "content-length" || header.key == "transfer-encoding"))
1011                continue;
1012 
1013             _headersBuffer.append(format("%s: %s\r\n", header.key, header.value));
1014             if (header.key == "content-type") has_content_type = true;
1015          }
1016 
1017          if (!_sendBody)
1018             _headersBuffer.append(format("content-length: 0\r\n"));
1019          else
1020          {
1021             _headersBuffer.append("content-length: ");
1022             _headersBuffer.append(_sendBuffer.length.to!string);
1023             _headersBuffer.append("\r\n");
1024          }
1025 
1026          // Default content-type is text/html if not defined by user
1027          if (!has_content_type && _sendBody)
1028             _headersBuffer.append(format("content-type: text/html;charset=utf-8\r\n"));
1029 
1030          // If required, I add headers to write cookies
1031          foreach(Cookie c;_cookies)
1032          {
1033             _headersBuffer.append(format("set-cookie: %s=%s", encodeComponent(c._name), encodeComponent(c._value)));
1034 
1035             if (c._maxAge != Duration.zero)
1036             {
1037                if (c._maxAge.isNegative) _headersBuffer.append("; Max-Age=-1");
1038                else _headersBuffer.append(format("; Max-Age=%s", c._maxAge.total!"seconds"));
1039             }
1040             else if (c._expire != SysTime.init)
1041             {
1042                _headersBuffer.append(format("; Expires=%s",  Output.toHTTPDate(c._expire)));
1043             }
1044 
1045             if (!c._path.length == 0) _headersBuffer.append(format("; path=%s", c._path.encodeComponent()));
1046             if (!c._domain.length == 0) _headersBuffer.append(format("; domain=%s", c._domain.encodeComponent()));
1047 
1048             if (c._sameSite != Cookie.SameSite.NotSet)
1049             {
1050                if (c._sameSite == Cookie.SameSite.None) c._secure = true;
1051                _headersBuffer.append(format("; SameSite=%s", c._sameSite.to!string));
1052             }
1053 
1054             if (c._secure) _headersBuffer.append(format("; Secure"));
1055             if (c._httpOnly) _headersBuffer.append(format("; HttpOnly"));
1056 
1057             _headersBuffer.append("\r\n");
1058          }
1059 
1060          _headersBuffer.append("\r\n");
1061       }
1062 
1063       void clear()
1064       {
1065          // HACK
1066          _timeout = 0.dur!"seconds";
1067          _httpVersion = HttpVersion.HTTP10;
1068          _dirty = false;
1069          _status = 200;
1070          _cookies = null;
1071          _headers = null;
1072          _keepAlive = false;
1073          _flushed = false;
1074          _headersBuffer.clear();
1075          _sendBuffer.clear();
1076          _sendBody = true;
1077       }
1078    }
1079 
1080    OutputImpl* _internal;
1081 }
1082