1 module hunt.http.server.HttpSession; 2 3 import hunt.http.Exceptions; 4 5 import hunt.collection.HashMap; 6 import hunt.collection.Map; 7 import hunt.Exceptions; 8 import hunt.util.CompilerHelper; 9 import hunt.util.DateTime; 10 import hunt.util.Lifecycle; 11 12 import std.algorithm; 13 import std.conv; 14 import std.digest.sha; 15 import std.json; 16 import std.range; 17 import std.string; 18 import std.traits; 19 import std.variant; 20 21 22 enum string DefaultSessionIdName = "hunt_session"; 23 24 25 /** 26 * 27 */ 28 class HttpSession { 29 30 private string id; 31 private long creationTime; 32 private long lastAccessedTime; 33 private int maxInactiveInterval; 34 private JSONValue attributes; 35 private bool newSession; 36 37 string getId() { 38 return id; 39 } 40 41 void setId(string id) { 42 this.id = id; 43 } 44 45 long getCreationTime() { 46 return creationTime; 47 } 48 49 void setCreationTime(long creationTime) { 50 this.creationTime = creationTime; 51 } 52 53 long getLastAccessedTime() { 54 return lastAccessedTime; 55 } 56 57 void setLastAccessedTime(long lastAccessedTime) { 58 this.lastAccessedTime = lastAccessedTime; 59 } 60 61 /** 62 * Get the max inactive interval. The time unit is second. 63 * 64 * @return The max inactive interval. 65 */ 66 int getMaxInactiveInterval() { 67 return maxInactiveInterval; 68 } 69 70 /** 71 * Set the max inactive interval. The time unit is second. 72 * 73 * @param maxInactiveInterval The max inactive interval. 74 */ 75 void setMaxInactiveInterval(int maxInactiveInterval) { 76 this.maxInactiveInterval = maxInactiveInterval; 77 } 78 79 ref JSONValue getAttributes() { 80 return attributes; 81 } 82 83 void setAttributes(ref JSONValue attributes) { 84 this.attributes = attributes; 85 } 86 87 void setAttribute(T)(string name, T value) { 88 this.attributes[name] = value; 89 } 90 91 T getAttribute(T=string)(string name) { 92 JSONValue jv = attributes[name]; 93 static if(CompilerHelper.isGreaterThan(2092)) { 94 return jv.get!T(); 95 } else { 96 static if (is(Unqual!T == string)) { 97 return jv.str; 98 } else static if (is(Unqual!T == bool)) { 99 return jv.boolean; 100 } else static if (isFloatingPoint!T) { 101 switch (type) { 102 case JSONType.float_: 103 return cast(T) jv.floating; 104 case JSONType.uinteger: 105 return cast(T) jv.uinteger; 106 case JSONType.integer: 107 return cast(T) jv.integer; 108 default: 109 throw new JSONException("JSONValue is not a number type"); 110 } 111 } else static if (__traits(isUnsigned, T)) { 112 return cast(T) jv.uinteger; 113 } else static if (isSigned!T) { 114 return cast(T) jv.integer; 115 } else { 116 static assert(false, "Unsupported type: " ~ T.stringof); 117 } 118 } 119 } 120 121 bool isNewSession() { 122 return newSession; 123 } 124 125 void setNewSession(bool newSession) { 126 this.newSession = newSession; 127 } 128 129 bool isValid() { 130 long currentTime = DateTime.currentTimeMillis(); 131 return (currentTime - lastAccessedTime) < (maxInactiveInterval * 1000); 132 } 133 134 void set(T)(string name, T value) { 135 this.attributes[name] = JSONValue(value); 136 } 137 138 T get(T = string)(string name, T defaultValue = T.init) { 139 if(attributes.isNull) 140 return defaultValue; 141 142 const(JSONValue)* itemPtr = name in attributes; 143 if (itemPtr is null) 144 return defaultValue; 145 static if (!is(T == string) && isDynamicArray!T && is(T : U[], U)) { 146 U[] r; 147 foreach (JSONValue jv; itemPtr.array) { 148 r ~= get!U(jv); 149 } 150 return r; 151 } 152 else { 153 return get!T(*itemPtr); 154 } 155 } 156 157 private static T get(T = string)(JSONValue itemPtr) { 158 static if (is(T == string)) { 159 return itemPtr.str; 160 } 161 else static if (isIntegral!T) { 162 return cast(T) itemPtr.integer; 163 } 164 else static if (isFloatingPoint!T) { 165 return cast(T) itemPtr.floating; 166 } 167 else { 168 static assert(false, "Unsupported type: " ~ typeid(T).name); 169 } 170 } 171 172 void remove(string key) { 173 if(attributes.isNull) 174 return; 175 176 JSONValue json; 177 foreach (string _key, ref value; attributes) { 178 if (_key != key) { 179 json[_key] = value; 180 } 181 } 182 183 attributes = json; 184 } 185 186 void remove(string[] keys) { 187 if(attributes.isNull) 188 return; 189 190 JSONValue json; 191 foreach (string _key, ref value; attributes) { 192 if (!keys.canFind(_key)) { 193 json[_key] = value; 194 } 195 } 196 197 attributes = json; 198 } 199 200 alias forget = remove; 201 202 string[] keys() { 203 if(attributes.isNull) 204 return null; 205 206 string[] ret; 207 208 foreach (string key, ref value; attributes) { 209 ret ~= key; 210 } 211 212 return ret; 213 } 214 215 /** 216 * Get all of the session data. 217 * 218 * @return array 219 */ 220 string[string] all() { 221 if (attributes.isNull) 222 return null; 223 224 string[string] v; 225 foreach (string key, ref JSONValue value; attributes) { 226 if (value.type == JSONType..string) 227 v[key] = value.str; 228 else 229 v[key] = value.toString(); 230 } 231 232 return v; 233 } 234 235 /** 236 * Checks if a key exists. 237 * 238 * @param string|array key 239 * @return bool 240 */ 241 bool exists(string key) { 242 if (attributes.isNull) 243 return false; 244 const(JSONValue)* item = key in attributes; 245 return item !is null; 246 } 247 248 /** 249 * Checks if a key is present and not null. 250 * 251 * @param string|array key 252 * @return bool 253 */ 254 bool has(string key) { 255 if (attributes.isNull) 256 return false; 257 258 auto item = key in attributes; 259 if ((item !is null) && (!item.str.empty)) 260 return true; 261 else 262 return false; 263 } 264 265 /** 266 * Get the value of a given key and then forget it. 267 * 268 * @param string key 269 * @param string default 270 * @return mixed 271 */ 272 void pull(string key, string value) { 273 attributes[key] = value; 274 } 275 276 /** 277 * Determine if the session contains old input. 278 * 279 * @param string key 280 * @return bool 281 */ 282 bool hasOldInput(string key) { 283 string old = getOldInput(key); 284 return !old.empty; 285 } 286 287 /** 288 * Get the requested item from the flashed input array. 289 * 290 * @param string key 291 * @param mixed default 292 * @return mixed 293 */ 294 string[string] getOldInput(string[string] defaults = null) { 295 string v = get("_old_input"); 296 if (v.empty) 297 return defaults; 298 else 299 return to!(string[string])(v); 300 } 301 302 /// ditto 303 string getOldInput(string key, string defaults = null) { 304 string old = get("_old_input"); 305 string[string] v = to!(string[string])(old); 306 return v.get(key, defaults); 307 } 308 309 /** 310 * Replace the given session attributes entirely. 311 * 312 * @param array attributes 313 * @return void 314 */ 315 void replace(string[string] attributes) { 316 this.attributes = JSONValue.init; 317 put(attributes); 318 } 319 320 /** 321 * Put a key / value pair or array of key / value pairs in the session. 322 * 323 * @param string|array key 324 * @param mixed value 325 * @return void 326 */ 327 void put(T = string)(string key, T value) { 328 attributes[key] = value; 329 } 330 331 /// ditto 332 void put(string[string] pairs) { 333 foreach (string key, string value; pairs) 334 attributes[key] = value; 335 } 336 337 /** 338 * Get an item from the session, or store the default value. 339 * 340 * @param string key 341 * @param \Closure callback 342 * @return mixed 343 */ 344 string remember(string key, string value) { 345 string v = this.get(key); 346 if (!v.empty) 347 return v; 348 349 this.put(key, value); 350 return value; 351 } 352 353 /** 354 * Push a value onto a session array. 355 * 356 * @param string key 357 * @param mixed value 358 * @return void 359 */ 360 void push(T = string)(string key, T value) { 361 T[] array = this.get!(T[])(key); 362 array ~= value; 363 364 this.put(key, array); 365 } 366 367 /** 368 * Flash a key / value pair to the session. 369 * 370 * @param string key 371 * @param mixed value 372 * @return void 373 */ 374 void flash(T = string)(string key, T value) { 375 this.put(key, value); 376 this.push("_flash.new", key); 377 this.removeFromOldFlashData([key]); 378 } 379 380 /** 381 * Flash a key / value pair to the session for immediate use. 382 * 383 * @param string key 384 * @param mixed value 385 * @return void 386 */ 387 void now(T = string)(string key, T value) { 388 this.put(key, value); 389 this.push("_flash.old", key); 390 } 391 392 /** 393 * Reflash all of the session flash data. 394 * 395 * @return void 396 */ 397 public void reflash() { 398 this.mergeNewFlashes(this.get!(string[])("_flash.old")); 399 this.put!(string[])("_flash.old", []); 400 } 401 402 /** 403 * Reflash a subset of the current flash data. 404 * 405 * @param array|mixed keys 406 * @return void 407 */ 408 void keep(string[] keys...) { 409 string[] ks = keys.dup; 410 mergeNewFlashes(ks); 411 removeFromOldFlashData(ks); 412 } 413 414 /** 415 * Merge new flash keys into the new flash array. 416 * 417 * @param array keys 418 * @return void 419 */ 420 protected void mergeNewFlashes(string[] keys) { 421 string[] oldKeys = this.get!(string[])("_flash.new"); 422 string[] values = oldKeys ~ keys; 423 values = values.sort().uniq().array; 424 425 this.put("_flash.new", values); 426 } 427 428 /** 429 * Remove the given keys from the old flash data. 430 * 431 * @param array keys 432 * @return void 433 */ 434 protected void removeFromOldFlashData(string[] keys) { 435 string[] olds = this.get!(string[])("_flash.old"); 436 string[] news = olds.remove!(x => keys.canFind(x)); 437 this.put("_flash.old", news); 438 } 439 440 /** 441 * Flash an input array to the session. 442 * 443 * @param array value 444 * @return void 445 */ 446 void flashInput(string[string] value) { 447 flash("_old_input", to!string(value)); 448 } 449 450 /** 451 * Flush the session data and regenerate the ID. 452 * 453 * @return bool 454 */ 455 // bool invalidate() 456 // { 457 // flush(); 458 459 // return migrate(true); 460 // } 461 462 /** 463 * Save the session data to storage. 464 */ 465 void save() { 466 string[] olds = this.get!(string[])("_flash.old"); 467 468 remove(olds); 469 470 // attributes 471 string[] news = this.get!(string[])("_flash.new"); 472 this.put("_flash.old", news); 473 this.put!(string[])("_flash.new", []); 474 475 news = this.get!(string[])("_flash.new"); 476 } 477 478 override bool opEquals(Object o) { 479 if (this is o) 480 return true; 481 HttpSession that = cast(HttpSession) o; 482 if (that is null) 483 return false; 484 return id == that.id; 485 } 486 487 override size_t toHash() @trusted nothrow { 488 return hashOf(id); 489 } 490 491 static HttpSession create(string id, int maxInactiveInterval) { 492 long currentTime = DateTime.currentTimeMillis(); 493 HttpSession session = new HttpSession(); 494 session.setId(id); 495 session.setMaxInactiveInterval(maxInactiveInterval); 496 session.setCreationTime(currentTime); 497 session.setLastAccessedTime(session.getCreationTime()); 498 // session.setAttributes(new HashMap!(string, Object)()); 499 session.setNewSession(true); 500 return session; 501 } 502 503 static string toJson(HttpSession session) { 504 JSONValue j; 505 j["CreationTime"] = session.creationTime; 506 j["attr"] = session.attributes; 507 return j.toString(); 508 } 509 510 static HttpSession fromJson(string id, string json) { 511 JSONValue j = parseJSON(json); 512 long currentTime = DateTime.currentTimeMillis(); 513 HttpSession session = new HttpSession(); 514 session.setId(id); 515 session.setCreationTime(j["CreationTime"].integer); 516 session.setLastAccessedTime(currentTime); 517 session.setNewSession(false); 518 session.attributes = j["attr"]; 519 520 return session; 521 } 522 523 static string generateSessionId(string sessionName = DefaultSessionIdName) { 524 SHA1 hash; 525 hash.start(); 526 hash.put(getRandom); 527 ubyte[20] result = hash.finish(); 528 string str = toLower(toHexString(result)); 529 530 return str; 531 } 532 } 533 534 ubyte[] getRandom(ushort len = 64) 535 { 536 assert(len); 537 ubyte[] buffer; 538 buffer.length = len; 539 version(Windows){ 540 import core.sys.windows.wincrypt; 541 import core.sys.windows.windef; 542 HCRYPTPROV hCryptProv; 543 assert(CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) != 0); 544 CryptGenRandom(hCryptProv, cast(DWORD)buffer.length, buffer.ptr); 545 scope(exit)CryptReleaseContext(hCryptProv, 0); 546 }else version(SecureARC4Random){ 547 arc4random_buf(buffer.ptr, len); 548 }else{ 549 import core.stdc.stdio : FILE, _IONBF, fopen, fclose, fread, setvbuf; 550 auto file = fopen("/dev/urandom","rb"); 551 scope(exit)fclose(file); 552 if(file is null)throw new Exception("Failed to open /dev/urandom"); 553 if(setvbuf(file, null, 0, _IONBF) != 0)throw new 554 Exception("Failed to disable buffering for random number file handle"); 555 if(fread(buffer.ptr, buffer.length, 1, file) != 1)throw new 556 Exception("Failed to read next random number"); 557 } 558 return buffer; 559 } 560 561 /** 562 * 563 */ 564 interface SessionStore : Lifecycle { 565 566 bool contains(string key); 567 568 bool remove(string key); 569 570 bool put(string key, HttpSession value); 571 572 HttpSession get(string key); 573 574 int size(); 575 576 } 577 578 /** 579 * 580 */ 581 interface HttpSessionHandler { 582 583 HttpSession getSessionById(string id); 584 585 HttpSession getSession(); 586 587 HttpSession getSession(bool create); 588 589 HttpSession getAndCreateSession(int maxAge); 590 591 int getSessionSize(); 592 593 bool removeSession(); 594 595 bool removeSessionById(string id); 596 597 bool updateSession(HttpSession httpSession); 598 599 bool isRequestedSessionIdFromURL(); 600 601 bool isRequestedSessionIdFromCookie(); 602 603 string getRequestedSessionId(); 604 605 string getSessionIdParameterName(); 606 }