1<?php
2
3/**
4 * Session handling
5 * @package framework
6 * @subpackage session
7 */
8
9/**
10 * Use for browser fingerprinting
11 */
12trait 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 = array('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 */
100abstract 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 = array();
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 (!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);
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     * @return boolean
331     */
332    public function secure_cookie($request, $name, $value, $path='', $domain='') {
333        list($path, $domain, $html_only) = $this->prep_cookie_params($request, $name, $path, $domain);
334        return Hm_Functions::setcookie($name, $value, $this->lifetime, $path, $domain, $request->tls, $html_only);
335    }
336
337    /**
338     * Prep cookie paramaters
339     * @param object $request request details
340     * @param string $name cookie name
341     * @param string $path cookie path
342     * @param string $domain cookie domain
343     * @return array
344     */
345    private function prep_cookie_params($request, $name, $path, $domain) {
346        $html_only = true;
347        if ($name == 'hm_reload_folders') {
348            $html_only = false;
349        }
350        if ($name != 'hm_reload_folders' && !$path && isset($request->path)) {
351            $path = $this->cookie_path($request);
352        }
353        if (!$domain) {
354            $domain = $this->cookie_domain($request);
355        }
356        if (preg_match("/:\d+$/", $domain, $matches)) {
357            $domain = str_replace($matches[0], '', $domain);
358        }
359        return array($path, $domain, $html_only);
360    }
361
362    /**
363     * Delete a cookie
364     * @param object $request request details
365     * @param string $name cookie name
366     * @param string $path cookie path
367     * @param string $domain cookie domain
368     * @return boolean
369     */
370    public function delete_cookie($request, $name, $path='', $domain='') {
371        list($path, $domain, $html_only) = $this->prep_cookie_params($request, $name, $path, $domain);
372        return Hm_Functions::setcookie($name, '', time()-3600, $path, $domain, $request->tls, $html_only);
373    }
374}
375
376
377/**
378 * Setup the session and authentication classes based on the site config
379 */
380class Hm_Session_Setup {
381
382    private $config;
383    private $auth_type;
384    private $session_type;
385
386    /**
387     * @param object $config site configuration
388     */
389    public function __construct($config) {
390        $this->config = $config;
391        $this->auth_type = $config->get('auth_type', false);
392        $this->session_type = $config->get('session_type', false);
393
394    }
395
396    /**
397     * @return object
398     */
399    public function setup_session() {
400        $auth_class = $this->setup_auth();
401        $session_class = $this->get_session_class();
402        if (!Hm_Functions::class_exists($auth_class)) {
403            Hm_Functions::cease('Invalid auth configuration');
404        }
405        Hm_Debug::add(sprintf('Using %s with %s', $session_class, $auth_class));
406        return new $session_class($this->config, $auth_class);
407    }
408
409    /**
410     * @return string
411     */
412    private function get_session_class() {
413        $custom_session_class = $this->config->get('session_class', 'Custom_Session');
414        if ($this->session_type == 'DB') {
415            $session_class = 'Hm_DB_Session';
416        }
417        elseif ($this->session_type == 'MEM') {
418            $session_class = 'Hm_Memcached_Session';
419        }
420        elseif ($this->session_type == 'REDIS') {
421            $session_class = 'Hm_Redis_Session';
422        }
423        elseif ($this->session_type == 'custom' && class_exists($custom_session_class)) {
424            $session_class = $custom_session_class;
425        }
426        else {
427            $session_class = 'Hm_PHP_Session';
428        }
429        return $session_class;
430    }
431
432    /**
433     * @return string
434     */
435    private function setup_auth() {
436        $auth_class = $this->standard_auth();
437        if ($auth_class === false) {
438            $auth_class = $this->dynamic_auth();
439        }
440        if ($auth_class === false) {
441            $auth_class = $this->custom_auth();
442        }
443        if ($auth_class === false) {
444            Hm_Functions::cease('Invalid auth configuration');
445            $auth_class = 'Hm_Auth_None';
446        }
447        return $auth_class;
448    }
449
450    /**
451     * @return string|false
452     */
453    private function dynamic_auth() {
454        if ($this->auth_type == 'dynamic' && in_array('dynamic_login', $this->config->get_modules(), true)) {
455            return 'Hm_Auth_Dynamic';
456        }
457        return false;
458    }
459
460    /**
461     * @return string|false
462     */
463    private function standard_auth() {
464        if ($this->auth_type && in_array($this->auth_type, array('DB', 'LDAP', 'IMAP', 'POP3'), true)) {
465            return sprintf('Hm_Auth_%s', $this->auth_type);
466        }
467        return false;
468    }
469
470    /**
471     * @return string|false
472     */
473    private function custom_auth() {
474        $custom_auth_class = $this->config->get('auth_class', 'Custom_Auth');
475        if ($this->auth_type == 'custom' && Hm_Functions::class_exists($custom_auth_class)) {
476            return 'Custom_Auth';
477        }
478        return false;
479    }
480}
481