diff -r d8156d18ac58 -r 97ae8e9d5e29 includes/sessions.php --- a/includes/sessions.php Thu Sep 27 15:55:37 2007 -0400 +++ b/includes/sessions.php Sat Nov 03 07:43:35 2007 -0400 @@ -362,6 +362,7 @@ function start() { global $db, $session, $paths, $template, $plugins; // Common objects + global $lang; if($this->started) return; $this->started = true; $user = false; @@ -381,6 +382,9 @@ if(!$this->compat && $userdata['account_active'] != 1 && $data[1] != 'Special' && $data[1] != 'Admin') { + $language = intval(getConfig('default_language')); + $lang = new Language($language); + $this->logout(); $a = getConfig('account_activation'); switch($a) @@ -480,6 +484,13 @@ } $user = true; + // Set language + if ( !defined('ENANO_ALLOW_LOAD_NOLANG') ) + { + $lang_id = intval($userdata['user_lang']); + $lang = new Language($lang_id); + } + if(isset($_REQUEST['auth']) && !$this->sid_super) { // Now he thinks he's a moderator. Or maybe even an administrator. Let's find out if he's telling the truth. @@ -547,14 +558,55 @@ * @param string $aes_key The MD5 hash of the encryption key, hex-encoded * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt * @param int $level The privilege level we're authenticating for, defaults to 0 + * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. + * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. * @return string 'success' on success, or error string on failure */ - function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER) + function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) { global $db, $session, $paths, $template, $plugins; // Common objects $privcache = $this->private_key; + + if ( !defined('IN_ENANO_INSTALL') ) + { + // Lockout stuff + $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; + $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; + // convert to minutes + $duration = $duration * 60; + $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; + if ( $policy == 'captcha' && $captcha_hash && $captcha_code ) + { + // policy is captcha -- check if it's correct, and if so, bypass lockout check + $real_code = $this->get_captcha($captcha_hash); + } + if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + $timestamp_cutoff = time() - $duration; + $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); + $fails = $db->numrows(); + if ( $fails >= $threshold ) + { + // ooh boy, somebody's in trouble ;-) + $row = $db->fetchrow(); + $db->free_result(); + return array( + 'success' => false, + 'error' => 'locked_out', + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'lockout_policy' => $policy, + 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), + 'lockout_last_time' => $row['timestamp'] + ); + } + $db->free_result(); + } + } // Instanciate the Rijndael encryption object $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE); @@ -563,13 +615,19 @@ $aes_key = $this->fetch_public_key($aes_key); if(!$aes_key) - return 'Couldn\'t look up public key "'.$aes_key.'" for decryption'; + return array( + 'success' => false, + 'error' => 'key_not_found' + ); // Convert the key to a binary string $bin_key = hexdecode($aes_key); if(strlen($bin_key) != AES_BITS / 8) - return 'The decryption key is the wrong length'; + return array( + 'success' => false, + 'error' => 'key_wrong_length' + ); // Decrypt our password $password = $aes->decrypt($aes_data, $bin_key, ENC_HEX); @@ -585,13 +643,34 @@ $this->sql('SELECT password,old_encryption,user_id,user_level,theme,style,temp_password,temp_password_time FROM '.table_prefix.'users WHERE lcase(username)=\''.$db_username_lower.'\' OR username=\'' . $db_username . '\';'); if($db->numrows() < 1) { - return "The username and/or password is incorrect.\n$db->latest_query"; // This wasn't logged in <1.0.2, dunno how it slipped through if($level > USER_LEVEL_MEMBER) $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); else $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); - + + if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + // increment fail count + $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');'); + $fails++; + // ooh boy, somebody's in trouble ;-) + return array( + 'success' => false, + 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'time_rem' => ( $duration / 60 ), + 'lockout_policy' => $policy + ); + } + + return array( + 'success' => false, + 'error' => 'invalid_credentials' + ); } $row = $db->fetchrow(); @@ -642,7 +721,10 @@ if($success) { if($level > $row['user_level']) - return 'You are not authorized for this level of access.'; + return array( + 'success' => false, + 'error' => 'too_big_for_britches' + ); $sess = $this->register_session(intval($row['user_id']), $username, $password, $level); if($sess) @@ -662,10 +744,15 @@ { eval($cmd); } - return 'success'; + return array( + 'success' => true + ); } else - return 'Your login credentials were correct, but an internal error occurred while registering the session key in the database.'; + return array( + 'success' => false, + 'error' => 'backend_fail' + ); } else { @@ -674,7 +761,28 @@ else $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); - return 'The username and/or password is incorrect.'; + // Do we also need to increment the lockout countdown? + if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + // increment fail count + $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');'); + $fails++; + return array( + 'success' => false, + 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'time_rem' => ( $duration / 60 ), + 'lockout_policy' => $policy + ); + } + + return array( + 'success' => false, + 'error' => 'invalid_credentials' + ); } } @@ -700,6 +808,45 @@ return $this->login_compat($username, $pass_hashed, $level); } + if ( !defined('IN_ENANO_INSTALL') ) + { + // Lockout stuff + $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; + $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; + // convert to minutes + $duration = $duration * 60; + $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; + if ( $policy == 'captcha' && $captcha_hash && $captcha_code ) + { + // policy is captcha -- check if it's correct, and if so, bypass lockout check + $real_code = $this->get_captcha($captcha_hash); + } + if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + $timestamp_cutoff = time() - $duration; + $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); + $fails = $db->numrows(); + if ( $fails > $threshold ) + { + // ooh boy, somebody's in trouble ;-) + $row = $db->fetchrow(); + $db->free_result(); + return array( + 'success' => false, + 'error' => 'locked_out', + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'lockout_policy' => $policy, + 'time_rem' => $duration - round( ( time() - $row['timestamp'] ) / 60 ), + 'lockout_last_time' => $row['timestamp'] + ); + } + $db->free_result(); + } + } + // Instanciate the Rijndael encryption object $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE); @@ -709,7 +856,35 @@ // Retrieve the real password from the database $this->sql('SELECT password,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix.'users WHERE lcase(username)=\''.$this->prepare_text(strtolower($username)).'\';'); if($db->numrows() < 1) - return 'The username and/or password is incorrect.'; + { + // This wasn't logged in <1.0.2, dunno how it slipped through + if($level > USER_LEVEL_MEMBER) + $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); + else + $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); + + // Do we also need to increment the lockout countdown? + if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + // increment fail count + $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');'); + $fails++; + return array( + 'success' => false, + 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'lockout_policy' => $policy + ); + } + + return array( + 'success' => false, + 'error' => 'invalid_credentials' + ); + } $row = $db->fetchrow(); // Check to see if we're logging in using a temporary password @@ -758,7 +933,10 @@ if($success) { if((int)$level > (int)$row['user_level']) - return 'You are not authorized for this level of access.'; + return array( + 'success' => false, + 'error' => 'too_big_for_britches' + ); $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level); if($sess) { @@ -773,10 +951,15 @@ eval($cmd); } - return 'success'; + return array( + 'success' => true + ); } else - return 'Your login credentials were correct, but an internal error occured while registering the session key in the database.'; + return array( + 'success' => false, + 'error' => 'backend_fail' + ); } else { @@ -785,7 +968,27 @@ else $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); - return 'The username and/or password is incorrect.'; + // Do we also need to increment the lockout countdown? + if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + // increment fail count + $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', UNIX_TIMESTAMP(), \'credential\');'); + $fails++; + return array( + 'success' => false, + 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'lockout_policy' => $policy + ); + } + + return array( + 'success' => false, + 'error' => 'invalid_credentials' + ); } } @@ -857,7 +1060,7 @@ { // Stash it in a cookie // For now, make the cookie last forever, we can change this in 1.1.x - setcookie( 'sid', $session_key, time()+315360000, scriptPath.'/' ); + setcookie( 'sid', $session_key, time()+315360000, scriptPath.'/', null, ( isset($_SERVER['HTTPS']) ) ); $_COOKIE['sid'] = $session_key; } // $keyhash is stored in the database, this is for compatibility with the older DB structure @@ -919,6 +1122,7 @@ function register_guest_session() { global $db, $session, $paths, $template, $plugins; // Common objects + global $lang; $this->username = $_SERVER['REMOTE_ADDR']; $this->user_level = USER_LEVEL_GUEST; if($this->compat || defined('IN_ENANO_INSTALL')) @@ -932,6 +1136,12 @@ $this->style = ( isset($_GET['style']) && file_exists(ENANO_ROOT.'/themes/'.$this->theme . '/css/'.$_GET['style'].'.css' )) ? $_GET['style'] : substr($template->named_theme_list[$this->theme]['default_style'], 0, strlen($template->named_theme_list[$this->theme]['default_style'])-4); } $this->user_id = 1; + if ( !defined('ENANO_ALLOW_LOAD_NOLANG') ) + { + // This is a VERY special case we are allowing. It lets the installer create languages using the Enano API. + $language = intval(getConfig('default_language')); + $lang = new Language($language); + } } /** @@ -959,7 +1169,7 @@ } $keyhash = md5($key); $salt = $db->escape($keydata[3]); - $query = $db->sql_query('SELECT u.user_id AS uid,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,k.source_ip,k.time,k.auth_level,COUNT(p.message_id) AS num_pms,x.* FROM '.table_prefix.'session_keys AS k + $query = $db->sql_query('SELECT u.user_id AS uid,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,k.source_ip,k.time,k.auth_level,COUNT(p.message_id) AS num_pms,u.user_lang,x.* FROM '.table_prefix.'session_keys AS k LEFT JOIN '.table_prefix.'users AS u ON ( u.user_id=k.user_id ) LEFT JOIN '.table_prefix.'users_extra AS x @@ -1114,7 +1324,10 @@ if($level > USER_LEVEL_CHPREF) { $aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE); - if(!$this->user_logged_in || $this->auth_level < USER_LEVEL_MOD) return 'success'; + if(!$this->user_logged_in || $this->auth_level < USER_LEVEL_MOD) + { + return 'success'; + } // Destroy elevated privileges $keyhash = md5(strrev($this->sid_super)); $this->sql('DELETE FROM '.table_prefix.'session_keys WHERE session_key=\''.$keyhash.'\' AND user_id=\'' . $this->user_id . '\';'); @@ -2117,13 +2330,30 @@ return false; } + // cache of permission objects (to save RAM and SQL queries) + static $objcache = array(); + + if ( count($objcache) == 0 ) + { + foreach ( $paths->nslist as $key => $_ ) + { + $objcache[$key] = array(); + } + } + + if ( isset($objcache[$namespace][$page_id]) ) + { + return $objcache[$namespace][$page_id]; + } + //if ( !isset( $paths->pages[$paths->nslist[$namespace] . $page_id] ) ) //{ // // Page does not exist // return false; //} - $object = new Session_ACLPageInfo( $page_id, $namespace, $this->acl_types, $this->acl_descs, $this->acl_deps, $this->acl_base_cache ); + $objcache[$namespace][$page_id] = new Session_ACLPageInfo( $page_id, $namespace, $this->acl_types, $this->acl_descs, $this->acl_deps, $this->acl_base_cache ); + $object =& $objcache[$namespace][$page_id]; return $object;