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 }