includes/sessions.php
changeset 1132 05fe0039d952
parent 1101 30d8bb88572d
child 1148 dcf6e9394e02
--- 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;
+  }
+  
 }
 
 /**