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 }