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 }