# HG changeset patch # User Dan # Date 1257304128 18000 # Node ID 05fe0039d952fe8c10e1b049f44aae720efb48d4 # Parent adfbe522c95f8d1ab265544fb4ae86c432e59bf4 Logins: reorganized data structures a bit. WiP - needs test routine done. diff -r adfbe522c95f -r 05fe0039d952 includes/clientside/jsres.php --- a/includes/clientside/jsres.php Sun Oct 25 00:09:11 2009 -0400 +++ b/includes/clientside/jsres.php Tue Nov 03 22:08:48 2009 -0500 @@ -12,7 +12,7 @@ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. */ -// define('ENANO_JS_DEBUG', 1); +define('ENANO_JS_DEBUG', 1); // if Enano's already loaded, we've been included from a helper script if ( defined('ENANO_CONFIG_FETCHED') ) diff -r adfbe522c95f -r 05fe0039d952 includes/clientside/static/login.js --- a/includes/clientside/static/login.js Sun Oct 25 00:09:11 2009 -0400 +++ b/includes/clientside/static/login.js Tue Nov 03 22:08:48 2009 -0500 @@ -115,13 +115,14 @@ var title = ( user_level > USER_LEVEL_MEMBER ) ? $lang.get('user_login_ajax_prompt_title_elev') : $lang.get('user_login_ajax_prompt_title'); logindata.mb_object = new MessageBox(MB_OKCANCEL | MB_ICONLOCK, title, ''); + // + // Cancel function: called when the "Cancel" button is clicked + // logindata.mb_object.onclick['Cancel'] = function() { - // Hide the error message and captcha - if ( document.getElementById('ajax_login_error_box') ) - { - document.getElementById('ajax_login_error_box').parentNode.removeChild(document.getElementById('ajax_login_error_box')); - } + // Hide the error message, if any + $('ajax_login_error_box').remove(); + // Hide the captcha, if any if ( document.getElementById('autoCaptcha') ) { var to = fly_out_top(document.getElementById('autoCaptcha'), false, true); @@ -130,7 +131,7 @@ d.parentNode.removeChild(d); }, to); } - // Ask the server to clean our key + // Ask the server to delete the encryption key we're using ajaxLoginPerformRequest({ mode: 'clean_key', key_aes: logindata.key_aes, @@ -138,8 +139,10 @@ }); }; + // Clicking OK will not cause the box to destroy, as this function returns true. logindata.mb_object.onbeforeclick['OK'] = function() { + // Just call the submitter and let it take care of everything ajaxLoginSubmitForm(); return true; } @@ -160,7 +163,8 @@ } /** - * For compatibility only. + * For compatibility only. Really, folks, it's ajaxLoginInit. If you need a + * mnemonic device, use "two 'in's." */ window.ajaxLogonInit = function(call_on_finish, user_level) @@ -170,7 +174,7 @@ /** * Sets the contents of the AJAX login window to the appropriate status message. - * @param int One of AJAX_STATUS_* + * @param int One of AJAX_STATUS_* constants */ window.ajaxLoginSetStatus = function(status) @@ -365,7 +369,7 @@ case null: case undefined: logindata.showing_status = false; - return null; + return; break; } logindata.showing_status = true; @@ -374,6 +378,7 @@ /** * Performs an AJAX logon request to the server and calls ajaxLoginProcessResponse() on the result. * @param object JSON packet to send + * @param function Optional function to call on the response as well. */ window.ajaxLoginPerformRequest = function(json, _hookfunc) @@ -420,12 +425,15 @@ } return false; } + // Main mode switch switch ( response.mode ) { - case 'build_box': + case 'initial': // Rid ourselves of any loading windows ajaxLoginSetStatus(AJAX_STATUS_DESTROY); + // show any errors + ajaxLoginShowFriendlyError(response); // The server wants us to build the login form, all the information is there ajaxLoginBuildForm(response); break; @@ -433,49 +441,33 @@ ajaxLoginSetStatus(AJAX_STATUS_SUCCESS); logindata.successfunc(response.key, response); break; - case 'login_failure': - // Rid ourselves of any loading windows - ajaxLoginSetStatus(AJAX_STATUS_DESTROY); - document.getElementById('messageBox').style.backgroundColor = '#C0C0C0'; - var mb_parent = document.getElementById('messageBox').parentNode; - var do_respawn = ( typeof(response.respawn) == 'boolean' && response.respawn == true ) || typeof(response.respawn) != 'boolean'; - if ( do_respawn ) - { - $(mb_parent).effect("shake", {}, 200); - setTimeout(function() - { - document.getElementById('messageBox').style.backgroundColor = '#FFF'; - - ajaxLoginBuildForm(response.respawn_info); - ajaxLoginShowFriendlyError(response); - }, 2500); - } - else - { - ajaxLoginShowFriendlyError(response); - } - break; - case 'login_success_reset': + case 'reset_pass_used': + // We logged in with a temporary password. Prompt the user to go to the temp password page and + // reset their real password. If they click no, treat it as a login failure, as no session key + // is actually issued when this type of login is performed. + var conf = confirm($lang.get('user_login_ajax_msg_used_temp_pass')); if ( conf ) { var url = makeUrlNS('Special', 'PasswordReset/stage2/' + response.user_id + '/' + response.temp_password); window.location = url; + break; } - else - { - // treat as a failure - ajaxLoginSetStatus(AJAX_STATUS_DESTROY); - document.getElementById('messageBox').style.backgroundColor = '#C0C0C0'; - var mb_parent = document.getElementById('messageBox').parentNode; - $(mb_parent).effect("shake", {}, 1500); - setTimeout(function() - { - document.getElementById('messageBox').style.backgroundColor = '#FFF'; - ajaxLoginBuildForm(response.respawn_info); - // don't show an error here, just silently respawn - }, 2500); - } + // else, treat as a failure + default: + // Rid ourselves of any loading windows + ajaxLoginSetStatus(AJAX_STATUS_DESTROY); + document.getElementById('messageBox').style.backgroundColor = '#C0C0C0'; + var mb_parent = document.getElementById('messageBox').parentNode; + $(mb_parent).effect("shake", {}, 200); + setTimeout(function() + { + document.getElementById('messageBox').style.backgroundColor = '#FFF'; + console.debug(response); + ajaxLoginShowFriendlyError(response); + ajaxLoginBuildForm(response); + }, 2500); + break; case 'logout_success': if ( ENANO_SID ) @@ -512,7 +504,7 @@ var div = document.createElement('div'); div.id = 'ajax_login_form'; - var show_captcha = ( data.locked_out.locked_out && data.locked_out.lockout_policy == 'captcha' ) ? data.locked_out.captcha : false; + var show_captcha = ( data.lockout.active && data.lockout.policy == 'captcha' ) ? data.lockout.captcha : false; // text displayed on re-auth if ( logindata.user_level > USER_LEVEL_MEMBER ) @@ -586,8 +578,6 @@ tr2.appendChild(td2_2); table.appendChild(tr2); - eval(setHook('login_build_form')); - // Field - captcha if ( show_captcha ) { @@ -617,6 +607,14 @@ table.appendChild(tr3); } + // ok, this is a compatibility hack + data.locked_out = { locked_out: data.lockout.active }; + + // hook for the login form + eval(setHook('login_build_form')); + + delete(data.locked_out); + // Done building the main part of the form form.appendChild(table); @@ -696,7 +694,7 @@ lbl_dh.innerHTML = $lang.get('user_login_ajax_check_dh_ie'); boxen.appendChild(lbl_dh); } - else if ( !data.allow_diffiehellman ) + else if ( !data.crypto.dh_enable ) { // create hidden control - server requested that DiffieHellman be disabled (usually means not supported) var check_dh = document.createElement('input'); @@ -769,24 +767,21 @@ // Post operations: show captcha window if ( show_captcha ) + { ajaxShowCaptcha(show_captcha); + } // Post operations: stash encryption keys and All That Jazz(TM) - logindata.key_aes = data.aes_key; - logindata.key_dh = data.dh_public_key; + logindata.key_aes = data.crypto_aes_key; + logindata.key_dh = data.crypto.dh_public_key; logindata.captcha_hash = show_captcha; - logindata.loggedin_username = data.username + logindata.loggedin_username = data.username; - // Are we locked out? If so simulate an error and disable the controls - if ( data.lockout_info.lockout_policy == 'lockout' && data.locked_out.locked_out ) + // If policy is lockout, also disable controls + if ( data.lockout.policy == 'lockout' && data.lockout.active ) { f_username.setAttribute('disabled', 'disabled'); f_password.setAttribute('disabled', 'disabled'); - var fake_packet = { - error_code: 'locked_out', - respawn_info: data - }; - ajaxLoginShowFriendlyError(fake_packet); } } @@ -978,11 +973,10 @@ window.ajaxLoginShowFriendlyError = function(response) { - if ( !response.respawn_info ) - return false; - if ( !response.error_code ) - return false; var text = ajaxLoginGetErrorText(response); + if ( text == false ) + return true; + if ( document.getElementById('ajax_login_error_box') ) { // console.info('Reusing existing error-box'); @@ -1021,85 +1015,53 @@ window.ajaxLoginGetErrorText = function(response) { - if ( !response.error_code.match(/^[a-z0-9]+_[a-z0-9_]+$/) ) + if ( response.lockout ) { - return response.error_code; + // set this pluralality thing + response.lockout.plural = response.lockout.time_rem == 1 ? '' : $lang.get('meta_plural'); } - switch ( response.error_code ) + + if ( response.mode == 'initial' ) { - default: - eval(setHook('ajax_login_process_error')); - if ( !ls ) - { - var ls = $lang.get('user_err_' + response.error_code); - if ( ls == 'user_err_' + response.error_code ) - // Adding response here allows language strings to utilize additional information passed from the error packet - ls = $lang.get(response.error_code, response); - } - - return ls; - break; - case 'locked_out': - if ( response.respawn_info.lockout_info.lockout_policy == 'lockout' ) - { - return $lang.get('user_err_locked_out', { - lockout_threshold: response.respawn_info.lockout_info.lockout_threshold, - lockout_duration: response.respawn_info.lockout_info.lockout_duration, - time_rem: response.respawn_info.lockout_info.time_rem, - plural: ( response.respawn_info.lockout_info.time_rem == 1 ) ? '' : $lang.get('meta_plural'), - captcha_blurb: '' - }); - break; - } - case 'invalid_credentials': - var base = $lang.get('user_err_invalid_credentials'); - if ( response.respawn_info.locked_out.locked_out ) - { - base += ' '; - var captcha_blurb = ''; - switch(response.respawn_info.lockout_info.lockout_policy) + // Just showing the box for the first time. If there's an error now, it's based on a preexisting lockout. + if ( response.lockout.active ) + { + return $lang.get('user_err_locked_out_initial_' + response.lockout.policy, response.lockout); + } + return false; + } + else + { + // An attempt was made. + switch(response.mode) + { + case 'login_failure': + // Generic login user error. + var error = '', x; + if ( (x = $lang.get(response.error)) != response.error ) + error = x; + else + error = $lang.get('user_err_' + response.error); + if ( response.lockout.active && response.lockout.policy == 'lockout' ) { - case 'captcha': - captcha_blurb = $lang.get('user_err_locked_out_captcha_blurb'); - break; - case 'lockout': - break; - default: - base += 'WTF? Shouldn\'t be locked out with lockout policy set to disable. '; - break; + // Lockout enforcement was just activated. + return $lang.get('user_err_locked_out_initial_' + response.lockout.policy, response.lockout); + } + else if ( response.lockout.policy != 'disable' && !response.lockout.active && response.lockout.fails > 0 ) + { + // Lockout is in a warning state. + error += ' ' + $lang.get('user_err_invalid_credentials_' + response.lockout.policy, response.lockout); } - base += $lang.get('user_err_locked_out', { - captcha_blurb: captcha_blurb, - lockout_threshold: response.respawn_info.lockout_info.lockout_threshold, - lockout_duration: response.respawn_info.lockout_info.lockout_duration, - time_rem: response.respawn_info.lockout_info.time_rem, - plural: ( response.respawn_info.lockout_info.time_rem == 1 ) ? '' : $lang.get('meta_plural') - }); - } - else if ( response.respawn_info.lockout_info.lockout_policy == 'lockout' || response.respawn_info.lockout_info.lockout_policy == 'captcha' ) - { - // if we have a lockout policy of captcha or lockout, then warn the user - switch ( response.respawn_info.lockout_info.lockout_policy ) - { - case 'captcha': - base += $lang.get('user_err_invalid_credentials_lockout_captcha', { - fails: response.respawn_info.lockout_info.lockout_fails, - lockout_threshold: response.respawn_info.lockout_info.lockout_threshold, - lockout_duration: response.respawn_info.lockout_info.lockout_duration - }); - break; - case 'lockout': - base += $lang.get('user_err_invalid_credentials_lockout', { - fails: response.respawn_info.lockout_info.lockout_fails, - lockout_threshold: response.respawn_info.lockout_info.lockout_threshold, - lockout_duration: response.respawn_info.lockout_info.lockout_duration - }); - break; - } - } - return base; - break; + return error; + break; + case 'api_error': + // Error in the API. + return $lang.get('user_err_login_generic_title') + ': ' + $lang.get('user_' + response.error.toLowerCase()); + break; + } } + + return typeof(response.error) == 'string' ? response.error : false; } window.ajaxShowCaptcha = function(code) @@ -1302,6 +1264,7 @@ { level = USER_LEVEL_ADMIN; } + ajaxLogonInit(function(k, response) { ajaxLoginReplaceSIDInline(k, old_sid, level); diff -r adfbe522c95f -r 05fe0039d952 includes/clientside/static/paginate.js --- a/includes/clientside/static/paginate.js Sun Oct 25 00:09:11 2009 -0400 +++ b/includes/clientside/static/paginate.js Tue Nov 03 22:08:48 2009 -0500 @@ -237,7 +237,7 @@ // White-out the old div and fade in the new one - if ( IE || is_Safari ) + if ( IE || is_Safari || aclDisableTransitionFX ) { current_div.style.display = 'none'; new_div.style.display = 'block'; diff -r adfbe522c95f -r 05fe0039d952 includes/sessions.php --- a/includes/sessions.php Sun Oct 25 00:09:11 2009 -0400 +++ b/includes/sessions.php Tue Nov 03 22:08:48 2009 -0500 @@ -676,12 +676,10 @@ * @param string $password The password -OR- the MD5 hash of the password if $already_md5ed is true * @param bool $already_md5ed This should be set to true if $password is an MD5 hash, and should be false if it's plaintext. Defaults to false. * @param int $level The privilege level we're authenticating for, defaults to 0 - * @param string $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. - * @param string $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. * @param bool $remember Optional. If true, remembers the session for X days. Otherwise, assigns a short session. Defaults to false. */ - function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false) + function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $remember = false) { global $db, $session, $paths, $template, $plugins; // Common objects @@ -704,40 +702,6 @@ return $this->login_compat($username, md5($password), $level); } - // Lockout check - if ( !defined('IN_ENANO_INSTALL') ) - { - $lockout_data = $this->get_lockout_info($lockout_data); - - $captcha_good = false; - if ( $lockout_data['lockout_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 ( strtolower($real_code) === strtolower($captcha_code) ) - { - $captcha_good = true; - } - } - if ( $lockout_data['lockout_policy'] != 'disable' && !$captcha_good ) - { - if ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] ) - { - // ooh boy, somebody's in trouble ;-) - return array( - 'success' => false, - 'error' => 'locked_out', - 'lockout_threshold' => $lockout_data['lockout_threshold'], - 'lockout_duration' => ( $lockout_data['lockout_duration'] ), - 'lockout_fails' => $lockout_data['lockout_fails'], - 'lockout_policy' => $lockout_data['lockout_policy'], - 'time_rem' => $lockout_data['time_rem'], - 'lockout_last_time' => $lockout_data['lockout_last_time'] - ); - } - } - } - // Instanciate the Rijndael encryption object $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); @@ -766,7 +730,14 @@ . '\''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); // Do we also need to increment the lockout countdown? - if ( @$lockout_data['lockout_policy'] != 'disable' && !defined('IN_ENANO_INSTALL') ) + if ( !defined('IN_ENANO_INSTALL') ) + $lockout_data = $this->get_lockout_info(); + else + $lockout_data = array( + 'lockout_policy' => 'disable' + ); + + if ( $lockout_data['lockout_policy'] != 'disable' && !defined('IN_ENANO_INSTALL') ) { $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); // increment fail count @@ -891,20 +862,11 @@ $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.enano_date(ED_DATE | ED_TIME).'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); // Do we also need to increment the lockout countdown? - if ( !defined('IN_ENANO_INSTALL') && $lockout_data['lockout_policy'] != 'disable' ) + if ( !defined('IN_ENANO_INSTALL') && getConfig('lockout_policy', 'lockout') !== 'disable' ) { $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); // increment fail count $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');'); - $lockout_data['lockout_fails']++; - return array( - 'success' => false, - 'error' => ( $lockout_data['lockout_fails'] >= $lockout_data['lockout_threshold'] ) ? 'locked_out' : 'invalid_credentials', - 'lockout_threshold' => $lockout_data['lockout_threshold'], - 'lockout_duration' => ( $lockout_data['lockout_duration'] ), - 'lockout_fails' => $lockout_data['lockout_fails'], - 'lockout_policy' => $lockout_data['lockout_policy'] - ); } return array( @@ -1071,7 +1033,7 @@ * @return bool True if locked out, false otherwise */ - function get_lockout_info(&$lockdata) + function get_lockout_info() { global $db; @@ -1096,14 +1058,14 @@ $row = $db->fetchrow($q); $locked_out = ( $fails >= $threshold ); $lockdata = array( - 'locked_out' => $locked_out, - 'lockout_threshold' => $threshold, - 'lockout_duration' => ( $duration / 60 ), - 'lockout_fails' => $fails, - 'lockout_policy' => $policy, - 'lockout_last_time' => $row['timestamp'], + 'active' => $locked_out, + 'threshold' => $threshold, + 'duration' => ( $duration / 60 ), + 'fails' => $fails, + 'policy' => $policy, + 'last_time' => $row['timestamp'], 'time_rem' => $locked_out ? ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ) : 0, - 'captcha' => '' + 'captcha' => $policy == 'captcha' ? $this->make_captcha() : '' ); $db->free_result(); } @@ -1111,12 +1073,12 @@ { // disabled; send back default dataset $lockdata = array( - 'locked_out' => false, - 'lockout_threshold' => $threshold, - 'lockout_duration' => ( $duration / 60 ), - 'lockout_fails' => 0, - 'lockout_policy' => $policy, - 'lockout_last_time' => 0, + 'active' => false, + 'threshold' => $threshold, + 'duration' => ( $duration / 60 ), + 'fails' => 0, + 'policy' => $policy, + 'last_time' => 0, 'time_rem' => 0, 'captcha' => '' ); @@ -3869,59 +3831,20 @@ // Check for the mode if ( !isset($req['mode']) ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_JSON_NO_MODE' - ); + return $this->get_login_response('api_error', 'ERR_JSON_NO_MODE'); } // Main processing switch switch ( $req['mode'] ) { default: - return array( - 'mode' => 'error', - 'error' => 'ERR_JSON_INVALID_MODE' - ); + return $this->get_login_response('api_error', 'ERR_JSON_INVALID_MODE'); break; case 'getkey': $this->start(); - $locked_out = $this->get_lockout_info($lockdata); - - $response = array('mode' => 'build_box'); - $response['allow_diffiehellman'] = $dh_supported; - - $response['username'] = ( $this->user_logged_in ) ? $this->username : false; - $response['aes_key'] = $this->rijndael_genkey(); - - $response['extended_time'] = intval(getConfig('session_remember_time', '30')); - - // Lockout info - $response['locked_out'] = $locked_out; - - $response['lockout_info'] = $lockdata; - if ( $lockdata['lockout_policy'] == 'captcha' && $locked_out ) - { - $response['lockout_info']['captcha'] = $this->make_captcha(); - } - - // Can we do Diffie-Hellman? If so, generate and stash a public/private key pair. - if ( $dh_supported ) - { - $dh_key_priv = dh_gen_private(); - $dh_key_pub = dh_gen_public($dh_key_priv); - $dh_key_priv = $_math->str($dh_key_priv); - $dh_key_pub = $_math->str($dh_key_pub); - $response['dh_public_key'] = $dh_key_pub; - // store the keys in the DB - $q = $db->sql_query('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );"); - if ( !$q ) - $db->die_json(); - } - - return $response; + return $this->get_login_response('initial'); break; case 'login_dh': // User is requesting a login and has sent Diffie-Hellman data. @@ -3937,10 +3860,7 @@ // Check the key if ( !ctype_digit($dh_public) || !ctype_digit($req['dh_client_key']) ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_DH_KEY_NOT_NUMERIC' - ); + return $this->get_login_response('api_error', 'ERR_DH_KEY_NOT_NUMERIC'); } // Fetch private key @@ -3950,10 +3870,7 @@ if ( $db->numrows() < 1 ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_DH_KEY_NOT_FOUND' - ); + return $this->get_login_response('api_error', 'ERR_DH_KEY_NOT_FOUND'); } list($dh_private, $dh_key_id) = $db->fetchrow_num(); @@ -3972,10 +3889,7 @@ $dh_secret_check = sha1($dh_secret); if ( $dh_secret_check !== $dh_hash ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_DH_HASH_NO_MATCH', - ); + return $this->get_login_response('api_error', 'ERR_DH_HASH_NO_MATCH'); } // All good! Generate the AES key @@ -3987,10 +3901,7 @@ $aes_key = $this->fetch_public_key($req['key_aes']); if ( !$aes_key ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_AES_LOOKUP_FAILED' - ); + return $this->get_login_response('api_error', 'ERR_AES_LOOKUP_FAILED'); } $userinfo_crypt = $req['userinfo']; } @@ -4003,10 +3914,7 @@ $userinfo_json = $aes->decrypt($userinfo_crypt, $aes_key, ENC_HEX, true); if ( !$userinfo_json ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_AES_DECRYPT_FAILED' - ); + return $this->get_login_response('api_error', 'ERR_AES_DECRYPT_FAILED'); } // de-JSON user info try @@ -4015,23 +3923,42 @@ } catch ( Exception $e ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_USERINFO_DECODE_FAILED' - ); + return $this->get_login_response('api_error', 'ERR_USERINFO_DECODE_FAILED'); + } + + case 'login_pt': + // plaintext login + if ( $req['mode'] == 'login_pt' ) + { + $userinfo = isset($req['userinfo']) ? $req['userinfo'] : array(); } if ( !isset($userinfo['username']) || !isset($userinfo['password']) ) { - return array( - 'mode' => 'error', - 'error' => 'ERR_USERINFO_MISSING_VALUES' - ); + return $this->get_login_response('api_error', 'ERR_USERINFO_MISSING_VALUES'); } $username =& $userinfo['username']; $password =& $userinfo['password']; + // locked out? check captcha + $lockout_data = $this->get_lockout_info(); + if ( $lockout_data['policy'] == 'captcha' && $lockout_data['active'] ) + { + // policy is captcha -- check if it's correct, and if so, bypass lockout check + $real_code = $this->get_captcha($req['captcha_hash']); + if ( strtolower($real_code) !== strtolower($req['captcha_code']) ) + { + // captcha is bad + return $this->get_login_response('login_failure', 'lockout_bad_captcha'); + } + } + else if ( $lockout_data['policy'] == 'lockout' && $lockout_data['active'] ) + { + // we're fully locked out + return $this->get_login_response('login_failure', 'lockout_request_denied'); + } + // At this point if any extra info was injected into the login data packet, we need to let plugins process it /** * Called upon processing an incoming login request. If you added anything to the userinfo object during the jshook @@ -4049,12 +3976,12 @@ $result = eval($cmd); if ( $result === true ) { - return array( - 'mode' => 'login_success', - 'key' => ( $this->sid_super ) ? $this->sid_super : false, + return $this->get_login_response('login_success', false, array( + 'key' => $this->sid_super, 'user_id' => $this->user_id, - 'user_level' => $this->user_level - ); + 'user_level' => $this->user_level, + 'reset' => false + )); } else if ( is_array($result) ) { @@ -4063,45 +3990,33 @@ // Pass back any additional information from the error response $append = $result; unset($append['mode'], $append['error']); + $append['from_plugin'] = true; - $return = array( - 'mode' => 'login_failure', - 'error_code' => $result['error'], - // Use this to provide a way to respawn the login box - 'respawn_info' => $this->process_login_request(array('mode' => 'getkey')) - ); - - $return = array_merge($append, $return); - return $return; + return $this->get_login_response('login_failure', $result['error'], $append); } } } - // If we're logging in with a temp password, attach to the login_password_reset hook to send our JSON response - // A bit hackish since it just dies with the response :-( - $plugins->attachHook('login_password_reset', '$this->process_login_request(array(\'mode\' => \'respond_password_reset\', \'user_id\' => $row[\'user_id\'], \'temp_password\' => $this->pk_encrypt($password)));'); - // attempt the login - // function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) - $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code'], @$req['remember']); + $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['remember']); if ( $login_result['success'] ) { - return array( - 'mode' => 'login_success', - 'key' => ( $this->sid_super ) ? $this->sid_super : false, + return $this->get_login_response('login_success', false, array( + 'key' => $this->sid_super, 'user_id' => $this->user_id, - 'user_level' => $this->user_level - ); + 'user_level' => $this->user_level, + )); + } + else if ( !$login_result['success'] && $login_result['error'] === 'valid_reset' ) + { + return $this->get_login_response('reset_pass_used', false, array( + 'redirect_url' => $login_result['redirect_url'] + )); } else { - return array( - 'mode' => 'login_failure', - 'error_code' => $login_result['error'], - // Use this to provide a way to respawn the login box - 'respawn_info' => $this->process_login_request(array('mode' => 'getkey')) - ); + return $this->get_login_response('login_failure', 'invalid_credentials'); } break; @@ -4164,6 +4079,58 @@ } + /** + * Generate a packet to send to the client for logins. + * @param string mode + * @param array + * @return array + */ + + function get_login_response($mode, $error = false, $base = array()) + { + $this->start(); + + // init + $response = $base; + // modules in the packet + $response['mode'] = $mode; + $response['error'] = $error; + $response['crypto'] = $mode !== 'login_success' ? $this->get_login_crypto_packet() : false; + $response['lockout'] = $mode !== 'login_success' ? $this->get_lockout_info() : false; + $response['extended_time'] = intval(getConfig('session_remember_time', '30')); + $response['username'] = $this->user_logged_in ? $this->username : false; + return $response; + } + + /** + * Get a packet of crypto flags for login. + * @return array + */ + + function get_login_crypto_packet() + { + global $dh_supported, $_math; + + $response = array(); + + $response['dh_enable'] = $dh_supported; + $response['aes_key'] = $this->rijndael_genkey(); + + // Can we do Diffie-Hellman? If so, generate and stash a public/private key pair. + if ( $dh_supported ) + { + $dh_key_priv = dh_gen_private(); + $dh_key_pub = dh_gen_public($dh_key_priv); + $dh_key_priv = $_math->str($dh_key_priv); + $dh_key_pub = $_math->str($dh_key_pub); + $response['dh_public_key'] = $dh_key_pub; + // store the keys in the DB + $this->sql('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );"); + } + + return $response; + } + } /** diff -r adfbe522c95f -r 05fe0039d952 includes/template.php --- a/includes/template.php Sun Oct 25 00:09:11 2009 -0400 +++ b/includes/template.php Tue Nov 03 22:08:48 2009 -0500 @@ -1212,6 +1212,7 @@ $protected = is_object($this->page) ? $this->page->ns->cdata['really_protected'] : false; // Generate the dynamic javascript vars + // Sorry. I know. This block is a mess. $js_dynamic = ' "; diff -r adfbe522c95f -r 05fe0039d952 language/english/user.json --- a/language/english/user.json Sun Oct 25 00:09:11 2009 -0400 +++ b/language/english/user.json Tue Nov 03 22:08:48 2009 -0500 @@ -47,6 +47,7 @@ login_success_short: 'Success.', login_check_remember: 'Keep me logged in on this computer for %session_length% %length_units% unless I log out', login_check_remember_infinite: 'Keep me logged in on this computer until I log out', + login_btn_log_in: 'Log in', login_noact_title: 'Account error', login_noact_msg_intro: 'It appears that your user account has not yet been activated.', @@ -90,11 +91,12 @@ err_key_wrong_length: 'The encryption key was the wrong length.', err_too_big_for_britches: 'You are trying to authenticate at a level that your user account does not permit.', err_invalid_credentials: 'You have entered an invalid username or password. Please enter your login details again.', - err_invalid_credentials_lockout: ' You have used up %fails% out of %config.lockout_threshold% login attempts. After you have used up all %config.lockout_threshold% login attempts, you will be locked out from logging in for %config.lockout_duration% minutes.', - err_invalid_credentials_lockout_captcha: ' You have used up %fails% out of %config.lockout_threshold% login attempts. After you have used up all %config.lockout_threshold% login attempts, you will have to enter a visual confirmation code while logging in, effective for %config.lockout_duration% minutes.', + err_invalid_credentials_lockout: ' You have used up %fails% out of %threshold% login attempts. After you have used up all %threshold% login attempts, you will be locked out from logging in for %duration% minutes.', + err_invalid_credentials_captcha: ' You have used up %fails% out of %threshold% login attempts. After you have used up all %threshold% login attempts, you will have to enter a visual confirmation code while logging in, effective for %duration% minutes.', err_backend_fail: 'You entered the right credentials and everything was validated, but for some reason Enano couldn\'t register your session. This is an internal problem with the site and you are encouraged to contact site administration.', - err_locked_out: 'You have used up all %config.lockout_threshold% allowed login attempts. Please wait %time_rem% minute%plural% before attempting to log in again%captcha_blurb%.', - err_locked_out_captcha_blurb: ', or enter the visual confirmation code shown above in the appropriate box', + err_locked_out_initial_lockout: 'Your computer is locked out from logging in because you entered invalid credentials too many times. This restriction will expire in %time_rem% minute%plural%.', + err_locked_out_initial_captcha: 'Please enter the correct visual confirmation code to log in. This restriction will expire in %time_rem% minute%plural%.', + err_lockout_bad_captcha: 'The confirmation code you entered was incorrect.', err_admin_session_timed_out: 'Your session has timed out; please log in again using the form above.', err_email_not_valid: 'The e-mail address you entered is invalid.', diff -r adfbe522c95f -r 05fe0039d952 plugins/SpecialUserFuncs.php --- a/plugins/SpecialUserFuncs.php Sun Oct 25 00:09:11 2009 -0400 +++ b/plugins/SpecialUserFuncs.php Tue Nov 03 22:08:48 2009 -0500 @@ -48,101 +48,27 @@ function page_Special_Login() { global $db, $session, $paths, $template, $plugins; // Common objects - global $__login_status; - global $lang; - - require_once(ENANO_ROOT . '/includes/math.php'); - require_once(ENANO_ROOT . '/includes/diffiehellman.php'); - global $dh_supported; + global $login_result; + global $lang, $output; - $locked_out = false; - // are we locked out? - $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 != 'disable' ) - { - $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); - $timestamp_cutoff = time() - $duration; - $q = $session->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); - $fails = $db->numrows(); - if ( $fails >= $threshold ) - { - $row = $db->fetchrow(); - $locked_out = true; - $lockdata = array( - 'locked_out' => true, - 'lockout_threshold' => $threshold, - 'lockout_duration' => ( $duration / 60 ), - 'lockout_fails' => $fails, - 'lockout_policy' => $policy, - 'lockout_last_time' => $row['timestamp'], - 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), - 'captcha' => '' - ); - if ( $policy == 'captcha' ) - { - $lockdata['captcha'] = $session->make_captcha(); - } - } - $db->free_result(); - } - - if ( isset($_GET['act']) && $_GET['act'] == 'getkey' ) - { - header('Content-type: text/javascript'); - $username = ( $session->user_logged_in ) ? $session->username : false; - $response = Array( - 'username' => $username, - 'key' => $pubkey, - 'challenge' => $challenge, - 'locked_out' => false - ); - - if ( $locked_out ) - { - foreach ( $lockdata as $x => $y ) - { - $response[$x] = $y; - } - unset($x, $y); - } - - // 1.1.3: generate diffie hellman key - $response['dh_supported'] = $dh_supported; - if ( $dh_supported ) - { - $dh_key_priv = dh_gen_private(); - $dh_key_pub = dh_gen_public($dh_key_priv); - $dh_key_priv = $_math->str($dh_key_priv); - $dh_key_pub = $_math->str($dh_key_pub); - $response['dh_public_key'] = $dh_key_pub; - // store the keys in the DB - $q = $db->sql_query('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );"); - if ( !$q ) - $db->die_json(); - } - - $response = enano_json_encode($response); - echo $response; - return null; - } - + // Determine which level we're going up to $level = ( isset($_GET['level']) && in_array($_GET['level'], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') ) ) ? intval($_GET['level']) : USER_LEVEL_MEMBER; if ( isset($_POST['login']) ) { - if ( in_array($_POST['auth_level'], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') ) ) + if ( in_array($_POST['level'], array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') ) ) { - $level = intval($_POST['auth_level']); + $level = intval($_POST['level']); } } - + // Don't allow going from guest straight to elevated + // FIXME do we want to allow this with a CSRF check? if ( $level > USER_LEVEL_MEMBER && !$session->user_logged_in ) { $level = USER_LEVEL_MEMBER; } + + // If we're already at or above this level, redirect to the target page or, if no target + // specified, back to the main page. if ( $level <= USER_LEVEL_MEMBER && $session->user_logged_in ) { if ( $target = $paths->getAllParams() ) @@ -152,176 +78,136 @@ $paths->main_page(); } - $template->header(); - echo '
'; - $header = ( $level > USER_LEVEL_MEMBER ) ? $lang->get('user_login_message_short_elev') : $lang->get('user_login_message_short'); - if ( isset($_POST['login']) ) - { - $errstring = $__login_status['error']; - switch($__login_status['error']) - { - case 'key_not_found': - $errstring = $lang->get('user_err_key_not_found'); - break; - case 'ERR_DH_KEY_NOT_FOUND': - $errstring = $lang->get('user_err_dh_key_not_found'); // . " -- {$__login_status['debug']}"; - break; - case 'ERR_DH_KEY_NOT_INTEGER': - $errstring = $lang->get('user_err_dh_key_not_numeric'); - break; - case 'key_wrong_length': - $errstring = $lang->get('user_err_key_wrong_length'); - break; - case 'too_big_for_britches': - $errstring = $lang->get('user_err_too_big_for_britches'); - break; - case 'invalid_credentials': - $errstring = $lang->get('user_err_invalid_credentials'); - if ( getConfig('lockout_policy', 'lockout') == 'lockout' ) - { - $errstring .= $lang->get('user_err_invalid_credentials_lockout', array('fails' => $__login_status['lockout_fails'])); - } - else if ( getConfig('lockout_policy', 'lockout') == 'captcha' ) - { - $errstring .= $lang->get('user_err_invalid_credentials_lockout_captcha', array('fails' => $__login_status['lockout_fails'])); - } - break; - case 'backend_fail': - $errstring = $lang->get('user_err_backend_fail'); - break; - case 'locked_out': - $attempts = intval($__login_status['lockout_fails']); - if ( $attempts > $__login_status['lockout_threshold']) - $attempts = $__login_status['lockout_threshold']; - - $server_time = time(); - $time_rem = ( intval(@$__login_status['lockout_last_time']) == time() ) ? $__login_status['lockout_duration'] : $__login_status['lockout_duration'] - round( ( $server_time - $__login_status['lockout_last_time'] ) / 60 ); - if ( $time_rem < 1 ) - $time_rem = $__login_status['lockout_duration']; - - $s = ( $time_rem == 1 ) ? '' : $lang->get('meta_plural'); - - $captcha_string = ( $__login_status['lockout_policy'] == 'captcha' ) ? $lang->get('user_err_locked_out_captcha_blurb') : ''; - $errstring = $lang->get('user_err_locked_out', array('plural' => $s, 'captcha_blurb' => $captcha_string, 'time_rem' => $time_rem)); - - break; - default: - $errstring = $lang->get($errstring); - break; - } - echo '
'.$errstring.'
'; - } + // Lockout aliasing + $lockout =& $login_result['lockout']; + + $output->header(); + echo ''; + if ( $p = $paths->getAllParams() ) { - echo ''; + echo ''; } else if ( isset($_POST['login']) && isset($_POST['return_to']) ) { - echo ''; + echo ''; + } + + // determine what the "remember me" checkbox should say + $session_time = intval(getConfig('session_remember_time', '30')); + if ( $session_time === 0 ) + { + // sessions are infinite + $text_remember = $lang->get('user_login_check_remember_infinite'); } + else + { + // is the number of days evenly divisible by 7? if so, use weeks + if ( $session_time % 7 == 0 ) + { + $session_time = $session_time / 7; + $unit = 'week'; + } + else + { + $unit = 'day'; + } + // if it's not equal to 1, pluralize it + if ( $session_time != 1 ) + { + $unit .= $lang->get('meta_plural'); + } + $text_remember = $lang->get('user_login_check_remember', array( + 'session_length' => $session_time, + 'length_units' => $lang->get("etc_unit_$unit") + )); + } + + if ( $error_text = login_get_error($login_result) ) + { + echo '
' . htmlspecialchars($error_text) . '
'; + } + + // + // START FORM + // ?>
- + + - - - + + + + + - + + setHook('login_form_html'); foreach ( $code as $cmd ) { eval($cmd); } + + // level-2 only: "Remember me" switch if ( $level <= USER_LEVEL_MEMBER ) { - // "remember me" switch - // first order of business is to determine what the checkbox should say - $session_time = intval(getConfig('session_remember_time', '30')); - if ( $session_time === 0 ) - { - // sessions are infinite - $text_remember = $lang->get('user_login_check_remember_infinite'); - } - else - { - // is the number of days evenly divisible by 7? if so, use weeks - if ( $session_time % 7 == 0 ) - { - $session_time = $session_time / 7; - $unit = 'week'; - } - else - { - $unit = 'day'; - } - // if it's not equal to 1, pluralize it - if ( $session_time != 1 ) - { - $unit .= 's'; - } - $text_remember = $lang->get('user_login_check_remember', array( - 'session_length' => $session_time, - 'length_units' => $lang->get("etc_unit_$unit") - )); - } ?> + + '; } - else if ( $level <= USER_LEVEL_MEMBER && ( isset($_GET['use_crypt']) && $_GET['use_crypt']=='0' ) ) + // Crypto disable: crypto OFF, normal login + else if ( $level <= USER_LEVEL_MEMBER && $crypto_disable ) { echo ''; } - else if ( $level > USER_LEVEL_MEMBER && !strstr($_SERVER['HTTP_USER_AGENT'], 'iPhone') && $dh_supported ) + // Crypto disable: crypto on, ELEV login + else if ( $level > USER_LEVEL_MEMBER && $GLOBALS['dh_supported'] ) { echo ''; echo '
+ + USER_LEVEL_MEMBER ) ? $lang->get('user_login_message_short_elev') : $lang->get('user_login_message_short'); ?> +
+ ' . $lang->get('user_login_body', array('reg_link' => makeUrlNS('Special', 'Register'))) . '

'; - } else - { echo '

' . $lang->get('user_login_body_elev') . '

'; - } ?>
get('user_login_field_username'); ?>: - user_logged_in ) - { - echo 'value="' . $session->username . '"'; - } - ?> /> + + + + get('user_login_forgotpass_blurb', array('forgotpass_link' => makeUrlNS('Special', 'PasswordReset'))); ?>
get('user_login_createaccount_blurb', array('reg_link' => makeUrlNS('Special', 'Register'))); ?>
get('user_login_field_password'); ?>: -
get('user_login_field_captcha'); ?>:
+ get('user_login_field_captcha'); ?>: +
+
+ + +
- +
@@ -334,9 +220,16 @@
'; @@ -349,7 +242,8 @@ echo '
'; @@ -362,7 +256,8 @@ echo '
'; @@ -372,15 +267,17 @@ } ?> +
- +
- + +