1 /**
2 * Copyright © DiamondMVC 2019
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.app.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   import diamond.app.appcache;
19 
20   static if (isWebServer)
21   {
22     public import diamond.views;
23   }
24 
25   import vibe.d : HTTPServerRequestDelegateS, HTTPServerSettings, HTTPServerRequest,
26                   HTTPServerResponse, HTTPServerErrorInfo, listenHTTP,
27                   HTTPMethod, HTTPStatus, HTTPStatusException,
28                   serveStaticFiles, URLRouter, runApplication,
29                   TLSContextKind, createTLSContext;
30 
31   static if (!isCustomMain)
32   {
33     /// Entry point for the web application.
34     private void main()
35     {
36       runDiamond();
37     }
38   }
39 
40   /// Initializes the Diamond run-time. This function does not initiate the server, tests, tasks, services etc.
41   void initializeDiamond()
42   {
43     setAppCache(new DiamondAppCache);
44 
45     loadWebConfig();
46 
47     if (webConfig.webservices && webConfig.webservices.length)
48     {
49       bool missingSoapDefinitions;
50 
51       foreach (service; webConfig.webservices)
52       {
53         import std.file : exists;
54 
55         if (!exists("__services/" ~ service.name ~ ".d"))
56         {
57           missingSoapDefinitions = true;
58 
59           import diamond.web.soap;
60           loadSoapDefinition(service.name, service.wsdl, service.moduleName);
61         }
62       }
63 
64       if (missingSoapDefinitions)
65       {
66         import diamond.io;
67         print("Must recompile the project, because of missing soap definitions.");
68       //  throw new InitializationError("Must recompile the project, because of missing soap definitions.");
69       }
70     }
71 
72     import diamond.data.mapping.engines.mysql : initializeMySql;
73     initializeMySql();
74 
75     if (webConfig.mongoDb)
76     {
77       import diamond.database.mongo;
78       initializeMongo(webConfig.mongoDb.host, webConfig.mongoDb.port);
79     }
80 
81     static if (hasMsSql)
82     {
83       import diamond.data.mapping.engines.mssql : initializeMsSql;
84       initializeMsSql();
85     }
86 
87     setDetaulfPermissions();
88 
89     loadWhiteListPaths();
90 
91     import diamond.security.validation.sensitive;
92 
93     initializeSensitiveDataValidator();
94 
95     initializeAuth();
96   }
97 
98   /// Runs the diamond application.
99   private void runDiamond()
100   {
101     try
102     {
103       initializeDiamond();
104 
105       import diamond.extensions;
106       mixin ExtensionEmit!(ExtensionType.applicationStart, q{
107         {{extensionEntry}}.onApplicationStart();
108       });
109       emitExtension();
110 
111       import websettings;
112       initializeWebSettings();
113 
114       if (webSettings)
115       {
116         webSettings.onApplicationStart();
117       }
118 
119       loadStaticFiles();
120 
121       loadSpecializedRoutes();
122 
123       foreach (address; webConfig.addresses)
124       {
125         loadServer(address.ipAddresses, address.port);
126       }
127 
128       print("The %s %s is now running.",
129         isWebServer ? "web-server" : "web-api", webConfig.name);
130 
131       static if (isTesting)
132       {
133         import diamond.tasks;
134 
135         executeTask({ initializeTests(); });
136       }
137 
138       executeBackup();
139 
140       runApplication();
141     }
142     catch (Throwable t)
143     {
144       handleUnhandledError(t);
145       throw t;
146     }
147   }
148 
149   /// Sets the default permissions for each http method.
150   private void setDetaulfPermissions()
151   {
152     defaultPermission = true;
153     requirePermissionMethod(HttpMethod.GET, PermissionType.readAccess);
154     requirePermissionMethod(HttpMethod.POST, PermissionType.writeAccess);
155     requirePermissionMethod(HttpMethod.PUT, PermissionType.updateAccess);
156     requirePermissionMethod(HttpMethod.DELETE, PermissionType.deleteAccess);
157   }
158 
159   /// Loads the specialized routes.
160   private void loadSpecializedRoutes()
161   {
162     if (webConfig.specializedRoutes)
163     {
164       foreach (key,route; webConfig.specializedRoutes)
165       {
166         switch (route.type)
167         {
168           case "external":
169             addSpecializedRoute(SpecializedRouteType.external, key, route.value);
170             break;
171 
172           case "internal":
173             addSpecializedRoute(SpecializedRouteType.internal, key, route.value);
174             break;
175 
176           case "local":
177             addSpecializedRoute(SpecializedRouteType.local, key, route.value);
178             break;
179 
180           default: break;
181         }
182       }
183     }
184   }
185 
186   static if (isWebServer)
187   {
188     mixin(generateGlobalView());
189 
190     mixin GenerateViews;
191 
192     static foreach (viewResult; generateViewsResult)
193     {
194       mixin("#line 1 \"view: " ~ viewResult.name ~ "\"\n" ~ viewResult.source);
195     }
196 
197     mixin GenerateGetView;
198   }
199 
200   static if (isWebApi)
201   {
202     import diamond.controllers;
203 
204     /// A compile-time constant of the controller data.
205     private enum controllerData = generateControllerData();
206 
207     mixin GenerateControllers!(controllerData);
208   }
209 
210 
211   private:
212   /// Loads the white-list paths.
213   void loadWhiteListPaths()
214   {
215     if (webConfig.whiteListPaths)
216     {
217       foreach (whiteListPath; webConfig.whiteListPaths)
218       {
219         import diamond.io.file : addPathToWhiteList;
220 
221         addPathToWhiteList(whiteListPath);
222       }
223     }
224   }
225 
226   /// The static file handlers.
227   __gshared HTTPServerRequestDelegateS[string] _staticFiles;
228 
229   /// Loads the static file handlers.
230   void loadStaticFiles()
231   {
232     if (webConfig.staticFileRoutes && webConfig.staticFileRoutes.length)
233     {
234       foreach (staticFileRoute; webConfig.staticFileRoutes)
235       {
236         import std.algorithm : map, filter;
237         import std.path : baseName;
238         import std.file : dirEntries, SpanMode;
239 
240         auto directoryNames = dirEntries(staticFileRoute, SpanMode.shallow)
241           .filter!(entry => !entry.isFile)
242           .map!(entry => baseName(entry.name));
243 
244         foreach (directoryName; directoryNames)
245         {
246           _staticFiles[directoryName] = serveStaticFiles(staticFileRoute);
247         }
248       }
249     }
250   }
251 
252   /**
253   * Loads the server with a specific range of ip addresses and the specified port.
254   * Params:
255   *   ipAddresses = The range of ip addresses to bind the server to.
256   *   port =        The port to bind the server to.
257   */
258   void loadServer(string[] ipAddresses, ushort port)
259   {
260     auto settings = new HTTPServerSettings;
261 
262     if (webConfig.sslCertificateFile && webConfig.sslPrivateKeyFile)
263     {
264       if (port == 80)
265       {
266         loadServer(ipAddresses, 443);
267       }
268       else if (port == 443)
269       {
270         settings.tlsContext = createTLSContext(TLSContextKind.server);
271         settings.tlsContext.useCertificateChainFile(webConfig.sslCertificateFile);
272         settings.tlsContext.usePrivateKeyFile(webConfig.sslPrivateKeyFile);
273       }
274     }
275 
276     settings.port = port;
277     settings.bindAddresses = ipAddresses;
278     settings.accessLogToConsole = webConfig.accessLogToConsole;
279     settings.maxRequestSize = webConfig.maxRequestSize ? webConfig.maxRequestSize : 4000000;
280     settings.maxRequestHeaderSize = webConfig.maxRequestHeaderSize ? webConfig.maxRequestHeaderSize : 8192;
281     settings.errorPageHandler = (HTTPServerRequest request, HTTPServerResponse response, HTTPServerErrorInfo error)
282     {
283       import diamond.extensions;
284       mixin ExtensionEmit!(ExtensionType.handleError, q{
285         if (!{{extensionEntry}}.handleError(request, response, error))
286         {
287           return;
288         }
289       });
290       emitExtension();
291 
292       auto e = cast(Exception)error.exception;
293 
294       if (e)
295       {
296         handleUserException(e,request,response,error);
297       }
298       else
299       {
300         handleUserError(error.exception,request,response,error);
301 
302         if (error.exception)
303         {
304           throw error.exception;
305         }
306       }
307     };
308 
309     import diamond.extensions;
310     mixin ExtensionEmit!(ExtensionType.httpSettings, q{
311       {{extensionEntry}}.handleSettings(settings);
312     });
313     emitExtension();
314 
315     auto router = new URLRouter;
316 
317     handleWebSockets(router);
318 
319     if (port == 443)
320     {
321       router.any("*", &handleHTTPSListen);
322     }
323     else
324     {
325       router.any("*", &handleHTTPListen);
326     }
327 
328     listenHTTP(settings, router);
329   }
330 
331   /**
332   * Handler for http requests.
333   * Params:
334   *   request =   The http request.
335   *   response =  The http response.
336   */
337   void handleHTTPListen(HTTPServerRequest request, HTTPServerResponse response)
338   {
339     handleHTTPListenWorker(request, response, false);
340   }
341 
342   /**
343   * Handler for https requests.
344   * Params:
345   *   request =   The http request.
346   *   response =  The http response.
347   */
348   void handleHTTPSListen(HTTPServerRequest request, HTTPServerResponse response)
349   {
350     handleHTTPListenWorker(request, response, true);
351   }
352 
353   /**
354   * Handler for http requests.
355   * Params:
356   *   request =   The http request.
357   *   response =  The http response.
358   */
359   void handleHTTPListenWorker(HTTPServerRequest request, HTTPServerResponse response, bool isSSL)
360   {
361     auto client = new HttpClient(request, response);
362 
363     try
364     {
365       if (!isSSL && webConfig.forceSSLUrl && webConfig.forceSSLUrl.length)
366       {
367         client.redirect(webConfig.forceSSLUrl);
368         return;
369       }
370 
371       import std.algorithm : canFind;
372 
373       if (webConfig.hostWhiteList && !webConfig.hostWhiteList.canFind(client.host))
374       {
375         client.forbidden();
376       }
377 
378       if (handleSpecializedRoute(client))
379       {
380         return;
381       }
382 
383       static if (loggingEnabled)
384       {
385         import diamond.core.logging;
386         executeLog(LogType.before, client);
387       }
388 
389       static if (isTesting)
390       {
391         if (!testsPassed || client.ipAddress != "127.0.0.1")
392         {
393           client.error(HttpStatus.serviceUnavailable);
394         }
395       }
396 
397       validateGlobalRestrictedIPs(client);
398 
399       import diamond.extensions;
400       mixin ExtensionEmit!(ExtensionType.httpRequest, q{
401         if (!{{extensionEntry}}.handleRequest(client))
402         {
403           return;
404         }
405       });
406       emitExtension();
407 
408       if (webSettings && !webSettings.onBeforeRequest(client))
409       {
410         client.error(HttpStatus.badRequest);
411       }
412 
413       client.handlingRequest = true;
414 
415       auto routes = hasRoutes ?
416         handleRoute(client.ipAddress == "127.0.0.1", client.path) :
417         [client.path];
418 
419       if (!routes)
420       {
421         client.error(HttpStatus.unauthorized);
422       }
423 
424       foreach (i; 0 .. routes.length)
425       {
426         auto route = routes[i];
427 
428         client.isLastRoute = i == (routes.length - 1);
429 
430         client.path = route[0] == '/' ? route : "/" ~ route;
431 
432         client.route = new Route(route);
433 
434         handleHTTPListenInternal(client);
435       }
436     }
437     catch (HTTPStatusException hse)
438     {
439       auto e = cast(Exception)hse;
440 
441       if (e)
442       {
443         handleUserException(e,request,response,null);
444       }
445     }
446     catch (Throwable t)
447     {
448       static if (loggingEnabled)
449       {
450         import diamond.core.logging;
451 
452         if (client.statusCode == HttpStatus.notFound)
453         {
454           executeLog(LogType.notFound, client);
455         }
456         else
457         {
458           executeLog(LogType.error, client, t.toString());
459         }
460       }
461 
462       auto e = cast(Exception)t;
463 
464       if (e)
465       {
466         handleUserException(e,request,response,null);
467       }
468       else
469       {
470         handleUserError(t,request,response,null);
471         throw t;
472       }
473     }
474   }
475 
476   /**
477   * Internal handler for http clients.
478   * Params:
479   *   client = The client to handle.
480   */
481   private void handleHTTPListenInternal(HttpClient client)
482   {
483     if (webConfig.authenticateStaticFiles)
484     {
485       handleHTTPPermissions(client);
486     }
487 
488     if (_staticFiles)
489     {
490       auto staticFile = _staticFiles.get(client.route.name, null);
491 
492       if (staticFile)
493       {
494         import diamond.app.files;
495         handleStaticFiles(client, staticFile);
496 
497         static if (loggingEnabled)
498         {
499           import diamond.core.logging;
500 
501           executeLog(LogType.staticFile, client);
502         }
503         return;
504       }
505     }
506 
507     if (!webConfig.authenticateStaticFiles)
508     {
509       handleHTTPPermissions(client);
510     }
511 
512     static if (isWebServer)
513     {
514       import diamond.app.server;
515       auto foundPage = client.forceApi ? false : handleWebServer(client);
516     }
517     else
518     {
519       auto foundPage = false;
520     }
521 
522     static if (isWebApi)
523     {
524       if (!foundPage)
525       {
526         import diamond.app.api;
527         handleWebApi(client);
528       }
529     }
530     else
531     {
532       if (!foundPage)
533       {
534         client.notFound();
535       }
536     }
537 
538     if (webSettings)
539     {
540       webSettings.onAfterRequest(client);
541     }
542 
543     static if (loggingEnabled)
544     {
545       import diamond.core.logging;
546       executeLog(LogType.after, client);
547     }
548   }
549 }
550 
551 void handleHTTPPermissions(HttpClient client)
552 {
553   if (hasRoles)
554   {
555     import std.array : split;
556 
557     auto hasRootAccess = hasAccess(
558       client.role, client.method,
559       client.route.name.split(webConfig.specialRouteSplitter)[0]
560     );
561 
562     if
563     (
564       !hasRootAccess ||
565       (
566         client.route.action &&
567         !hasAccess(client.role, client.method, client.route.name ~ "/" ~ client.route.action)
568       )
569     )
570     {
571       client.error(HttpStatus.unauthorized);
572     }
573   }
574 }