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.http.routing; 7 8 import diamond.core.apptype; 9 10 static if (isWeb) 11 { 12 import vibe.d : HTTPServerRequest, HTTPClientRequest, HTTPClientResponse, requestHTTP, HTTPMethod; 13 import vibe.stream.operations : readAllUTF8; 14 15 import diamond.errors : enforce; 16 import diamond.http.client; 17 import diamond.http.method; 18 19 static if (isWebServer) 20 { 21 private static __gshared string[string] _routableViews; 22 23 /** 24 * Adds a route to a view. 25 * Params: 26 * route = The route. 27 * viewName = The view name. 28 */ 29 void addViewRoute(string route, string viewName) 30 { 31 _routableViews[route] = viewName; 32 } 33 34 /** 35 * Gets the view name from a custom route. 36 * Params: 37 * route = The route used to retrieve the view name. 38 * Returns: 39 * The view name if routable, null otherwise. 40 */ 41 package(diamond) string getViewNameFromRoute(string route) 42 { 43 return _routableViews.get(route, null); 44 } 45 } 46 47 /// Collection of routes. 48 private static __gshared RouteEntry[string] _routes; 49 50 /// Collection of specialized routes. 51 private static __gshared ISpecializedRoute[string] _specializedRoutes; 52 53 /// Disallowed headers for proxies, used by specialized routes. 54 private static __gshared const disallowedHeaders = ["Content-Length", "Transfer-Encoding", "Content-Encoding", "Connection"]; 55 56 /// Enumeration of specialized route types. 57 enum SpecializedRouteType 58 { 59 /// An external specialized route can access external resources at another host. 60 external, 61 62 /// An internal specialized route can access resources internal to the application. 63 internal, 64 65 /// A local specialized route can access local resources on the same host. 66 local 67 } 68 69 /// An interface for a specialized route. 70 private interface ISpecializedRoute 71 { 72 /** 73 * The function to handle the specialized route. 74 * Params: 75 * client = The client to handle for the specialized route. 76 * Returns: 77 * False if the client should still be handled internally or true if the client has been handled by the specialized route. 78 */ 79 bool handle(HttpClient client); 80 } 81 82 /// An implementation of an external specialized route. 83 private final class ExternalSpecializedRoute : ISpecializedRoute 84 { 85 /// The external url. 86 string url; 87 88 /** 89 * Creates a new external specialized route. 90 * Params: 91 * url = The url of the external specialized route. 92 */ 93 this(string url) 94 { 95 this.url = url; 96 } 97 98 /** 99 * Handles the external specialized route. 100 * Params: 101 * client = The client to handle for the specialized route. 102 * Returns: 103 * True, because the external specialized route handles the client. 104 */ 105 final bool handle(HttpClient client) 106 { 107 auto queryString = client.rawRequest.queryString; 108 109 requestHTTP 110 ( 111 url ~ (queryString ? queryString : ""), 112 (scope request) 113 { 114 request.method = cast(HTTPMethod)client.method; 115 request.headers = client.rawRequest.headers.dup; 116 117 foreach (disallowedHeader; disallowedHeaders) 118 { 119 if (disallowedHeader in request.headers) 120 { 121 request.headers.remove(disallowedHeader); 122 } 123 } 124 125 if (client.method == HttpMethod.POST || client.method == HttpMethod.PUT || client.method == HttpMethod.PATCH) 126 { 127 auto data = client.requestStream.readAllUTF8(); 128 129 if (data && data.length) 130 { 131 request.writeBody(cast(ubyte[])data); 132 } 133 } 134 }, 135 (scope response) 136 { 137 client.rawResponse.headers = response.headers.dup; 138 139 foreach (disallowedHeader; disallowedHeaders) 140 { 141 if (disallowedHeader in response.headers) 142 { 143 client.rawResponse.headers.remove(disallowedHeader); 144 } 145 } 146 147 auto data = response.bodyReader.readAllUTF8(); 148 149 client.write(data); 150 } 151 ); 152 153 return true; 154 } 155 } 156 157 /// Implementation of an internal specialized route. 158 private final class InternalSpecializedRoute : ISpecializedRoute 159 { 160 /// The internal route. 161 string route; 162 163 /** 164 * Creates a new internal specialized route. 165 * Params: 166 * route = The internal route. 167 */ 168 this(string route) 169 { 170 this.route = route; 171 } 172 173 /** 174 * Handles the internal specialized route. 175 * Params: 176 * client = The client to handle for the specialized route. 177 * Returns: 178 * False, because the client should still be handled internally. 179 */ 180 final bool handle(HttpClient client) 181 { 182 client.path = route; 183 184 return false; 185 } 186 } 187 188 /// Implementation of a local specialized route. 189 private final class LocalSpecializedRoute : ISpecializedRoute 190 { 191 /// The local port. 192 ushort port; 193 194 /** 195 * Creates a new local specialized route. 196 * Params: 197 * port = The port of the specialized route. 198 */ 199 this(ushort port) 200 { 201 this.port = port; 202 } 203 204 /** 205 * Handles the local specialized route. 206 * Params: 207 * client = The client to handle for the specialized route. 208 * Returns: 209 * True, because the local specialized route handles the client. 210 */ 211 final bool handle(HttpClient client) 212 { 213 import std.conv : to; 214 215 return new ExternalSpecializedRoute("http://127.0.0.1:" ~ to!string(port) ~ "/").handle(client); 216 } 217 } 218 219 package(diamond) 220 { 221 /** 222 * Adds a specialized route. 223 * Params: 224 * routeType = The type of the route. 225 * route = The route. 226 * value = The value of the specialized route eg. url, internal-route or port. 227 */ 228 void addSpecializedRoute(SpecializedRouteType routeType, string route, string value) 229 { 230 final switch (routeType) 231 { 232 case SpecializedRouteType.external: 233 _specializedRoutes[route] = new ExternalSpecializedRoute(value); 234 break; 235 236 case SpecializedRouteType.internal: 237 _specializedRoutes[route] = new InternalSpecializedRoute(value); 238 break; 239 240 case SpecializedRouteType.local: 241 import std.conv : to; 242 _specializedRoutes[route] = new LocalSpecializedRoute(to!ushort(value)); 243 break; 244 } 245 } 246 247 bool handleSpecializedRoute(HttpClient client) 248 { 249 if (!hasSpecializedRoutes) 250 { 251 return false; 252 } 253 254 auto route = client.path; 255 256 if (route[0] == '/' && route.length > 1) 257 { 258 route = route[1 .. $]; 259 } 260 261 auto specializedRoute = _specializedRoutes.get(route, null); 262 263 if (!specializedRoute) 264 { 265 return false; 266 } 267 268 return specializedRoute.handle(client); 269 } 270 271 /// Gets a boolean determining whether there are special routes specified. 272 @property bool hasSpecializedRoutes() 273 { 274 return _specializedRoutes && _specializedRoutes.length; 275 } 276 277 /// Gets a boolean determining whether there are routes specified. 278 @property bool hasRoutes() 279 { 280 return _routes && _routes.length; 281 } 282 283 /// Enumeration of route modes. 284 enum RouteMode 285 { 286 /// Specifies a redirection route. 287 redirect, 288 /// Specifies a combination route. 289 combine, 290 /// Specifies an internal route. 291 internal 292 } 293 294 /// A route entry. 295 class RouteEntry 296 { 297 /// The mode of the route. 298 RouteMode mode; 299 /// The routes. 300 string[] routes; 301 302 /** 303 * Creates a new route entry. 304 * Params: 305 * mode = The mode. 306 * routes = The routes. 307 */ 308 this(RouteMode mode, string[] routes) 309 { 310 this.mode = mode; 311 this.routes = routes; 312 } 313 } 314 315 /** 316 * Handling a route. 317 * Params: 318 * isInternal = Boolean determining whether the handling is internal. 319 * route = The route. 320 * Returns: 321 * The routes to handle for the specific route. 322 */ 323 auto handleRoute(bool isInternal, string route) 324 { 325 auto routeEntry = _routes.get(rewriteRoute(route), null); 326 327 if (!routeEntry) 328 { 329 return [route]; 330 } 331 332 if (!routeEntry.routes || !routeEntry.routes.length) 333 { 334 return null; 335 } 336 337 final switch (routeEntry.mode) 338 { 339 case RouteMode.redirect: return routeEntry.routes[1 .. $]; // Using slices avoid s allocation 340 case RouteMode.combine: return routeEntry.routes; 341 case RouteMode.internal: return isInternal ? routeEntry.routes[0 .. 1] : null; // Using slices avoids allocation 342 } 343 } 344 } 345 346 /** 347 * Rewrites a route. 348 * Params: 349 * route = The route to rewrite. 350 * Returns: 351 * The rewritten route. 352 */ 353 private string rewriteRoute(string route) 354 { 355 import std..string : strip; 356 import diamond.core..string : firstToLower; 357 358 if (route == "/") 359 { 360 import diamond.core : webConfig; 361 362 route = webConfig.homeRoute.firstToLower(); 363 } 364 365 if (route[0] == '/') 366 { 367 route = route[1 .. $]; 368 } 369 370 if (route[$-1] == '/') 371 { 372 route = route[0 .. $-1]; 373 } 374 375 return route.strip(); 376 } 377 378 /** 379 * Adds a route. 380 * Params: 381 * mode = The mode of the route. 382 * sourceRoute = The source route. 383 * destinationRoute = The destination route. (Should be null for RouteMode.internal) 384 */ 385 private void addRoute(RouteMode mode, string sourceRoute, string destinationRoute) 386 { 387 import std..string : strip; 388 389 enforce(sourceRoute && sourceRoute.strip().length, "Found no source route."); 390 391 if (mode != RouteMode.internal) 392 { 393 enforce(destinationRoute && destinationRoute.strip().length, "Found no destination route."); 394 } 395 396 sourceRoute = rewriteRoute(sourceRoute); 397 398 if (mode != RouteMode.internal) 399 { 400 destinationRoute = rewriteRoute(destinationRoute); 401 } 402 403 _routes[sourceRoute] = new RouteEntry(mode, destinationRoute ? [sourceRoute, destinationRoute] : [sourceRoute]); 404 } 405 406 /// Static class to create routes. 407 static final class Routes 408 { 409 public: 410 final: 411 static: 412 /** 413 * Creates a redirection route. 414 * Will redirect 'sourceRoute' to 'destinationRoute' internally. 415 * Params: 416 * sourceRoute = The source route. 417 * destinationRoute = The destination route. 418 */ 419 void redirect(string sourceRoute, string destinationRoute) 420 { 421 addRoute(RouteMode.redirect, sourceRoute, destinationRoute); 422 } 423 424 /** 425 * Creates a combination route. 426 * Will handle 'sourceRoute' first, then 'destinationRoute' afterwaards. 427 * The first route should not create respponse data! 428 * For data passing from first route to the second use the request context. 429 * Params: 430 * sourceRoute = The source route. 431 * destinationRoute = The destination route. 432 */ 433 void combine(string sourceRoute, string destinationRoute) 434 { 435 addRoute(RouteMode.combine, sourceRoute, destinationRoute); 436 } 437 438 /** 439 * Creates an internal route. 440 * Internal routes can only be accessed internally, which measn any external access to them will throw an unauthorized error. 441 * Params: 442 * route = The route. 443 */ 444 void internal(string route) 445 { 446 addRoute(RouteMode.internal, route, null); 447 } 448 } 449 450 // A http route. 451 final class Route 452 { 453 private: 454 /// The raw route url. 455 string _raw; 456 /// The name of the route. 457 string _name; 458 /// The action of the route. 459 string _action; 460 /// The paramters of the route. 461 string[] _params; 462 463 public: 464 final: 465 /** 466 * Creates a new route. 467 * Params: 468 * url = The url of the route. 469 */ 470 this(string url) 471 { 472 enforce(url && url.length, "Invalid route url."); 473 474 import std..string : strip; 475 import diamond.core..string : firstToLower; 476 477 url = url.strip(); 478 _raw = url; 479 480 if (url == "/") 481 { 482 import diamond.core : webConfig; 483 484 _name = webConfig.homeRoute.firstToLower(); 485 return; 486 } 487 488 import std.array : split, array; 489 import std.algorithm : map, canFind; 490 491 if (url[0] == '/') 492 { 493 url = url[1 .. $]; 494 } 495 496 if (url[$-1] == '/') 497 { 498 url = url[0 .. $-1]; 499 } 500 501 auto routeData = url.split("/"); 502 503 enforce(!routeData.length || !routeData[$-1].canFind("?"), "Found query string in the routing url."); 504 505 _name = routeData.length ? routeData[0].strip().firstToLower() : ""; 506 507 if (routeData.length > 1) 508 { 509 _action = routeData[1].strip(); 510 511 if (routeData.length > 2) 512 { 513 _params = routeData[2 .. $].map!(d => d.strip()).array; 514 } 515 } 516 } 517 518 /** 519 * Creates a new route. 520 * Params: 521 * client = The client to create a route of. 522 */ 523 this(HttpClient client) 524 { 525 enforce(client, "No client given."); 526 527 this(client.path); 528 } 529 530 @property 531 { 532 /// Gets the name. 533 string name() { return _name; } 534 535 /// Gets the action. 536 string action() { return _action; } 537 538 /// Gets the parameters. 539 string[] params() { return _params; } 540 541 /// Gets a boolean determining whether the route has paramters or not. 542 bool hasParams() { return _params && _params.length; } 543 } 544 545 package(diamond) void passDataToAction() 546 { 547 if (!hasParams) 548 { 549 _action = null; 550 return; 551 } 552 553 _action = _params[0]; 554 555 if (_params.length > 1) 556 { 557 _params = _params[1 .. $]; 558 } 559 else 560 { 561 _params = null; 562 } 563 } 564 565 /** 566 * Gets data from a specific parameter. 567 * Params: 568 * index = The index to get data from. 569 * Returns: 570 * The data of the parameter. 571 */ 572 T getData(T)(size_t index) 573 { 574 enforce(hasParams, "No parameters specified."); 575 enforce(index < _params.length, "Index out of bounds."); 576 577 import std.conv : to; 578 579 return to!T(_params[index]); 580 } 581 582 /// Converts the route to a string. 583 override string toString() 584 { 585 return _raw; 586 } 587 } 588 }