1 /+
2     This file is part of Reloaded Vibes.
3     Copyright (c) 2019  0xEAB
4 
5     Distributed under the Boost Software License, Version 1.0.
6        (See accompanying file LICENSE_1_0.txt or copy at
7              https://www.boost.org/LICENSE_1_0.txt)
8  +/
9 module reloadedvibes.server;
10 
11 import std.file : exists;
12 import std.datetime : dur;
13 import std.stdio : File;
14 import std.string : endsWith;
15 
16 import vibe.core.core;
17 import vibe.core.path;
18 import vibe.core.sync;
19 import vibe.http.fileserver;
20 import vibe.http.router;
21 import vibe.http.server;
22 import vibe.http.websockets;
23 
24 import reloadedvibes.script;
25 import reloadedvibes.utils;
26 import reloadedvibes.watcher;
27 
28 static TaskMutex tm;
29 
30 static this()
31 {
32     tm = new TaskMutex();
33 }
34 
35 HTTPListener registerService(Socket s, Watcher w)
36 {
37     void index(scope HTTPServerRequest, scope HTTPServerResponse res) @safe
38     {
39         immutable scriptTag = s.buildScriptLoaderHTML();
40         render!("index.dt", scriptTag)(res);
41     }
42 
43     void script(scope HTTPServerRequest req, scope HTTPServerResponse res)
44     {
45         immutable disableMsg = (("quiet" in req.query()) !is null);
46         res.writeBody(s.buildScript(disableMsg), 200, "application/javascript; charset=utf-8");
47     }
48 
49     void test(scope HTTPServerRequest, scope HTTPServerResponse res)
50     {
51         immutable scriptURL = s.buildScriptURL();
52         render!("test.dt", scriptURL)(res);
53     }
54 
55     void webSocket(scope WebSocket ws)
56     {
57         if (ws.connected)
58         {
59             auto msg = ws.receiveText();
60             if (msg != "ReloadedVibes::Init;")
61             {
62                 ws.close(WebSocketCloseReason.unsupportedData);
63                 return;
64             }
65             ws.send(msg);
66         }
67 
68         WatcherClient wcl = new WatcherClient(w);
69 
70         do
71         {
72             synchronized (tm)
73             {
74                 if (wcl.query())
75                 {
76                     ws.send("ReloadedVibes::Trigger;");
77                 }
78             }
79             sleep(dur!"msecs"(200));
80         }
81         while (ws.connected);
82 
83         wcl.unregister();
84     }
85 
86     auto router = new URLRouter();
87     router.get("/", &index);
88     router.get("/test", &test);
89     router.get("/reloaded-vibes.js", &script);
90     router.get("/reloaded-vibes.ws", handleWebSockets(&webSocket));
91 
92     auto settings = new HTTPServerSettings();
93     settings.port = s.port;
94     settings.bindAddresses = [s.address];
95     return listenHTTP(settings, router);
96 }
97 
98 HTTPListener registerStaticWebserver(Socket s, string docroot) @safe
99 {
100     auto settings = new HTTPServerSettings();
101     settings.port = s.port;
102     settings.bindAddresses = [s.address];
103     return listenHTTP(settings, serveStaticFiles(docroot));
104 }
105 
106 HTTPListener registerStaticWebserver(Socket s, string docroot, Socket nfService) @safe
107 {
108     immutable htd = NativePath(docroot);
109 
110     void injectingServer(scope HTTPServerRequest req, scope HTTPServerResponse res) @trusted
111     {
112         auto p = req.requestPath;
113         string pString = p.toString;
114 
115         if (pString.endsWith("/"))
116         {
117             p = InetPath(pString[1 .. $] ~ "index.html");
118         }
119         else
120         {
121             p = InetPath(pString[1 .. $]);
122         }
123 
124         try
125         {
126             p.normalize();
127         }
128         catch (Exception)
129         {
130             res.statusCode = 400;
131         }
132 
133         if (p.absolute)
134         {
135             res.statusCode = 500;
136             return;
137         }
138 
139         if (!p.empty && p.bySegment.front.name == "..")
140         {
141             res.statusCode = 400;
142             return;
143         }
144 
145         NativePath file = (htd ~ p);
146         immutable fileS = file.toString;
147         if (fileS.exists && fileS.endsWith(".html"))
148         {
149             res.headers["Content-Type"] = "text/html";
150             auto b = res.bodyWriter;
151 
152             auto f = File(file.toString, "r");
153             ubyte[1] buffer;
154 
155             Outer: while (!f.eof)
156             {
157                 if (f.rawRead(buffer).length == 0)
158                 {
159                     break;
160                 }
161                 if (buffer[0] != '<')
162                 {
163                     b.write(buffer);
164                     continue;
165                 }
166 
167                 static immutable chars = "/body>";
168                 ubyte[6] buffer2;
169 
170                 static foreach (i, c; chars)
171                 {
172                     if (f.rawRead(buffer2[i .. (i + 1)]).length == 0)
173                     {
174                         b.write(buffer);
175                         b.write(buffer2[0 .. i]);
176                         break Outer;
177                     }
178                     if (buffer2[i] != c)
179                     {
180                         b.write(buffer);
181                         b.write(buffer2[0 .. (i + 1)]);
182                         continue Outer;
183                     }
184                 }
185 
186                 // inject
187                 b.write(cast(ubyte[])(nfService.buildScriptLoaderHTML()));
188                 b.write(buffer);
189                 b.write(buffer2);
190             }
191 
192             return;
193         }
194 
195         sendFile(req, res, file);
196     }
197 
198     auto settings = new HTTPServerSettings();
199     settings.port = s.port;
200     settings.bindAddresses = [s.address];
201 
202     return listenHTTP(settings, &injectingServer);
203 }
204 
205 void run(HTTPListeners)(HTTPListeners listeners)
206 {
207     scope (exit)
208     {
209         foreach (HTTPListener h; listeners)
210         {
211             h.stopListening();
212         }
213     }
214     runEventLoop();
215 }