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