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.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   /// Wrapper around the client's request aand response.
16   final class HttpClient
17   {
18     import std.conv : to;
19 
20     import vibe.d : HTTPServerRequest, HTTPServerResponse,
21                     HTTPStatusException;
22 
23     import diamond.authentication;
24     import diamond.http.sessions;
25     import diamond.http.cookies;
26     import diamond.http.method;
27     import diamond.http.status;
28     import diamond.http.routing;
29     import diamond.errors.checks;
30 
31     private:
32     /// The request.
33     HTTPServerRequest _request;
34 
35     /// The response.
36     HTTPServerResponse _response;
37 
38     /// The session.
39     HttpSession _session;
40 
41     /// The cookies.
42     HttpCookies _cookies;
43 
44     /// The route.
45     Route _route;
46 
47     /// The role.
48     Role _role;
49 
50     /// The ip address.
51     string _ipAddress;
52 
53     /// Boolean determnining whether the client has been redirected or not.
54     bool _redirected;
55 
56     /// The data written to the response.
57     ubyte[] _data;
58 
59     /// the status code for the response.
60     HttpStatus _statusCode;
61 
62     /// The language of the client.
63     string _language;
64 
65     /// Boolean determining whether the client's route is the last route to handle.
66     bool _isLastRoute;
67 
68     /// The path.
69     string _path;
70 
71     final:
72     package(diamond)
73     {
74       /**
75       * Createsa  new http client.
76       * Params:
77       *   request =   The request.
78       *   response =  The response.
79       */
80       this(HTTPServerRequest request, HTTPServerResponse response)
81       {
82         _request = enforceInput(request, "Cannot create a client without a request.");
83         _response = enforceInput(response, "Cannot create a client without a response.");
84 
85         addContext("__D_RAW_HTTP_CLIENT", this);
86 
87         _path = request.requestPath.toString();
88       }
89     }
90 
91     public:
92     @property
93     {
94       /// Gets the raw vibe.d request.
95       package(diamond) HTTPServerRequest rawRequest() { return _request; }
96 
97       /// Gets the raw vibe.d response.
98       package(diamond) HTTPServerResponse rawResponse() { return _response; }
99 
100       /// Gets the route.
101       Route route() { return _route; }
102 
103       /// Sets the route.
104       package(diamond) void route(Route newRoute)
105       {
106         _route = newRoute;
107       }
108 
109       /// Gets a boolean determining whether it's the client's last route to handle.
110       package(diamond) bool isLastRoute() { return _isLastRoute; }
111 
112       /// Sets a boolean determining whether it's the client's last route to handle.
113       package(diamond) void isLastRoute(bool isLastRouteState)
114       {
115         _isLastRoute = isLastRouteState;
116       }
117 
118       /// Gets the method.
119       HttpMethod method() { return cast(HttpMethod)_request.method; }
120 
121       /// Gets the session.
122       HttpSession session()
123       {
124         if (_session)
125         {
126           return _session;
127         }
128 
129         _session = getSession(this);
130 
131         return _session;
132       }
133 
134       /// Gets the cookies.
135       HttpCookies cookies()
136       {
137         if (_cookies)
138         {
139           return _cookies;
140         }
141 
142         _cookies = new HttpCookies(this);
143 
144         return _cookies;
145       }
146 
147       /// Gets the ip address.
148       string ipAddress()
149       {
150         if (_ipAddress)
151         {
152           return _ipAddress;
153         }
154 
155         _ipAddress = _request.clientAddress.toAddressString();
156 
157         return _ipAddress;
158       }
159 
160       /// Gets the raw request stream.
161       auto requestStream() { return _request.bodyReader; }
162 
163       /// Gets the raw response stream.
164       auto responseStream() { return _response.bodyWriter; }
165 
166       /// Gets a boolean determnining whether the response is connected or not.
167       bool connected() { return _response.connected; }
168 
169       /// Gets the path.
170       string path()
171       {
172         return _path;
173       }
174 
175       /// Sets the path of the client.
176       package(diamond) void path(string newPath)
177       {
178         _path = newPath;
179       }
180 
181       /// Gets the query string.
182       string queryString() { return _request.queryString; }
183 
184       /// Gets a mapped query of the query string.
185       auto query() { return _request.query; }
186 
187       /// Gets the generic http parameters.
188       auto httpParams() { return _request.params; }
189 
190       /// Gets the files from the request.
191       auto files() { return _request.files; }
192 
193       /// Gets the form from the request.
194       auto form() { return _request.form; }
195 
196       /// Gets the full url from the request.
197       auto fullUrl() { return _request.fullURL; }
198 
199       /// Gets the json from the request.
200       auto json() { return _request.json; }
201 
202       /// Gets the content type from the request.
203       string contentType() { return _request.contentType; }
204 
205       /// Gets the content type parameters from the request.
206       string contentTypeParameters() { return _request.contentTypeParameters; }
207 
208       /// Gets the host from the request.
209       string host() { return _request.host; }
210 
211       /// Gets the headers from the request.
212       auto headers() { return _request.headers; }
213 
214       /// Gets a boolean determnining whether the request was done over a secure tls protocol.
215       bool tls() { return _request.tls; }
216 
217       /// Gets the raw client address of the request.
218       auto clientAddress() { return _request.clientAddress; }
219 
220       /// Gets the client certificate from the request.
221       auto clientCertificate() { return _request.clientCertificate; }
222 
223       /// Boolean determining whether the client has been redirected or not.
224       bool redirected() { return _redirected; }
225 
226       /// Gets the role associated with the client.
227       Role role()
228       {
229         if (_role)
230         {
231           return _role;
232         }
233 
234         if (!_role)
235         {
236           validateAuthentication(this);
237         }
238 
239         _role = getRole(this);
240 
241         return _role;
242       }
243 
244       /// Gets the status code of the response.
245       HttpStatus statusCode() { return _statusCode; }
246 
247       /// Gets the language of the client.
248       string language()
249       {
250         if (_language is null)
251         {
252           _language = session.getValue!string(languageSessionKey, "");
253         }
254 
255         return _language;
256       }
257 
258       /// Sets the language of the client.
259       void language(string newLanguage)
260       {
261         _language = newLanguage;
262         session.setValue(languageSessionKey, _language);
263       }
264     }
265 
266     /// Gets a model from the request's json.
267     T getModelFromJson(T, CTORARGS...)(CTORARGS args)
268     {
269       import vibe.data.json;
270 
271       static if (is(T == struct))
272       {
273         T value;
274 
275         value.deserializeJson(_request.json);
276 
277         return value;
278       }
279       else static if (is(T == class))
280       {
281         auto value = new T(args);
282 
283         value.deserializeJson(_request.json);
284 
285         return value;
286       }
287       else
288       {
289         static assert(0);
290       }
291     }
292 
293     /**
294     * Adds a generic context value to the client.
295     * Params:
296     *   name =  The name of the value.
297     *   value = The value.
298     */
299     void addContext(T)(string name, T value)
300     {
301       _request.context[name] = value;
302     }
303 
304     /**
305     * Gets a value from the client's context.
306     * Params:
307     *   name =          The name of the value to retrieve.
308     *   defaultValue =  The default value to retrieve if the value wasn't found in the context.
309     * Returns:
310     *   The value if found, defaultValue otherwise.
311     */
312     T getContext(T)(string name, lazy T defaultValue = T.init)
313     {
314       import std.variant : Variant;
315       Variant value = _request.context.get(name, Variant.init);
316 
317       if (!value.hasValue)
318       {
319         return defaultValue;
320       }
321 
322       return value.get!T;
323     }
324 
325     /**
326     * Checks whether a value is present n the client's context or not.
327     * Params:
328     *   name = The name to check for existence.
329     * Returns:
330     *   True if the value is present, false otherwise.
331     */
332     bool hasContext(string name)
333     {
334       import std.variant : Variant;
335 
336       return _request.context.get(name, Variant.init).hasValue;
337     }
338 
339     /**
340     * Redirects the client.
341     * Params:
342     *   url =    The url to redirect the client to.
343     *   status = The status of the redirection.
344     */
345     void redirect(string url, HttpStatus status = HttpStatus.found)
346     {
347       _response.redirect(url, status);
348 
349       import diamond.core.webconfig;
350       foreach (headerKey,headerValue; webConfig.defaultHeaders.general)
351       {
352         _response.headers[headerKey] = headerValue;
353       }
354 
355       _redirected = true;
356     }
357 
358     /**
359     * Throws a http status exception.
360     * Params:
361     *   status = The status.
362     * Throws:
363     *   Always throws HTTPStatusException.
364     */
365     void error(HttpStatus status)
366     {
367       _statusCode = status;
368       _response.statusCode = _statusCode;
369 
370       throw new HTTPStatusException(status);
371     }
372 
373     /// Sends a 404 status.
374     void notFound()
375     {
376       error(HttpStatus.notFound);
377     }
378 
379     /// Sends an unauthorized error
380     void unauthorized() {
381       error(HttpStatus.unauthorized);
382     }
383 
384     /**
385     * Logs the client in.
386     * Params:
387     *   loginTime = The time the client should be logged in.
388     *   role =      The role the client should be after being logged in.
389     */
390     void login(long loginTime, Role role)
391     {
392       import diamondauth = diamond.authentication;
393       diamondauth.login(this, loginTime, role);
394     }
395 
396     /// Logs the client out.
397     void logout()
398     {
399       import diamondauth = diamond.authentication;
400       diamondauth.logout(this);
401     }
402 
403     /**
404     * Writes data to the response stream.
405     * Params:
406     *   data = The data to write.
407     */
408     void write(string data)
409     {
410       static if (loggingEnabled)
411       {
412         _data ~= cast(ubyte[])data;
413       }
414 
415       _response.bodyWriter.write(data);
416     }
417 
418     /**
419     * Writes data to the response stream.
420     * Params:
421     *   data = The data to write.
422     */
423     void write(ubyte[] data)
424     {
425       static if (loggingEnabled)
426       {
427         _data ~= data;
428       }
429 
430       _response.bodyWriter.write(data);
431     }
432 
433     static if (loggingEnabled)
434     {
435       /// Gets the body data from the response stream.
436       package(diamond) ubyte[] getBody()
437       {
438         return _data;
439       }
440     }
441   }
442 }