1 /**
2 * Copyright © DiamondMVC 2018
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.init.web;
7 
8 import diamond.core.apptype;
9 
10 static if (isWeb)
11 {
12   import diamond.core;
13   import diamond.http;
14   import diamond.errors;
15   import diamond.authentication;
16   import diamond.security;
17   import diamond.unittesting;
18 
19   static if (isWebServer)
20   {
21     public import diamond.views;
22   }
23 
24   import vibe.d : HTTPServerRequestDelegateS, HTTPServerSettings, HTTPServerRequest,
25                   HTTPServerResponse, HTTPServerErrorInfo, listenHTTP,
26                   HTTPMethod, HTTPStatus, HTTPStatusException,
27                   serveStaticFiles, URLRouter, runApplication;
28 
29   /// Entry point for the web application.
30   private void main()
31   {
32     try
33     {
34       loadWebConfig();
35 
36       defaultPermission = true;
37       requirePermissionMethod(HttpMethod.GET, PermissionType.readAccess);
38       requirePermissionMethod(HttpMethod.POST, PermissionType.writeAccess);
39       requirePermissionMethod(HttpMethod.PUT, PermissionType.updateAccess);
40       requirePermissionMethod(HttpMethod.DELETE, PermissionType.deleteAccess);
41 
42       import diamond.extensions;
43       mixin ExtensionEmit!(ExtensionType.applicationStart, q{
44         {{extensionEntry}}.onApplicationStart();
45       });
46       emitExtension();
47 
48       import websettings;
49       initializeWebSettings();
50 
51       if (webSettings)
52       {
53         webSettings.onApplicationStart();
54       }
55 
56       loadStaticFiles();
57 
58       if (webConfig.specializedRoutes)
59       {
60         foreach (key,route; webConfig.specializedRoutes)
61         {
62           switch (route.type)
63           {
64             case "external":
65               addSpecializedRoute(SpecializedRouteType.external, key, route.value);
66               break;
67 
68             case "internal":
69               addSpecializedRoute(SpecializedRouteType.internal, key, route.value);
70               break;
71 
72             case "local":
73               addSpecializedRoute(SpecializedRouteType.local, key, route.value);
74               break;
75 
76             default: break;
77           }
78         }
79       }
80 
81       foreach (address; webConfig.addresses)
82       {
83         loadServer(address.ipAddresses, address.port);
84       }
85 
86       print("The %s %s is now running.",
87         isWebServer ? "web-server" : "web-api", webConfig.name);
88 
89       static if (isTesting)
90       {
91         import vibe.core.core;
92 
93         runTask({ initializeTests(); });
94       }
95 
96       runApplication();
97     }
98     catch (Throwable t)
99     {
100       handleUnhandledError(t);
101       throw t;
102     }
103   }
104 
105   static if (isWebServer)
106   {
107     mixin GenerateViews;
108 
109     import std.array : join;
110 
111     mixin(generateViewsResult.join(""));
112 
113     mixin GenerateGetView;
114   }
115 
116   static if (isWebApi)
117   {
118     import diamond.controllers;
119 
120     /// A compile-time constant of the controller data.
121     private enum controllerData = generateControllerData();
122 
123     mixin GenerateControllers!(controllerData);
124   }
125 
126 
127   private:
128   /// The static file handlers.
129   __gshared HTTPServerRequestDelegateS[string] _staticFiles;
130 
131   /// Loads the static file handlers.
132   void loadStaticFiles()
133   {
134     foreach (staticFileRoute; webConfig.staticFileRoutes)
135     {
136       import std.algorithm : map, filter;
137       import std.path : baseName;
138       import std.file : dirEntries, SpanMode;
139 
140       auto directoryNames = dirEntries(staticFileRoute, SpanMode.shallow)
141         .filter!(entry => !entry.isFile)
142         .map!(entry => baseName(entry.name));
143 
144       foreach (directoryName; directoryNames)
145       {
146         _staticFiles[directoryName] = serveStaticFiles(staticFileRoute);
147       }
148     }
149   }
150 
151   /**
152   * Loads the server with a specific range of ip addresses and the specified port.
153   * Params:
154   *   ipAddresses = The range of ip addresses to bind the server to.
155   *   port =        The port to bind the server to.
156   */
157   void loadServer(string[] ipAddresses, ushort port)
158   {
159     auto settings = new HTTPServerSettings;
160     settings.port = port;
161     settings.bindAddresses = ipAddresses;
162     settings.accessLogToConsole = webConfig.accessLogToConsole;
163     settings.errorPageHandler = (HTTPServerRequest request, HTTPServerResponse response, HTTPServerErrorInfo error)
164     {
165       import diamond.extensions;
166       mixin ExtensionEmit!(ExtensionType.handleError, q{
167         if (!{{extensionEntry}}.handleError(request, response, error))
168         {
169           return;
170         }
171       });
172       emitExtension();
173 
174       auto e = cast(Exception)error.exception;
175 
176       if (e)
177       {
178         handleUserException(e,request,response,error);
179       }
180       else
181       {
182         handleUserError(error.exception,request,response,error);
183 
184         if (error.exception)
185         {
186           throw error.exception;
187         }
188       }
189     };
190 
191     import diamond.extensions;
192     mixin ExtensionEmit!(ExtensionType.httpSettings, q{
193       {{extensionEntry}}.handleSettings(setting);
194     });
195     emitExtension();
196 
197     auto router = new URLRouter;
198 
199     handleWebSockets(router);
200 
201     router.any("*", &handleHTTPListen);
202 
203     listenHTTP(settings, router);
204   }
205 
206   /**
207   * Handler for http requests.
208   * Params:
209   *   request =   The http request.
210   *   response =  The http response.
211   */
212   void handleHTTPListen(HTTPServerRequest request, HTTPServerResponse response)
213   {
214     auto client = new HttpClient(request, response);
215 
216     try
217     {
218       if (handleSpecializedRoute(client))
219       {
220         return;
221       }
222 
223       auto routes = hasRoutes ?
224         handleRoute(client.ipAddress == "127.0.0.1", client.path) :
225         [client.path];
226 
227       if (!routes)
228       {
229         client.error(HttpStatus.unauthorized);
230       }
231 
232       foreach (i; 0 .. routes.length)
233       {
234         auto route = routes[i];
235 
236         client.isLastRoute = i == (routes.length - 1);
237 
238         client.path = route[0] == '/' ? route : "/" ~ route;
239 
240         client.route = new Route(route);
241 
242         handleHTTPListenInternal(client);
243       }
244     }
245     catch (Throwable t)
246     {
247       static if (loggingEnabled)
248       {
249         import diamond.core.logging;
250 
251         if (client.statusCode == HttpStatus.notFound)
252         {
253           executeLog(LogType.notFound, client);
254         }
255         else
256         {
257           executeLog(LogType.error, client, t.toString());
258         }
259       }
260 
261       auto e = cast(Exception)t;
262 
263       if (e)
264       {
265         handleUserException(e,request,response,null);
266       }
267       else
268       {
269         handleUserError(t,request,response,null);
270         throw t;
271       }
272     }
273   }
274 
275   /**
276   * Internal handler for http clients.
277   * Params:
278   *   client = The client to handle.
279   */
280   private void handleHTTPListenInternal(HttpClient client)
281   {
282     static if (loggingEnabled)
283     {
284       import diamond.core.logging;
285       executeLog(LogType.before, client);
286     }
287 
288     static if (isTesting)
289     {
290       if (!testsPassed || client.ipAddress != "127.0.0.1")
291       {
292         client.error(HttpStatus.serviceUnavailable);
293       }
294     }
295 
296     validateGlobalRestrictedIPs(client);
297 
298     import diamond.extensions;
299     mixin ExtensionEmit!(ExtensionType.httpRequest, q{
300       if (!{{extensionEntry}}.handleRequest(client))
301       {
302         return;
303       }
304     });
305     emitExtension();
306 
307     if (webSettings && !webSettings.onBeforeRequest(client))
308     {
309       client.error(HttpStatus.badRequest);
310     }
311 
312     if (hasRoles)
313     {
314       import std.array : split;
315 
316       auto hasRootAccess = hasAccess(
317         client.role, client.method,
318         client.route.name.split(webConfig.specialRouteSplitter)[0]
319       );
320 
321       if
322       (
323         !hasRootAccess ||
324         (
325           client.route.action &&
326           !hasAccess(client.role, client.method, client.route.name ~ "/" ~ client.route.action)
327         )
328       )
329       {
330         client.error(HttpStatus.unauthorized);
331       }
332     }
333 
334     auto staticFile = _staticFiles.get(client.route.name, null);
335 
336     if (staticFile)
337     {
338       import diamond.init.files;
339       handleStaticFiles(client, staticFile);
340 
341       static if (loggingEnabled)
342       {
343         import diamond.core.logging;
344 
345         executeLog(LogType.staticFile, client);
346       }
347       return;
348     }
349 
350     static if (isWebServer)
351     {
352       import diamond.init.server;
353       handleWebServer(client);
354     }
355     else
356     {
357       import diamond.init.api;
358       handleWebApi(client);
359     }
360 
361     if (webSettings)
362     {
363       webSettings.onAfterRequest(client);
364     }
365 
366     static if (loggingEnabled)
367     {
368       import diamond.core.logging;
369       executeLog(LogType.after, client);
370     }
371   }
372 }