1: <?php
2:
3: /**
4: * Session handling
5: * @package framework
6: * @subpackage session
7: */
8:
9: /**
10: * Use for browser fingerprinting
11: */
12: trait Hm_Session_Fingerprint {
13:
14: /**
15: * Save a value in the session
16: * @param string $name the name to save
17: * @param string $value the value to save
18: * @return void
19: */
20: abstract protected function set($name, $value);
21:
22: /**
23: * Destroy a session for good
24: * @param object $request request details
25: * @return void
26: */
27: abstract protected function destroy($request);
28:
29: /**
30: * Return a session value, or a user settings value stored in the session
31: * @param string $name session value name to return
32: * @param string $default value to return if $name is not found
33: * @return mixed the value if found, otherwise $default
34: */
35: abstract protected function get($name, $default = false);
36:
37: /**
38: * Check HTTP header "fingerprint" against the session value
39: * @param object $request request details
40: * @return void
41: */
42: public function check_fingerprint($request) {
43: if ($this->site_config->get('disable_fingerprint')) {
44: return;
45: }
46: $id = $this->build_fingerprint($request->server);
47: $fingerprint = $this->get('fingerprint', null);
48: if ($fingerprint === false) {
49: $this->set_fingerprint($request);
50: return;
51: }
52: if (!$fingerprint || $fingerprint !== $id) {
53: Hm_Debug::add('HTTP header fingerprint check failed');
54: $this->destroy($request);
55: }
56: }
57:
58: /**
59: * Browser request properties to build a fingerprint with
60: * @return array
61: */
62: private function fingerprint_flds() {
63: $flds = ['HTTP_USER_AGENT', 'REQUEST_SCHEME', 'HTTP_ACCEPT_LANGUAGE',
64: 'HTTP_ACCEPT_CHARSET', 'HTTP_HOST'];
65: if (!$this->site_config->get('allow_long_session') && !$this->site_config->get('disable_ip_check')) {
66: $flds[] = 'REMOTE_ADDR';
67: }
68: return $flds;
69: }
70:
71: /**
72: * Build HTTP header "fingerprint"
73: * @param array $env server env values
74: * @return string fingerprint value
75: */
76: public function build_fingerprint($env, $input = '') {
77: $id = $input;
78: foreach ($this->fingerprint_flds() as $val) {
79: $id .= (array_key_exists($val, $env)) ? $env[$val] : '';
80: }
81: return hash('sha256', $id);
82: }
83:
84: /**
85: * Save a fingerprint in the session
86: * @param object $request request details
87: * @return void
88: */
89: protected function set_fingerprint($request) {
90: $id = $this->build_fingerprint($request->server);
91: $this->set('fingerprint', $id);
92: }
93: }
94:
95: /**
96: * Base class for session management. All session interaction happens through
97: * classes that extend this.
98: * @abstract
99: */
100: abstract class Hm_Session {
101:
102: use Hm_Session_Fingerprint;
103:
104: /* set to true if the session was just loaded on this request */
105: public $loaded = false;
106:
107: /* set to true if the session is active */
108: public $active = false;
109:
110: /* set to true if the user authentication is local (DB) */
111: public $internal_users = false;
112:
113: /* key used to encrypt session data */
114: public $enc_key = '';
115:
116: /* authentication class name */
117: public $auth_class;
118:
119: /* site config object */
120: public $site_config;
121:
122: /* session data */
123: protected $data = [];
124:
125: /* session cookie name */
126: protected $cname = 'hm_session';
127:
128: /* authentication object */
129: protected $auth_mech;
130:
131: /* close early flag */
132: protected $session_closed = false;
133:
134: /* session key */
135: public $session_key = '';
136:
137: /* session lifetime */
138: public $lifetime = 0;
139:
140: /**
141: * check for an active session or an attempt to start one
142: * @param object $request request object
143: * @return bool
144: */
145: abstract protected function check($request);
146:
147: /**
148: * Start the session. This could be an existing session or a new login
149: * @param object $request request details
150: * @return void
151: */
152: abstract protected function start($request);
153:
154: /**
155: * Call the configured authentication method to check user credentials
156: * @param string $user username
157: * @param string $pass password
158: * @return bool true if the authentication was successful
159: */
160: abstract protected function auth($user, $pass);
161:
162: /**
163: * Delete a value from the session
164: * @param string $name name of value to delete
165: * @return void
166: */
167: abstract protected function del($name);
168:
169: /**
170: * End a session after a page request is complete. This only closes the session and
171: * does not destroy it
172: * @return void
173: */
174: abstract protected function end();
175:
176: /**
177: * Setup initial data
178: * @param object $config site config
179: * @param string $auth_type authentication class
180: */
181: public function __construct($config, $auth_type='Hm_Auth_DB') {
182: $this->site_config = $config;
183: $this->auth_class = $auth_type;
184: $this->internal_users = $auth_type::$internal_users;
185: }
186:
187: /**
188: * Lazy loader for the auth mech so modules can define their own
189: * overrides
190: * @return void
191: */
192: protected function load_auth_mech() {
193: if (!is_object($this->auth_mech)) {
194: $this->auth_mech = new $this->auth_class($this->site_config);
195: }
196: }
197:
198: /**
199: * Dump current session contents
200: * @return array
201: */
202: public function dump() {
203: return $this->data;
204: }
205:
206: /**
207: * Method called on a new login
208: * @return void
209: */
210: protected function just_started() {
211: $this->set('login_time', time());
212: }
213:
214: /**
215: * Record session level changes not yet saved in persistant storage
216: * @param string $value short description of the unsaved value
217: * @return void
218: */
219: public function record_unsaved($value) {
220: $this->data['changed_settings'][] = $value;
221: }
222:
223: /**
224: * Returns bool true if the session is active
225: * @return bool
226: */
227: public function is_active() {
228: return $this->active;
229: }
230:
231: /**
232: * Returns bool true if the user is an admin
233: * @return bool
234: */
235: public function is_admin() {
236: if (!$this->active) {
237: return false;
238: }
239: $admins = array_filter(explode(',', $this->site_config->get('admin_users', '')));
240: if (empty($admins)) {
241: return false;
242: }
243: $user = $this->get('username', '');
244: if (!mb_strlen($user)) {
245: return false;
246: }
247: return in_array($user, $admins, true);
248: }
249:
250: /**
251: * Encrypt session data
252: * @param array $data session data to encrypt
253: * @return string encrypted session data
254: */
255: public function ciphertext($data) {
256: return Hm_Crypt::ciphertext(Hm_transform::stringify($data), $this->enc_key);
257: }
258:
259: /**
260: * Decrypt session data
261: * @param string $data encrypted session data
262: * @return false|array decrpted session data
263: */
264: public function plaintext($data) {
265: return Hm_transform::unstringify(Hm_Crypt::plaintext($data, $this->enc_key));
266: }
267:
268: /**
269: * Set the session level encryption key
270: * @param Hm_Request $request request details
271: * @return void
272: */
273: protected function set_key($request) {
274: $this->enc_key = Hm_Crypt::unique_id();
275: $this->secure_cookie($request, 'hm_id', $this->enc_key, '', '', 'Lax');
276: }
277:
278: /**
279: * Fetch the current encryption key
280: * @param object $request request details
281: * @return void
282: */
283: public function get_key($request) {
284: if (array_key_exists('hm_id', $request->cookie)) {
285: $this->enc_key = $request->cookie['hm_id'];
286: }
287: else {
288: Hm_Debug::add('Unable to get session encryption key');
289: }
290: }
291:
292: /**
293: * @param Hm_Request $request request object
294: * @return string
295: */
296: private function cookie_domain($request) {
297: $domain = $this->site_config->get('cookie_domain', false);
298: if ($domain == 'none') {
299: return '';
300: }
301: if (!$domain && array_key_exists('HTTP_HOST', $request->server)) {
302: $domain = $request->server['HTTP_HOST'];
303: }
304: return $domain;
305: }
306:
307: /**
308: * @param Hm_Request $request request object
309: * @return string
310: */
311: private function cookie_path($request) {
312: $path = $this->site_config->get('cookie_path', false);
313: if ($path == 'none') {
314: $path = '';
315: }
316: if (!$path) {
317: $path = $request->path;
318: }
319: return $path;
320: }
321:
322:
323: /**
324: * Set a cookie, secure if possible
325: * @param object $request request details
326: * @param string $name cookie name
327: * @param string $value cookie value
328: * @param string $path cookie path
329: * @param string $domain cookie domain
330: * @param string $same_site cookie SameSite
331: * @return boolean
332: */
333: public function secure_cookie($request, $name, $value, $path='', $domain='', $same_site = 'Strict') {
334: list($path, $domain, $html_only) = $this->prep_cookie_params($request, $name, $path, $domain);
335: return Hm_Functions::setcookie($name, $value, $this->lifetime, $path, $domain, $request->tls, $html_only, $same_site);
336: }
337:
338: /**
339: * Prep cookie paramaters
340: * @param object $request request details
341: * @param string $name cookie name
342: * @param string $path cookie path
343: * @param string $domain cookie domain
344: * @return array
345: */
346: private function prep_cookie_params($request, $name, $path, $domain) {
347: $html_only = true;
348: if ($name == 'hm_reload_folders') {
349: $html_only = false;
350: }
351: if ($name != 'hm_reload_folders' && !$path && isset($request->path)) {
352: $path = $this->cookie_path($request);
353: }
354: if (!$domain) {
355: $domain = $this->cookie_domain($request);
356: }
357: if (preg_match("/:\d+$/", $domain, $matches)) {
358: $domain = str_replace($matches[0], '', $domain);
359: }
360: return [$path, $domain, $html_only];
361: }
362:
363: /**
364: * Delete a cookie
365: * @param object $request request details
366: * @param string $name cookie name
367: * @param string $path cookie path
368: * @param string $domain cookie domain
369: * @return boolean
370: */
371: public function delete_cookie($request, $name, $path='', $domain='') {
372: list($path, $domain, $html_only) = $this->prep_cookie_params($request, $name, $path, $domain);
373: return Hm_Functions::setcookie($name, '', time()-3600, $path, $domain, $request->tls, $html_only);
374: }
375: }
376:
377:
378: /**
379: * Setup the session and authentication classes based on the site config
380: */
381: class Hm_Session_Setup {
382:
383: private $config;
384: private $auth_type;
385: private $session_type;
386:
387: /**
388: * @param object $config site configuration
389: */
390: public function __construct($config) {
391: $this->config = $config;
392: $this->auth_type = $config->get('auth_type', false);
393: $this->session_type = $config->get('session_type', false);
394:
395: }
396:
397: /**
398: * @return object
399: */
400: public function setup_session() {
401: $auth_class = $this->setup_auth();
402: $session_class = $this->get_session_class();
403: if (!Hm_Functions::class_exists($auth_class)) {
404: Hm_Functions::cease('Invalid auth configuration');
405: }
406: Hm_Debug::add(sprintf('Using %s with %s', $session_class, $auth_class), 'info');
407: return new $session_class($this->config, $auth_class);
408: }
409:
410: /**
411: * @return string
412: */
413: private function get_session_class() {
414: switch ($this->session_type) {
415: case 'DB':
416: $session_class = 'Hm_DB_Session';
417: break;
418: case 'MEM':
419: $session_class = 'Hm_Memcached_Session';
420: break;
421: case 'REDIS':
422: $session_class = 'Hm_Redis_Session';
423: break;
424: case 'custom':
425: $session_class = $this->config->get('session_class', 'Custom_Session');
426: break;
427: }
428: return (isset($session_class) && class_exists($session_class))
429: ? $session_class
430: : 'Hm_PHP_Session';
431: }
432:
433: /**
434: * @return string
435: */
436: private function setup_auth() {
437: $auth_class = $this->standard_auth();
438: if ($auth_class === false) {
439: $auth_class = $this->dynamic_auth();
440: }
441: if ($auth_class === false) {
442: $auth_class = $this->custom_auth();
443: }
444: if ($auth_class === false) {
445: Hm_Functions::cease('Invalid auth configuration');
446: $auth_class = 'Hm_Auth_None';
447: }
448: return $auth_class;
449: }
450:
451: /**
452: * @return string|false
453: */
454: private function dynamic_auth() {
455: if ($this->auth_type == 'dynamic' && in_array('dynamic_login', $this->config->get_modules(), true)) {
456: return 'Hm_Auth_Dynamic';
457: }
458: return false;
459: }
460:
461: /**
462: * @return string|false
463: */
464: private function standard_auth() {
465: if ($this->auth_type && in_array($this->auth_type, ['DB', 'LDAP', 'IMAP'], true)) {
466: return sprintf('Hm_Auth_%s', $this->auth_type);
467: }
468: return false;
469: }
470:
471: /**
472: * @return string|false
473: */
474: private function custom_auth() {
475: $custom_auth_class = $this->config->get('auth_class', 'Custom_Auth');
476: if ($this->auth_type == 'custom' && Hm_Functions::class_exists($custom_auth_class)) {
477: return $custom_auth_class;
478: }
479: return false;
480: }
481: }
482: