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.client;
7 
8 import diamond.core.apptype;
9 
10 static if (isWeb)
11 {
12   /// The name of the language session key.
13   private static __gshared const languageSessionKey = "__D_LANGUAGE";
14 
15   /// The name of the cookie consent's cookie.
16   private static const __gshared consentCookieName = "__D_COOKIE_CONSENT";
17 
18   /// The name of the privacy key.
19   private static __gshared const privacySessionKey = "__D_PRIVACY_CONFIG";
20 
21   /// Wrapper around the client's request aand response.
22   final class HttpClient
23   {
24     import std.conv : to;
25     import std.typecons : Nullable;
26     import std.array : Appender, appender;
27 
28     import vibe.d : HTTPServerRequest, HTTPServerResponse,
29                     HTTPStatusException;
30 
31     import diamond.authentication;
32     import diamond.http.sessions;
33     import diamond.http.cookies;
34     import diamond.http.method;
35     import diamond.http.status;
36     import diamond.http.routing;
37     import diamond.http.privacy;
38     import diamond.errors.checks;
39     import diamond.core.webconfig;
40 
41     private:
42     /// The request.
43     HTTPServerRequest _request;
44 
45     /// The response.
46     HTTPServerResponse _response;
47 
48     /// The session.
49     HttpSession _session;
50 
51     /// The cookies.
52     HttpCookies _cookies;
53 
54     /// The cookie consent of a user.
55     Nullable!HttpCookieConsent _cookieConsent;
56 
57     /// The route.
58     Route _route;
59 
60     /// The role.
61     Role _role;
62 
63     /// The ip address.
64     string _ipAddress;
65 
66     /// Boolean determnining whether the client has been redirected or not.
67     bool _redirected;
68 
69     static if (loggingEnabled)
70     {
71       /// The data written to the response.
72       Appender!(ubyte[]) _data;
73     }
74 
75     /// the status code for the response.
76     HttpStatus _statusCode;
77 
78     /// The language of the client.
79     string _language;
80 
81     /// Boolean determining whether the client's route is the last route to handle.
82     bool _isLastRoute;
83 
84     /// The path.
85     string _path;
86 
87     /// The privacy collection.
88     PrivacyCollection _privacyCollection;
89 
90     /// Boolean determining whether the client is handling the request or not.
91     bool _handlingRequest;
92 
93     /// Force the request as a web-api request.
94     bool _forceApi;
95 
96     final:
97     package(diamond)
98     {
99       /**
100       * Createsa  new http client.
101       * Params:
102       *   request =   The request.
103       *   response =  The response.
104       */
105       this(HTTPServerRequest request, HTTPServerResponse response)
106       {
107         _request = enforceInput(request, "Cannot create a client without a request.");
108         _response = enforceInput(response, "Cannot create a client without a response.");
109 
110         addContext("__D_RAW_HTTP_CLIENT", this);
111 
112         _path = request.requestPath.toString();
113 
114         static if (loggingEnabled)
115         {
116           _data = appender!(ubyte[]);
117         }
118       }
119     }
120 
121     public:
122     @property
123     {
124       /// Gets the raw vibe.d request.
125       package(diamond) HTTPServerRequest rawRequest() { return _request; }
126 
127       /// Gets the raw vibe.d response.
128       package(diamond) HTTPServerResponse rawResponse() { return _response; }
129 
130       /// Gets the route.
131       Route route() { return _route; }
132 
133       /// Sets the route.
134       package(diamond) void route(Route newRoute)
135       {
136         _route = newRoute;
137       }
138 
139       /// Gets a boolean determining whether it's the client's last route to handle.
140       package(diamond) bool isLastRoute() { return _isLastRoute; }
141 
142       /// Sets a boolean determining whether it's the client's last route to handle.
143       package(diamond) void isLastRoute(bool isLastRouteState)
144       {
145         _isLastRoute = isLastRouteState;
146       }
147 
148       /// Gets the method.
149       HttpMethod method() { return cast(HttpMethod)_request.method; }
150 
151       /// Gets the session.
152       HttpSession session()
153       {
154         if (_session)
155         {
156           return _session;
157         }
158 
159         _session = getSession(this);
160 
161         return _session;
162       }
163 
164       /// Gets the cookies.
165       HttpCookies cookies()
166       {
167         if (_cookies)
168         {
169           return _cookies;
170         }
171 
172         _cookies = new HttpCookies(this);
173 
174         return _cookies;
175       }
176 
177       /// Gets the cookie consent of a user.
178       HttpCookieConsent cookieConsent()
179       {
180         if (_cookieConsent.isNull)
181         {
182           auto consent = cookies.get(consentCookieName);
183 
184           if (!consent || !consent.length)
185           {
186             cookieConsent = HttpCookieConsent.all;
187           }
188           else
189           {
190             _cookieConsent = cast(HttpCookieConsent)consent;
191           }
192         }
193 
194         return _cookieConsent.get;
195       }
196 
197       /// Sets the cookie consent of a user.
198       void cookieConsent(HttpCookieConsent newCookieConsent)
199       {
200         _cookieConsent = newCookieConsent;
201 
202         cookies.remove(consentCookieName);
203         cookies.create(HttpCookieType.session, consentCookieName, cast(string)_cookieConsent.get, 60 * 60 * 24 * 14);
204       }
205 
206       /// Gets the ip address.
207       string ipAddress()
208       {
209         if (!_ipAddress)
210         {
211           if (webConfig.ipHeader && webConfig.ipHeader.length)
212           {
213             _ipAddress = _request.headers[webConfig.ipHeader];
214           }
215           else
216           {
217              auto ip = _request.headers.get("X-Real-IP", null);
218 
219              if (!ip || !ip.length)
220              {
221                ip = _request.headers.get("X-Forwarded-For", null);
222              }
223 
224             _ipAddress = ip && ip.length ? ip : _request.clientAddress.toAddressString();
225           }
226         }
227 
228         return _ipAddress;
229       }
230 
231       /// Gets the raw request stream.
232       auto requestStream() { return _request.bodyReader; }
233 
234       /// Gets the raw response stream.
235       auto responseStream() { return _response.bodyWriter; }
236 
237       /// Gets a boolean determnining whether the response is connected or not.
238       bool connected() { return _response.connected; }
239 
240       /// Gets the current path.
241       string path()
242       {
243         return _path;
244       }
245 
246       /// Sets the path of the client.
247       package(diamond) void path(string newPath)
248       {
249         _path = newPath;
250       }
251 
252       /// Gets the query string.
253       string queryString() { return _request.queryString; }
254 
255       /// Gets a mapped query of the query string.
256       auto query() { return _request.query; }
257 
258       /// Gets the generic http parameters.
259       auto httpParams() { return _request.params; }
260 
261       /// Gets the files from the request.
262       auto files() { return _request.files; }
263 
264       /// Gets the form from the request.
265       auto form() { return _request.form; }
266 
267       /// Gets the full url from the request.
268       auto fullUrl() { return _request.fullURL; }
269 
270       /// Gets the json from the request.
271       auto json() { return _request.json; }
272 
273       /// Gets the content type from the request.
274       string contentType() { return _request.contentType; }
275 
276       /// Gets the content type parameters from the request.
277       string contentTypeParameters() { return _request.contentTypeParameters; }
278 
279       /// Gets the host from the request.
280       string host() { return _request.host; }
281 
282       /// Gets the headers from the request.
283       auto headers() { return _request.headers; }
284 
285       /// Gets a boolean determnining whether the request was done over a secure tls protocol.
286       bool tls() { return _request.tls; }
287 
288       /// Gets the raw client address of the request.
289       auto clientAddress() { return _request.clientAddress; }
290 
291       /// Gets the client certificate from the request.
292       auto clientCertificate() { return _request.clientCertificate; }
293 
294       /// Boolean determining whether the client has been redirected or not.
295       bool redirected() { return _redirected; }
296 
297       /// Gets the role associated with the client.
298       Role role()
299       {
300         if (_role)
301         {
302           return _role;
303         }
304 
305         if (!_role)
306         {
307           validateAuthentication(this);
308         }
309 
310         _role = getRole(this);
311 
312         return _role;
313       }
314 
315       /// Gets the status code of the response.
316       HttpStatus statusCode() { return _statusCode; }
317 
318       /// Gets the language of the client.
319       string language()
320       {
321         if (_language is null)
322         {
323           import diamond.data.i18n.messages : _defaultLanguage;
324 
325           _language = session.getValue!string(languageSessionKey, _defaultLanguage);
326         }
327 
328         return _language;
329       }
330 
331       /// Sets the language of the client.
332       void language(string newLanguage)
333       {
334         _language = newLanguage;
335         session.setValue(languageSessionKey, _language);
336       }
337 
338       /// Gets the privacy collection of the client.
339       PrivacyCollection privacy()
340       {
341         if (_privacyCollection is null)
342         {
343           _privacyCollection = session.getValue!PrivacyCollection(privacySessionKey, null);
344 
345           if (_privacyCollection is null)
346           {
347             _privacyCollection = new PrivacyCollection;
348 
349             session.setValue(privacySessionKey, _privacyCollection);
350           }
351         }
352 
353         return _privacyCollection;
354       }
355 
356       /// Sets a boolean determining whether the client is handling the request or not.
357       package(diamond) void handlingRequest(bool isHandlingRequest)
358       {
359         _handlingRequest = isHandlingRequest;
360       }
361 
362       /// Sets a boolean determining whether the request should be forced as an api request or not.
363       void forceApi(bool shouldForceApi)
364       {
365         _forceApi = shouldForceApi;
366       }
367 
368       /// Gets a boolean determining whether the request should be forced as an api request or not.
369       bool forceApi() { return _forceApi; }
370     }
371 
372     /// Gets a model from the request's json.
373     T getModelFromJson(T, CTORARGS...)(CTORARGS args)
374     {
375       import vibe.data.json;
376 
377       static if (is(T == struct))
378       {
379         T value;
380 
381         value.deserializeJson(_request.json);
382 
383         return value;
384       }
385       else static if (is(T == class))
386       {
387         auto value = new T(args);
388 
389         value.deserializeJson(_request.json);
390 
391         return value;
392       }
393       else
394       {
395         static assert(0);
396       }
397     }
398 
399     /// Gets a santized model from the request's json.
400     T getSanitizedModelFromJson(T, CTORARGS...)(CTORARGS args)
401     {
402       import vibe.data.json;
403       import vibe.stream.operations : readAllUTF8;
404 
405       import diamond.security.html;
406 
407       auto json = _request.bodyReader ? _request.bodyReader.readAllUTF8() : "";
408 
409       if (!json)
410       {
411         return T.init;
412       }
413 
414       static if (is(T == struct))
415       {
416         T value = deserializeJson!T(escapeJson(json));
417 
418         return value;
419       }
420       else static if (is(T == class))
421       {
422         auto value = new T(args);
423 
424         Json jsonObject = deserializeJson!Json(escapeJson(json));
425 
426         value.deserializeJson(jsonObject);
427 
428         return value;
429       }
430       else
431       {
432         static assert(0);
433       }
434     }
435 
436     /**
437     * Adds a generic context value to the client.
438     * Params:
439     *   name =  The name of the value.
440     *   value = The value.
441     */
442     void addContext(T)(string name, T value)
443     {
444       _request.context[name] = value;
445     }
446 
447     /**
448     * Gets a value from the client's context.
449     * Params:
450     *   name =          The name of the value to retrieve.
451     *   defaultValue =  The default value to retrieve if the value wasn't found in the context.
452     * Returns:
453     *   The value if found, defaultValue otherwise.
454     */
455     T getContext(T)(string name, lazy T defaultValue = T.init)
456     {
457       import std.variant : Variant;
458       Variant value = _request.context.get(name, Variant.init);
459 
460       if (!value.hasValue)
461       {
462         return defaultValue;
463       }
464 
465       return value.get!T;
466     }
467 
468     /**
469     * Checks whether a value is present n the client's context or not.
470     * Params:
471     *   name = The name to check for existence.
472     * Returns:
473     *   True if the value is present, false otherwise.
474     */
475     bool hasContext(string name)
476     {
477       import std.variant : Variant;
478 
479       return _request.context.get(name, Variant.init).hasValue;
480     }
481 
482     /**
483     * Redirects the client.
484     * Params:
485     *   url =    The url to redirect the client to.
486     *   status = The status of the redirection.
487     */
488     void redirect(string url, HttpStatus status = HttpStatus.found)
489     {
490       _response.redirect(url, status);
491 
492       import diamond.core.webconfig;
493       foreach (headerKey,headerValue; webConfig.defaultHeaders.general)
494       {
495         _response.headers[headerKey] = headerValue;
496       }
497 
498       _redirected = true;
499     }
500 
501     /**
502     * Does an internal redirect.
503     * Params:
504     *   path = The path.
505     */
506     void internalRedirect(string path)
507     {
508       if (_handlingRequest)
509       {
510         return;
511       }
512 
513       _path = path;
514     }
515 
516     /**
517     * Throws a http status exception.
518     * Params:
519     *   status = The status.
520     * Throws:
521     *   Always throws HTTPStatusException.
522     */
523     void error(HttpStatus status)
524     {
525       _statusCode = status;
526       _response.statusCode = _statusCode;
527 
528       throw new HTTPStatusException(status);
529     }
530 
531     /// Sends a 404 status.
532     void notFound()
533     {
534       error(HttpStatus.notFound);
535     }
536 
537     /// Sends an unauthorized error
538     void unauthorized()
539     {
540       error(HttpStatus.unauthorized);
541     }
542 
543     /// Sends a forbidden error
544     void forbidden()
545     {
546       error(HttpStatus.forbidden);
547     }
548 
549     /**
550     * Logs the client in.
551     * Params:
552     *   loginTime = The time the client should be logged in.
553     *   role =      The role the client should be after being logged in.
554     */
555     void login(long loginTime, Role role)
556     {
557       import diamondauth = diamond.authentication;
558       diamondauth.login(this, loginTime, role);
559     }
560 
561     /// Logs the client out.
562     void logout()
563     {
564       import diamondauth = diamond.authentication;
565       diamondauth.logout(this);
566     }
567 
568     /**
569     * Writes data to the response stream.
570     * Params:
571     *   data = The data to write.
572     */
573     void write(string data)
574     {
575       static if (loggingEnabled)
576       {
577         _data ~= cast(ubyte[])data;
578       }
579 
580       _response.bodyWriter.write(data);
581     }
582 
583     /**
584     * Writes data to the response stream.
585     * Params:
586     *   data = The data to write.
587     */
588     void write(ubyte[] data)
589     {
590       static if (loggingEnabled)
591       {
592         _data ~= data;
593       }
594 
595       _response.bodyWriter.write(data);
596     }
597 
598     static if (loggingEnabled)
599     {
600       /// Gets the body data from the response stream.
601       package(diamond) ubyte[] getBody()
602       {
603         return _data.data;
604       }
605     }
606   }
607 }