$_SESSION['username'], 'address' => session_remote_address(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'result' => $auth_log], 'authlog'); if (isset($_COOKIE['ckey'])) { dbDelete('users_ckeys', "`username` = ? AND `user_ckey` = ?", [$_SESSION['username'], $_COOKIE['ckey']]); } // Remove old ckeys from DB // Unset cookies $cookie_params = session_get_cookie_params(); $past = time() - 3600; foreach ($_COOKIE as $cookie => $value) { if (empty($cookie_params['domain'])) { setcookie($cookie, '', $past, $cookie_params['path']); } else { setcookie($cookie, '', $past, $cookie_params['path'], $cookie_params['domain']); } } unset($_COOKIE); // Clean cache if possible $cache_tags = ['__anonymous']; if ($_SESSION['authenticated']) { $cache_tags = ['__username=' . $_SESSION['username']]; } del_cache_items($cache_tags); // Unset session @session_start(); if ($relogin) { // Reset session and relogin (for example: HTTP auth) $_SESSION['relogin'] = TRUE; unset($_SESSION['authenticated'], $_SESSION['user_id'], $_SESSION['username'], $_SESSION['user_encpass'], $_SESSION['password'], $_SESSION['userlevel']); session_write_close(); session_regenerate_id(TRUE); } else { // Kill current session, as authentication failed unset($_SESSION); session_unset(); session_destroy(); session_write_close(); //setcookie(session_name(),'',0,'/'); session_regenerate_id(TRUE); // Re-set auth failure message for use on login page //session_start(); $_SESSION['auth_message'] = $message; } } /** * Safe session_regenerate_id that doesn't loose session * * Code borrowed from https://www.php.net/manual/en/function.session-regenerate-id.php */ function safe_session_regenerate_id() { global $debug_auth; // New session ID is required to set proper session ID // when session ID is not set due to unstable network. $new_session_id = session_create_id(); if ($debug_auth) { logfile('debug_auth.log', __LINE__ . " Session ID regenerated. IP=[" . get_remote_addr($GLOBALS['config']['web_session_ip_by_header']) . "]. ID=[$new_session_id]." . web_debug_log_message()); } $_SESSION['new_session_id'] = $new_session_id; // Set destroy timestamp $_SESSION['destroyed'] = time(); // Write and close current session; session_write_close(); $tmp = $_SESSION; // Somehow the SESSION data is not kept // Start session with new session ID session_id($new_session_id); ini_set('session.use_strict_mode', 0); session_start(); ini_set('session.use_strict_mode', 1); $_SESSION = $tmp; // New session does not need them unset($_SESSION['destroyed'], $_SESSION['new_session_id']); } /** * Regenerate session ID for prevent attacks session hijacking and session fixation. * Note, use this function after session_start() and before next session_commit()! * * Code borrowed from https://www.php.net/manual/en/function.session-regenerate-id.php * * @param int $lifetime_id Time in seconds for next regenerate session ID (default 30 min) */ function session_regenerate($lifetime_id = 1800) { session_start(); if (isset($_SESSION['destroyed'])) { // logfile('debug_auth.log', __LINE__ . ' session destroyed ' . json_encode($_SESSION)); if ($_SESSION['destroyed'] < time() - 300) { // logfile('debug_auth.log', __LINE__ . ' < 300 - logout'); // Should not happen usually. This could be attack or due to unstable network. // Remove all authentication status of this users session. session_logout(TRUE, 'Session destroyed'); return; } if (isset($_SESSION['new_session_id'])) { // logfile('debug_auth.log', __LINE__ . ' got new_session_id: ' . $_SESSION['new_session_id']); // Not fully expired yet. Could be lost cookie by unstable network. // Try again to set proper session ID cookie. // NOTE: Do not try to set session ID again if you would like to remove // authentication flag. session_write_close(); session_id($_SESSION['new_session_id']); // New session ID should exist session_start(); return; } } $currenttime = time(); if ($lifetime_id != 1800 && is_numeric($lifetime_id) && $lifetime_id >= 300) { $lifetime_id = (int)$lifetime_id; } else { $lifetime_id = 1800; } if (isset($_SESSION['starttime'])) { // logfile('debug_auth.log', __LINE__ . ' ' . json_encode([$_SESSION['starttime'], $currenttime, $currenttime - $_SESSION['starttime']])); if (($currenttime - $_SESSION['starttime']) >= $lifetime_id && !is_graph() && !is_ajax()) // Skip regenerate in graphs and in ajax { // logfile('debug_auth.log', __LINE__ . ' ' . 'session_regenerate() called at '.$currenttime); // ID Lifetime expired, regenerate safe_session_regenerate_id(); // Clean cache from _SESSION first, this cache used in ajax calls if (isset($_SESSION['cache'])) { unset($_SESSION['cache']); } $_SESSION['starttime'] = $currenttime; } } else { $_SESSION['starttime'] = $currenttime; } } /** * Store encrypted password in $_SESSION['user_encpass'], required for some auth mechanism, ie ldap * * @param string $auth_password Plain password * @param string $key Key for password encrypt * * @return string Encrypted password */ function session_encrypt_password($auth_password, $key) { // Store encrypted password if ($GLOBALS['config']['auth_mechanism'] === 'ldap' && !($GLOBALS['config']['auth_ldap_bindanonymous'] || !safe_empty($GLOBALS['config']['auth_ldap_binddn'] . $GLOBALS['config']['auth_ldap_bindpw']))) { if (OBS_ENCRYPT) { if (OBS_ENCRYPT_MODULE === 'mcrypt') { $key .= get_unique_id(); } // For some admin LDAP functions required store encrypted password in session (userslist) session_set_var('user_encpass', encrypt($auth_password, $key)); } else { //session_set_var('user_encpass', base64_encode($auth_password)); session_set_var('encrypt_required', 1); } } return $_SESSION['user_encpass']; } // DOCME needs phpdoc block function session_is_active() { if (!is_cli()) { // logfile('debug_auth.log', __LINE__ . " CONSTANT? ".var_export(session_status(), TRUE)." vs ".var_export(PHP_SESSION_ACTIVE, TRUE)); return session_status() === PHP_SESSION_ACTIVE; } // logfile('debug_auth.log', __LINE__ . " CLI?"); return FALSE; } /** * Generate unique id for current user/browser, based on some unique params * * @return string */ function session_unique_id() { global $config, $debug_auth; /* WiP. New User-Agent if ($debug_auth && isset($_SERVER['HTTP_SEC_CH_UA'])) { //print_message($_SERVER['HTTP_SEC_CH_UA']); //print_message($_SERVER['HTTP_SEC_CH_UA_MOBILE']); //r($_SERVER); } */ $id = $_SERVER['HTTP_USER_AGENT']; // User agent //$id .= $_SERVER['HTTP_ACCEPT']; // Browser accept headers // WTF, this header different for main and js/ajax queries // Less entropy than HTTP_ACCEPT, but stable! $id .= $_SERVER['HTTP_ACCEPT_ENCODING']; $id .= $_SERVER['HTTP_ACCEPT_LANGUAGE']; if ($config['web_session_ip']) { $remote_ip = get_remote_addr($config['web_session_ip_by_header']); // Remote address by header if configured $config['web_session_ipv6_prefix'] = ltrim($config['web_session_ipv6_prefix'], '/'); if (is_numeric($config['web_session_ipv6_prefix']) && $config['web_session_ipv6_prefix'] >= 1 && $config['web_session_ipv6_prefix'] <= 127 && get_ip_version($remote_ip) === 6) { $remote_addr = Net_IPv6 ::getNetmask($remote_ip, (int)$config['web_session_ipv6_prefix']); } else { $remote_addr = $remote_ip; } $id .= $remote_addr; // User IP address // Force reauth if remote IP changed if ($_SESSION['authenticated']) { if (isset($_SESSION['PREV_REMOTE_ADDR']) && $remote_addr !== $_SESSION['PREV_REMOTE_ADDR']) { if ($debug_auth) { logfile('debug_auth.log', __LINE__ . " IP changed. IP=[$remote_ip]. ID=[$id]." . web_debug_log_message()); logfile('debug_auth.log', __LINE__ . ' ' . var_export($_SESSION, TRUE)); } // FIXME. Hrm, I forgot why not just session_logout() //session_logout(); unset($_SESSION['authenticated'], $_SESSION['user_id'], $_SESSION['username'], $_SESSION['user_encpass'], $_SESSION['password'], $_SESSION['userlevel'], $_SESSION['PREV_REMOTE_ADDR']); reauth_with_message('Remote IP has changed.'); } session_set_var('PREV_REMOTE_ADDR', $remote_addr); // Store current remote IP } } // Force reauth if user-agent or login changed if ($_SESSION['authenticated']) { // Check and validate if User agent not changed if (session_useragent_changed()) { if ($debug_auth) { logfile('debug_auth.log', __LINE__ . " UA changed. IP=[" . get_remote_addr($config['web_session_ip_by_header']) . "]. ID=[$id]." . web_debug_log_message()); logfile('debug_auth.log', __LINE__ . ' ' . var_export($_SESSION, TRUE)); } session_logout(); reauth_with_message('Browser has changed.'); } // Ie in API request can force different user if (session_user_changed()) { if ($debug_auth) { logfile('debug_auth.log', __LINE__ . " Username changed. IP=[" . get_remote_addr($config['web_session_ip_by_header']) . "]. ID=[$id]." . web_debug_log_message()); logfile('debug_auth.log', __LINE__ . ' ' . var_export($_SESSION, TRUE)); } session_logout(); reauth_with_message('User login has changed.'); } //if ($debug_auth) //{ // logfile('debug_auth.log', __LINE__ . ' ' . "IP=[".get_remote_addr($config['web_session_ip_by_header'])."]. ID=[$id]. URL=[".$_SERVER['REQUEST_URI']."]"); //} } $user_unique_id = md5($id); // Next required JS cals: // resolution = screen.width+"x"+screen.height+"x"+screen.colorDepth; // timezone = new Date().getTimezoneOffset(); if (FALSE && $debug_auth) { $debug_log_array = [$user_unique_id, $remote_addr, $_SERVER['HTTP_USER_AGENT'], $_SERVER['HTTP_ACCEPT_ENCODING'], $_SERVER['HTTP_ACCEPT_LANGUAGE'], $_COOKIE['OBSID']]; logfile('debug_auth.log', __LINE__ . ' ' . json_encode($debug_log_array)); } //print_vars($id); return $user_unique_id; } function session_useragent_changed() { $ua = md5($_SERVER['HTTP_USER_AGENT']); if (!isset($_SESSION['ua'])) { session_set_var('ua', $ua); return FALSE; } return $_SESSION['ua'] !== $ua; } function session_user_changed() { if (isset($_GET['username'], $_GET['password']) && $_GET['username'] !== $_SESSION['username']) { // GET Auth return TRUE; } if (isset($_POST['username'], $_POST['password']) && $_POST['username'] !== $_SESSION['username']) { // POST Auth return TRUE; } if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) && $_SERVER['PHP_AUTH_USER'] !== $_SESSION['username']) { // Basic Auth return TRUE; } return FALSE; } /** * Use this function to write to the $_SESSION global variable. * This function prevents session blocking. * If the value is NULL, the session variable will be unset. * * @param string|array $var A string representing the key or nested keys (e.g., 'key1->key2->key3') or an array of keys. * @param mixed $value The value to set or NULL to unset the session variable. */ function session_set_var($var, $value) { // Extract nested keys if they exist $keys = is_array($var) ? $var : explode('->', $var); // Start the session (unblock) @session_start(); // Check if the session variable is unchanged and return early if it is if (get_value_by_keys($_SESSION, $keys) === $value) { return; } // Set or unset the session variable based on the value if (is_null($value)) { unset_value_by_keys($_SESSION, $keys); } else { set_value_by_keys($_SESSION, $keys, $value); } // Commit the session (write and block) session_write_close(); } /** * Unset a session variable using the provided key or nested keys. * * @param string $var A string representing the key or nested keys (e.g., 'key1->key2->key3'). */ function session_unset_var($var) { session_set_var($var, NULL); } // Cookies function cookie_get_auth_password($user_unique_id, $debug = FALSE) { if (OBS_ENCRYPT && isset($_COOKIE['ckey']) && is_string($_COOKIE['ckey'])) { ///DEBUG if ($debug) { $debug_log = $GLOBALS['config']['log_dir'] . '/debug_cookie_' . date("Y-m-d_H:i:s") . '.log'; file_put_contents($debug_log, var_export($_SERVER, TRUE), FILE_APPEND); file_put_contents($debug_log, var_export($_SESSION, TRUE), FILE_APPEND); file_put_contents($debug_log, var_export($_COOKIE, TRUE), FILE_APPEND); } $ckey = dbFetchRow("SELECT * FROM `users_ckeys` WHERE `user_uniq` = ? AND `user_ckey` = ? LIMIT 1", [ $user_unique_id, $_COOKIE['ckey'] ]); if (is_array($ckey) && $ckey['expire'] > get_time() && session_allow_cidr()) { // If userlevel == 0 - user disabled and can not be logon if (auth_user_level($ckey['username']) < 1) { session_logout(FALSE, 'User disabled'); reauth_with_message('User login disabled'); return FALSE; } session_set_var('username', $ckey['username']); $auth_password = decrypt($ckey['user_encpass'], $_COOKIE['dkey']); // Store encrypted password session_encrypt_password($auth_password, $user_unique_id); session_set_var('user_ckey_id', $ckey['user_ckey_id']); session_set_var('cookie_auth', TRUE); dbInsert([ 'user' => $_SESSION['username'], 'address' => session_remote_address(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'result' => 'Logged In (cookie)' ], 'authlog'); //logfile('wui_auth_cookie.log', var_export($_SERVER, TRUE)); ///DEBUG return $auth_password; } } return FALSE; } function cookie_set_keys($user_unique_id, $auth_password, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, $cookie_httponly = FALSE) { if (OBS_ENCRYPT && isset($_POST['remember'])) { $ckey = md5(random_string()); $dkey = md5(random_string()); $encpass = encrypt($auth_password, $dkey); dbDelete('users_ckeys', "`username` = ? AND `expire` < ?", [ $_SESSION['username'], get_time() - 3600 ]); // Remove old ckeys from DB dbInsert(['user_encpass' => $encpass, 'expire' => $cookie_expire, 'username' => $_SESSION['username'], 'user_uniq' => $user_unique_id, 'user_ckey' => $ckey], 'users_ckeys'); // AJAX request not have access to cookies with httponly, and for example widgets lost auth //setcookie("ckey", $ckey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, $cookie_httponly); //setcookie("dkey", $dkey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, $cookie_httponly); setcookie("ckey", $ckey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, FALSE); setcookie("dkey", $dkey, $cookie_expire, $cookie_path, $cookie_domain, $cookie_https, FALSE); session_unset_var('user_ckey_id'); } } function cookie_auth($cookie_expire) { if (isset($_SESSION['cookie_auth']) && $_SESSION['cookie_auth']) { @session_start(); $_SESSION['authenticated'] = TRUE; dbUpdate([ 'expire' => $cookie_expire ], 'users_ckeys', '`user_ckey_id` = ?', [ $_SESSION['user_ckey_id'] ]); unset($_SESSION['user_ckey_id'], $_SESSION['cookie_auth']); session_write_close(); return TRUE; } return FALSE; } function cookie_clean_user() { if (isset($_COOKIE['password'])) { setcookie("password", NULL); } if (isset($_COOKIE['username'])) { setcookie("username", NULL); } if (isset($_COOKIE['user_id'])) { setcookie("user_id", NULL); } } /** * Every time you call session_start(), PHP adds another * identical session cookie to the response header. Do this * enough times, and your response header becomes big enough * to choke the web server. * * This method clears out the duplicate session cookies. You can * call it after each time you've called session_start(), or call it * just before you send your headers. */ function clear_duplicate_cookies() { // If headers have already been sent, there's nothing we can do if (headers_sent()) { return; } $cookies = []; foreach (headers_list() as $header) { // Identify cookie headers if (str_starts_with($header, 'Set-Cookie:')) { $cookies[] = $header; } } // Removes all cookie headers, including duplicates header_remove('Set-Cookie'); // Restore one copy of each cookie foreach (array_unique($cookies) as $cookie) { header($cookie, FALSE); } } /** * Redirects to the front page with the specified authentication failure message. * In the case of 'remote', no redirect is performed (as this would create an infinite loop, * as there is no way to logout), so the message is simply printed. * * @param string $message Message to display to the user */ function reauth_with_message($message) { global $config; // Detect AJAX request, do not write any messages or redirects there! if (OBS_API || OBS_AJAX) { // FIXME. But probably here required redirect to requested ajax page with params.. return; } if ($config['auth_mechanism'] === 'remote') { print('