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 }