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 }