source: trunk/lib/Auth_SQL.inc.php @ 312

Last change on this file since 312 was 277, checked in by quinn, 17 years ago

Fixed issue with user remote_ip matching

File size: 46.1 KB
RevLine 
[1]1<?php
2/**
[136]3 * The Auth_SQL class provides a SQL implementation for authentication.
[1]4 *
5 * @author  Quinn Comendant <quinn@strangecode.com>
[124]6 * @version 2.1
[1]7 */
8
9// Available encryption types for class Auth_SQL.
[136]10define('AUTH_ENCRYPT_PLAINTEXT', 1);
[124]11define('AUTH_ENCRYPT_CRYPT', 2);
12define('AUTH_ENCRYPT_SHA1', 3);
[136]13define('AUTH_ENCRYPT_SHA1_HARDENED', 4);
14define('AUTH_ENCRYPT_MD5', 5);
15define('AUTH_ENCRYPT_MD5_HARDENED', 6);
[1]16
[43]17require_once dirname(__FILE__) . '/Email.inc.php';
18
[1]19class Auth_SQL {
[136]20       
21    // Namespace of this auth object.
22    var $_ns;
23   
24    // Static var for test.
25    var $_authentication_tested;
[1]26
[136]27    // Paramters to be configured by setParam.
[1]28    var $_params = array();
29    var $_default_params = array(
[42]30
[1]31        // Automatically create table and verify columns. Better set to false after site launch.
32        'create_table' => true,
[42]33
[1]34        // The database table containing users to authenticate.
35        'db_table' => 'user_tbl',
[42]36
[1]37        // The name of the primary key for the db_table.
38        'db_primary_key' => 'user_id',
[42]39
[1]40        // The name of the username key for the db_table.
41        'db_username_column' => 'username',
[42]42
[1]43        // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table.
[14]44        'db_login_table' => 'user_login_tbl',
[42]45
[1]46        // The type of encryption to use for passwords stored in the db_table. Use one of the AUTH_ENCRYPT_* types specified above.
[136]47        // Hardened password hashes rely on the same key/salt being used to compare encryptions.
48        // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change!
49        'encryption_type' => AUTH_ENCRYPT_MD5,
[1]50
[25]51        // The URL to the login script.
[1]52        'login_url' => '/',
53
54        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
[124]55        // In seconds. 21600 seconds = 6 hours.
[1]56        'login_timeout' => 21600,
[42]57
[1]58        // The maximum amount of time a user is allowed to be idle before their session expires. They will be forced to login again if they expire.
[124]59        // In seconds. 3600 seconds = 1 hour.
[1]60        'idle_timeout' => 3600,
61
62        // The period of time to compare login abuse attempts. If a threshold of logins is reached in this amount of time the account is blocked.
63        // Days and hours, like this: 'DD:HH'
64        'login_abuse_timeframe' => '04:00',
65
66        // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked.
67        'login_abuse_warnings' => 3,
68
69        // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked.
70        'login_abuse_max_ips' => 5,
71
[42]72        // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheatsheet at bottom).
73        // Any integar between 0 and 32 is permitted. Setting this to '24' permits any address in a
[1]74        // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely.
75        // Setting to '0' ignores all IPs, thus disabling login_abuse checking.
76        'login_abuse_ip_bitmask' => 32,
77
[42]78        // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at
[1]79        // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field.
80        'login_abuse_exempt_usernames' => array(),
[42]81
[275]82        // Specify usernames to exclude from remote_ip matching. Users behind proxy servers should be appended to this array so their shifting remote IP will not log them out.
83        'match_remote_ip_exempt_usernames' => array(),
84
85        // Match the user's current remote IP against the one they logged in with.
86        'match_remote_ip' => true,
87
[103]88        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
[1]89        'trusted_networks' => array(),
90
91        // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason'
92        'blocking' => false,
[42]93
[1]94        // Use a db_login_table to detect excessive logins. This requires blocking to be enabled.
95        'abuse_detection' => false,
96    );
97
98    /**
99     * Constructs a new authentication object.
100     *
101     * @access public
102     * @param optional array $params  A hash containing parameters.
103     */
[136]104    function Auth_SQL($namespace='')
[1]105    {
[136]106        $app =& App::getInstance();
107       
[154]108        $this->_ns = $namespace;
[195]109       
[1]110        // Initialize default parameters.
111        $this->setParam($this->_default_params);
112
113        // Get create tables config from global context.
[136]114        if (!is_null($app->getParam('db_create_tables'))) {
115            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
[1]116        }
[198]117
118        if (!isset($_SESSION['_auth_sql'][$this->_ns])) {
119            $this->clear();
120        }
[1]121    }
[42]122
[1]123    /**
124     * Setup the database tables for this class.
125     *
126     * @access  public
127     * @author  Quinn Comendant <quinn@strangecode.com>
128     * @since   26 Aug 2005 17:09:36
129     */
130    function initDB($recreate_db=false)
131    {
[136]132        $app =& App::getInstance();
133        $db =& DB::getInstance();
134   
135   
[1]136        static $_db_tested = false;
[42]137
[1]138        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
[42]139
[1]140            // User table.
141            if ($recreate_db) {
[136]142                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
[201]143                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
[1]144            }
145
[15]146            // The minimal columns for a table compatable with the Auth_SQL class.
[146]147            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
[1]148                " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL auto_increment,
149                " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
[259]150                userpass VARCHAR(255) NOT NULL DEFAULT '',
151                first_name VARCHAR(255) NOT NULL DEFAULT '',
152                last_name VARCHAR(255) NOT NULL DEFAULT '',
153                email VARCHAR(255) NOT NULL DEFAULT '',
154                login_abuse_exempt ENUM('TRUE') DEFAULT NULL,
155                blocked ENUM('TRUE') DEFAULT NULL,
156                blocked_reason VARCHAR(255) NOT NULL DEFAULT '',
157                abuse_warning_level TINYINT(4) NOT NULL DEFAULT '0',
158                seconds_online INT(11) NOT NULL DEFAULT '0',
159                last_login_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
160                last_access_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
161                last_login_ip VARCHAR(255) NOT NULL DEFAULT '0.0.0.0',
162                added_by_user_id SMALLINT(11) DEFAULT NULL,
163                modified_by_user_id SMALLINT(11) DEFAULT NULL,
164                added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
165                modified_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
[1]166                PRIMARY KEY (" . $this->getParam('db_primary_key') . "),
167                KEY " . $this->getParam('db_username_column') . " (" . $this->getParam('db_username_column') . "),
168                KEY userpass (userpass),
169                KEY email (email)
170            )");
171
[136]172            if (!$db->columnExists($this->getParam('db_table'), array(
[42]173                $this->getParam('db_primary_key'),
174                $this->getParam('db_username_column'),
175                'userpass',
176                'first_name',
177                'last_name',
178                'email',
179                'login_abuse_exempt',
180                'blocked',
181                'blocked_reason',
182                'abuse_warning_level',
183                'seconds_online',
184                'last_login_datetime',
185                'last_access_datetime',
186                'last_login_ip',
187                'added_by_user_id',
188                'modified_by_user_id',
189                'added_datetime',
190                'modified_datetime',
[1]191            ), false, false)) {
[136]192                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
[1]193                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
194            }
[42]195
[17]196            // Login table is used for abuse_detection features.
197            if ($this->getParam('abuse_detection')) {
198                if ($recreate_db) {
[136]199                    $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table'));
[201]200                    $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_INFO, __FILE__, __LINE__);
[17]201                }
[136]202                $db->query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_login_table') . " (
[259]203                    " . $this->getParam('db_primary_key') . " SMALLINT(11) NOT NULL DEFAULT '0',
204                    login_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
205                    remote_ip_binary CHAR(32) NOT NULL DEFAULT '',
[17]206                    KEY " . $this->getParam('db_primary_key') . " (" . $this->getParam('db_primary_key') . "),
207                    KEY login_datetime (login_datetime),
208                    KEY remote_ip_binary (remote_ip_binary)
209                )");
[42]210
[136]211                if (!$db->columnExists($this->getParam('db_login_table'), array(
[17]212                    $this->getParam('db_primary_key'),
213                    'login_datetime',
214                    'remote_ip_binary',
215                ), false, false)) {
[136]216                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__);
[17]217                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR);
218                }
[1]219            }
[42]220        }
[1]221        $_db_tested = true;
222    }
223
224    /**
225     * Set the params of an auth object.
226     *
227     * @param  array $params   Array of parameter keys and value to set.
228     * @return bool true on success, false on failure
229     */
230    function setParam($params)
231    {
232        if (isset($params) && is_array($params)) {
233            // Merge new parameters with old overriding only those passed.
234            $this->_params = array_merge($this->_params, $params);
235        }
236    }
237
238    /**
239     * Return the value of a parameter, if it exists.
240     *
241     * @access public
242     * @param string $param        Which parameter to return.
243     * @return mixed               Configured parameter value.
244     */
245    function getParam($param)
246    {
[136]247        $app =& App::getInstance();
248   
[1]249        if (isset($this->_params[$param])) {
250            return $this->_params[$param];
251        } else {
[146]252            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
[1]253            return null;
254        }
255    }
256
257    /**
258     * Clear any authentication tokens in the current session. A.K.A. logout.
259     *
260     * @access public
261     */
[149]262    function clear()
[1]263    {
[136]264        $db =& DB::getInstance();
265   
[1]266        $this->initDB();
[42]267
[199]268        if ($this->get('user_id', false)) {
269            // FIX ME: Should we check if the session is active?
[195]270            $db->query("
271                UPDATE " . $this->_params['db_table'] . " SET
272                seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)),
273                last_login_datetime = '0000-00-00 00:00:00'
274                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
275            ");
276        }
[154]277        $_SESSION['_auth_sql'][$this->_ns] = array('authenticated' => false);
[1]278    }
279
280    /**
[103]281     * Sets a variable into a registered auth session.
282     *
283     * @access public
284     * @param mixed $key      Which value to set.
285     * @param mixed $val      Value to set variable to.
286     */
[149]287    function set($key, $val)
[103]288    {
[154]289        if (!isset($_SESSION['_auth_sql'][$this->_ns]['user_data'])) {
290            $_SESSION['_auth_sql'][$this->_ns]['user_data'] = array();
[103]291        }
[154]292        $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key] = $val;
[103]293    }
294
295    /**
296     * Returns a specified value from a registered auth session.
297     *
298     * @access public
299     * @param mixed $key      Which value to return.
300     * @param mixed $default  Value to return if key not found in user_data.
301     * @return mixed          Value stored in session.
302     */
[148]303    function get($key, $default='')
[103]304    {
[154]305        if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) {
306            return $_SESSION['_auth_sql'][$this->_ns][$key];
307        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['user_data'][$key])) {
308            return $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key];
[103]309        } else {
310            return $default;
311        }
312    }
313
314    /**
[1]315     * Find out if a set of login credentials are valid.
316     *
317     * @access private
318     * @param string $username      The username to check.
319     * @param string $password      The password to compare to username.
320     * @return mixed  False if credentials not found in DB, or returns DB row matching credentials.
321     */
322    function authenticate($username, $password)
323    {
[136]324        $app =& App::getInstance();
325        $db =& DB::getInstance();
326
[1]327        $this->initDB();
[42]328
[124]329        switch ($this->_params['encryption_type']) {
330        case AUTH_ENCRYPT_CRYPT :
331            // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
[136]332            $qid = $db->query("
[124]333                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
334                FROM " . $this->_params['db_table'] . "
[136]335                WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
336                AND BINARY userpass = ENCRYPT('" . $db->escapeString($password) . "', LEFT(userpass, 2)))
[124]337            ");
338            break;
339        case AUTH_ENCRYPT_PLAINTEXT :
340        case AUTH_ENCRYPT_MD5 :
341        case AUTH_ENCRYPT_SHA1 :
342        default :
343            // Query DB for user matching credentials. Directly compare cyphertext with result from encryptPassword().
[136]344            $qid = $db->query("
[124]345                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
346                FROM " . $this->_params['db_table'] . "
[136]347                WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
348                AND BINARY userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
[124]349            ");
350            break;
351        }
[42]352
[1]353        // Return user data if found.
354        if ($user_data = mysql_fetch_assoc($qid)) {
[158]355            // Don't return password value.
356            unset($user_data['userpass']);
[136]357            $app->logMsg(sprintf('Authentication successful for user %s (%s)', $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__);
[1]358            return $user_data;
359        } else {
[136]360            $app->logMsg(sprintf('Authentication failed for user %s (encrypted attempted password: %s)', $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__);
[1]361            return false;
362        }
363    }
364
365    /**
366     * If user authenticated, register login into session.
367     *
368     * @access private
369     * @param string $username     The username to check.
370     * @param string $password     The password to compare to username.
371     * @return boolean  Whether or not the credentials are valid.
372     */
373    function login($username, $password)
374    {
[136]375        $app =& App::getInstance();
376        $db =& DB::getInstance();
377   
[1]378        $this->initDB();
[42]379
[149]380        $this->clear();
[1]381
382        if (!$user_data = $this->authenticate($username, $password)) {
383            // No login: failed authentication!
384            return false;
385        }
386
387        // Register authenticated session.
[154]388        $_SESSION['_auth_sql'][$this->_ns] = array(
[1]389            'authenticated'         => true,
390            'user_id'               => $user_data['user_id'],
391            'username'              => $username,
392            'login_datetime'        => date('Y-m-d H:i:s'),
393            'last_access_datetime'  => date('Y-m-d H:i:s'),
394            'remote_ip'             => getRemoteAddr(),
[275]395            'login_abuse_exempt'    => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array(strtolower($username), $this->_params['login_abuse_exempt_usernames']),
396            'match_remote_ip_exempt'=> isset($user_data['match_remote_ip_exempt']) ? !empty($user_data['match_remote_ip_exempt']) : in_array(strtolower($username), $this->_params['match_remote_ip_exempt_usernames']),
[1]397            'user_data'             => $user_data
398        );
[42]399
[1]400        /**
401         * Check if the account is blocked, respond in context to reason. Cancel the login if blocked.
402         */
403        if ($this->getParam('blocking')) {
404            if (!empty($user_data['blocked'])) {
[42]405
[148]406                $app->logMsg(sprintf('User %s (%s) login failed due to blocked account: %s', $this->get('user_id'), $this->get('username'), $this->get('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
[42]407
[1]408                switch ($user_data['blocked_reason']) {
409                    case 'account abuse' :
[136]410                        $app->raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact us to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
[1]411                        break;
412                    default :
[136]413                        $app->raiseMsg(sprintf(_("This account is currently not active. %s"), $user_data['blocked_reason']), MSG_WARNING, __FILE__, __LINE__);
[1]414                        break;
415                }
[42]416
[1]417                // No login: user is blocked!
[149]418                $this->clear();
[1]419                return false;
420            }
421        }
[42]422
[1]423        /**
424         * Check the db_login_table for too many logins under this account.
425         * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe
426         * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account.
427        **/
[148]428        if ($this->getParam('abuse_detection') && !$this->get('login_abuse_exempt')) {
[136]429            $qid = $db->query("
[1]430                SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . "))
431                FROM " . $this->_params['db_login_table'] . "
[148]432                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
[1]433                AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW()
434            ");
435            list($distinct_ips) = mysql_fetch_row($qid);
436            if ($distinct_ips > $this->_params['login_abuse_max_ips']) {
[148]437                if ($this->get('abuse_warning_level') < $this->_params['login_abuse_warnings']) {
[1]438                    // Warn the user with a password reset.
[15]439                    $this->resetPassword(null, _("This is a security precaution. We have detected this account has been accessed from multiple computers simultaneously. It is against policy to share login information with others. If further account abuse is detected this account will be blocked."));
[136]440                    $app->raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__);
[148]441                    $app->logMsg(sprintf('Account abuse detected for user %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_WARNING, __FILE__, __LINE__);
[1]442                } else {
443                    // Block the account with the reason of account abuse.
444                    $this->blockAccount(null, 'account abuse');
[136]445                    $app->raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__);
[148]446                    $app->logMsg(sprintf('Account blocked for user %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_ALERT, __FILE__, __LINE__);
[1]447                }
448                // Increment user's warning level.
[148]449                $db->query("UPDATE " . $this->_params['db_table'] . " SET abuse_warning_level = abuse_warning_level + 1 WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
[1]450                // Reset the login counter for this user.
[148]451                $db->query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
[1]452                // No login: reset password because of account abuse!
[149]453                $this->clear();
[1]454                return false;
455            }
456
457            // Update the login counter table with this login access. Convert IP to binary.
[159]458            // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
[136]459            $db->query("
[1]460                INSERT INTO " . $this->_params['db_login_table'] . " (
[42]461                    " . $this->_params['db_primary_key'] . ",
462                    login_datetime,
[1]463                    remote_ip_binary
464                ) VALUES (
[148]465                    '" . $this->get('user_id') . "',
466                    '" . $this->get('login_datetime') . "',
467                    '" . sprintf('%032b', ip2long($this->get('remote_ip'))) . "'
[1]468                )
469            ");
470        }
[42]471
[1]472        // Update user table with this login.
[136]473        $db->query("
[1]474            UPDATE " . $this->_params['db_table'] . " SET
[148]475                last_login_datetime = '" . $this->get('login_datetime') . "',
476                last_access_datetime = '" . $this->get('login_datetime') . "',
477                last_login_ip = '" . $this->get('remote_ip') . "'
478            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
[1]479        ");
[42]480
[1]481        // We're logged-in!
482        return true;
483    }
484
485    /**
486     * Test if user has a currently logged-in session.
487     *  - authentication flag set to true
488     *  - username not empty
489     *  - total logged-in time is not greater than login_timeout
490     *  - idle time is not greater than idle_timeout
491     *  - remote address is the same as the login remote address (aol users excluded).
492     *
493     * @access public
494     */
495    function isLoggedIn($user_id=null)
496    {
[136]497        $app =& App::getInstance();
498        $db =& DB::getInstance();
499   
[1]500        $this->initDB();
[42]501
[1]502        if (isset($user_id)) {
503            // Check the login status of a specific user.
[136]504            $qid = $db->query("
[1]505                SELECT 1 FROM " . $this->_params['db_table'] . "
[136]506                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
[1]507                AND DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW()
508                AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW()
509            ");
510            return (mysql_num_rows($qid) > 0);
511        }
512
513        // User login test need only be run once per script execution. We cache the result in the session.
[154]514        if ($this->_authentication_tested && isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])) {
515            return $_SESSION['_auth_sql'][$this->_ns]['authenticated'];
[1]516        }
[42]517
[1]518        // Tesing login should occur once. This is the first time. Set flag.
519        $this->_authentication_tested = true;
[35]520
[1]521        // Some users will access from networks with a changing IP number (i.e. behind a proxy server). These users must be allowed entry by adding their IP to the list of trusted_networks.
522        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
523            $user_in_trusted_network = true;
[136]524            $app->logMsg(sprintf('User %s accessing from trusted network %s',
[148]525                ($this->get('user_id') ? ' ' . $this->get('user_id') . ' (' .  $this->get('username') . ')' : ''),
[1]526                $trusted_net
[71]527            ), LOG_DEBUG, __FILE__, __LINE__);
[1]528        } else if (preg_match('/proxy.aol.com$/i', getRemoteAddr(true))) {
529            $user_in_trusted_network = true;
[136]530            $app->logMsg(sprintf('User %s accessing from trusted network proxy.aol.com',
[148]531                ($this->get('user_id') ? ' ' . $this->get('user_id') . ' (' .  $this->get('username') . ')' : '')
[71]532            ), LOG_DEBUG, __FILE__, __LINE__);
[1]533        } else {
534            $user_in_trusted_network = false;
535        }
[275]536       
537        // Do we match the user's remote IP at all? Yes, if set in config and not disabled for specific user.
538        if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt')) {
[276]539            $remote_ip_is_matched = (isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) && $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] == getRemoteAddr()) || $user_in_trusted_network;
[275]540        } else {
[277]541            $app->logMsg(sprintf('User %s exempt from remote_ip match (comparing %s == %s)', 
542                ($this->get('user_id') ? ' ' . $this->get('user_id') . ' (' .  $this->get('username') . ')' : ''),
543                $_SESSION['_auth_sql'][$this->_ns]['remote_ip'],
544                getRemoteAddr()
[275]545            ), LOG_DEBUG, __FILE__, __LINE__);
546            $remote_ip_is_matched = true;
547        }
[42]548
[1]549        // Test login with information stored in session. Skip IP matching for users from trusted networks.
[224]550        if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) 
[154]551            && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']
552            && !empty($_SESSION['_auth_sql'][$this->_ns]['username'])
553            && strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) > time() - $this->_params['login_timeout']
554            && strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > time() - $this->_params['idle_timeout']
[275]555            && $remote_ip_is_matched
[1]556        ) {
557            // User is authenticated!
[154]558            $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
[1]559
560            // Update the DB with the last_access_datetime and increment the seconds_online.
[136]561            $db->query("
[42]562                UPDATE " . $this->_params['db_table'] . " SET
[1]563                seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1,
[148]564                last_access_datetime = '" . $this->get('last_access_datetime') . "'
565                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
[1]566            ");
[136]567            if (mysql_affected_rows($db->getDBH()) > 0) {
[1]568                // User record still exists in DB. Do this to ensure user was not delete from DB between accesses. Notice "+ 1" in SQL above to ensure record is modified.
569                return true;
570            } else {
[148]571                $app->logMsg(sprintf('User update failed. Record not found for user %s (%s).', $this->get('user_id'), $this->get('username')), LOG_NOTICE, __FILE__, __LINE__);
[1]572            }
[224]573        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']) {
[1]574            // User is authenticated, but login has expired.
[154]575            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > time() - 43200) {
[35]576                // Only raise message if last session is less than 12 hours old.
[136]577                $app->raiseMsg(_("Your session has expired. You need to log-in again."), MSG_NOTICE, __FILE__, __LINE__);
[35]578            }
[42]579
[1]580            // Log the reason for login expiration.
581            $expire_reasons = array();
[154]582            if (empty($_SESSION['_auth_sql'][$this->_ns]['username'])) {
[1]583                $expire_reasons[] = 'username not found';
584            }
[154]585            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) <= time() - $this->_params['login_timeout']) {
[1]586                $expire_reasons[] = 'login_timeout expired';
587            }
[154]588            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
[1]589                $expire_reasons[] = 'idle_timeout expired';
590            }
[154]591            if ($_SESSION['_auth_sql'][$this->_ns]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
[276]592                if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt')) {
[275]593                    $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
594                } else {
595                    $expire_reasons[] = sprintf('remote_ip not matched but user was exempt from this check (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
596                }
[1]597            }
[148]598            $app->logMsg(sprintf('User %s (%s) session expired: %s', $this->get('user_id'), $this->get('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
[1]599        }
600
601        // User is not authenticated.
[149]602        $this->clear();
[1]603        return false;
604    }
605
606    /**
607     * Redirect user to login page if they are not logged in.
608     *
[32]609     * @param string $message The text description of a message to raise.
[1]610     * @param int    $type    The type of message: MSG_NOTICE,
611     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
612     * @param string $file    __FILE__.
613     * @param string $line    __LINE__.
614     * @access public
615     */
[32]616    function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
[1]617    {
[136]618        $app =& App::getInstance();
619   
[1]620        if (!$this->isLoggedIn()) {
[103]621            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
[203]622            if ('' != $message) {
623                $app->raiseMsg($message, $type, $file, $line);
624            }
[32]625
[28]626            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
[136]627            $app->setBoomerangURL(absoluteMe(), 'login');
628            $app->dieURL($this->_params['login_url']);
[1]629        }
630    }
631
632    /**
633     * This sets the 'blocked' field for a user in the db_table, and also
634     * adds an optional reason
[42]635     *
[1]636     * @param  string   $reason      The reason for blocking the account.
637     */
638    function blockAccount($user_id=null, $reason='')
639    {
[136]640        $app =& App::getInstance();
641        $db =& DB::getInstance();
642   
[1]643        $this->initDB();
[42]644
[1]645        if ($this->getParam('blocking')) {
[247]646            if (mb_strlen($db->escapeString($reason)) > 255) {
[1]647                // blocked_reason field is varchar(255).
[136]648                $app->logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__);
[1]649            }
[42]650
[1]651            // Get user_id if specified.
[148]652            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
[136]653            $db->query("
[1]654                UPDATE " . $this->_params['db_table'] . " SET
655                blocked = 'true',
[136]656                blocked_reason = '" . $db->escapeString($reason) . "'
657                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
[1]658            ");
659        }
660    }
661
662    /**
[266]663     * Tests if the "blocked" flag is set for a user.
664     *
665     * @param  int      $user_id    User id to look for.
666     * @return boolean              True if the user is blocked, false otherwise.
667     */
668    function isBlocked($user_id=null)
669    {
670        $db =& DB::getInstance();
671
672        $this->initDB();
673
674        if ($this->getParam('blocking')) {
675            // Get user_id if specified.
676            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
677            $qid = $db->query("
678                SELECT 1
679                FROM " . $this->_params['db_table'] . "
680                WHERE blocked = 'true'
681                AND " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
682            ");
683            return mysql_num_rows($qid) === 1;
684        }
685    }
686
687    /**
[42]688     * Unblocks a user in the db_table, and clears any blocked_reason.
[1]689     */
690    function unblockAccount($user_id=null)
691    {
[136]692        $db =& DB::getInstance();
693   
[1]694        $this->initDB();
[136]695   
[1]696        if ($this->getParam('blocking')) {
697            // Get user_id if specified.
[148]698            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
[136]699            $db->query("
[1]700                UPDATE " . $this->_params['db_table'] . " SET
701                blocked = '',
702                blocked_reason = ''
[136]703                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
[1]704            ");
705        }
706    }
707
708    /**
709     * Returns true if username already exists in database.
710     *
711     * @param  string  $username    Username to look for.
712     * @return bool                 True if username exists.
713     */
714    function usernameExists($username)
[42]715    {
[136]716        $db =& DB::getInstance();
717   
[1]718        $this->initDB();
[42]719
[136]720        $qid = $db->query("
[42]721            SELECT 1
[15]722            FROM " . $this->_params['db_table'] . "
[136]723            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
[15]724        ");
[1]725        return (mysql_num_rows($qid) > 0);
726    }
727
728    /**
729     * Returns a username for a specified user id.
730     *
731     * @param  string  $user_id     User id to look for.
732     * @return string               Username, or false if none found.
733     */
734    function getUsername($user_id)
[42]735    {
[136]736        $db =& DB::getInstance();
737   
[1]738        $this->initDB();
[42]739
[136]740        $qid = $db->query("
[15]741            SELECT " . $this->_params['db_username_column'] . "
742            FROM " . $this->_params['db_table'] . "
[136]743            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
[15]744        ");
[1]745        if (list($username) = mysql_fetch_row($qid)) {
746            return $username;
747        } else {
748            return false;
749        }
750    }
751
752    /**
753     * Returns a randomly generated password based on $pattern. The pattern is any
754     * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this
755     * function will generate a pronouncable password. Recommend using more complex
756     * patterns, at minimum the US State Department standard: cvcddcvc.
757     *
758     * - x    a random upper or lower alpha character or digit
759     * - C    a random upper or lower consanant
760     * - V    a random upper or lower vowel
761     * - c    a random lowercase consanant
762     * - v    a random lowercase vowel
763     * - d    a random digit
764     *
765     * @param  string $pattern  a sequence of character types, above.
766     * @return string           a password
767     */
[174]768    function generatePassword($pattern='CvcdCvc')
[1]769    {
[174]770        $app =& App::getInstance();
771        if (preg_match('/[^xCVcvd]/', $pattern)) {
772            $app->logMsg(sprintf('Invalid pattern: %s', $pattern), LOG_WARNING, __FILE__, __LINE__);
773            $pattern='CvcdCvc';
774        }
[1]775        $str = '';
[247]776        for ($i=0; $i<mb_strlen($pattern); $i++) {
[259]777            $x = mb_substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789!@#%&*-=+.?', (mt_rand() % 71), 1);
[247]778            $c = mb_substr('bcdfghjklmnprstvwxz', (mt_rand() % 19), 1);
779            $C = mb_substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ', (mt_rand() % 38), 1);
780            $v = mb_substr('aeiouy', (mt_rand() % 6), 1);
781            $V = mb_substr('aeiouyAEIOUY', (mt_rand() % 12), 1);
782            $d = mb_substr('0123456789', (mt_rand() % 10), 1);
[259]783            $str .= $$pattern[$i];
[1]784        }
785        return $str;
786    }
[42]787
[1]788    /**
789     *
790     */
[124]791    function encryptPassword($password, $salt=null)
[1]792    {
[136]793        $app =& App::getInstance();
794       
795        // Existing password hashes rely on the same key/salt being used to compare encryptions.
796        // Don't change this unless you know existing hashes or signatures will not be affected!
797        $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81';
798       
[1]799        switch ($this->_params['encryption_type']) {
800        case AUTH_ENCRYPT_PLAINTEXT :
801            return $password;
802            break;
[42]803
[1]804        case AUTH_ENCRYPT_CRYPT :
[124]805            // If comparing plaintext password with a hash, provide first two chars of the hash as the salt.
[247]806            return isset($salt) ? crypt($password, mb_substr($salt, 0, 2)) : crypt($password);
[1]807            break;
[42]808
[1]809        case AUTH_ENCRYPT_SHA1 :
[136]810            return sha1($password);
[15]811            break;
[42]812
[136]813        case AUTH_ENCRYPT_SHA1_HARDENED :
814            $hash = sha1($app->getParam('signing_key') . $password . $more_salt);
815            // Increase key strength by 12 bits.
816            for ($i=0; $i < 4096; $i++) { 
817                $hash = sha1($hash); 
818            } 
819            return $hash;
820            break;
821
[1]822        case AUTH_ENCRYPT_MD5 :
[136]823            return md5($password);
824            break;
825
826        case AUTH_ENCRYPT_MD5_HARDENED :
827            // Include salt to improve hash
828            $hash = md5($app->getParam('signing_key') . $password . $more_salt);
829            // Increase key strength by 12 bits.
830            for ($i=0; $i < 4096; $i++) { 
831                $hash = md5($hash); 
832            } 
833            return $hash;
834            break;
[1]835        default :
[136]836            $app->logMsg(sprintf('Authentication encrypt type specified is unrecognized: %s', $this->_params['encryption_type']), LOG_NOTICE, __FILE__, __LINE__);
837            return false;
[1]838            break;
839        }
840    }
841
842    /**
[42]843     *
[1]844     */
845    function setPassword($user_id=null, $password)
[42]846    {
[136]847        $app =& App::getInstance();
848        $db =& DB::getInstance();
849   
[1]850        $this->initDB();
[42]851
[1]852        // Get user_id if specified.
[148]853        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
[42]854
[229]855        // Get old password.
856        $qid = $db->query("
857            SELECT userpass
858            FROM " . $this->_params['db_table'] . "
859            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
860        ");
861        if (!list($old_encrypted_password) = mysql_fetch_row($qid)) {
862            $app->logMsg(sprintf('Cannot set password for nonexistant user_id %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
863            return false;
864        }
865       
866        // Compare old with new to ensure we're actually *changing* the password.
867        $encrypted_password = $this->encryptPassword($password);
868        if ($old_encrypted_password == $encrypted_password) {
869            $app->logMsg(sprintf('Not setting password: new is the same as old.', null), LOG_INFO, __FILE__, __LINE__);
870            return false;
871        }
872
[1]873        // Issue the password change query.
[136]874        $db->query("
[42]875            UPDATE " . $this->_params['db_table'] . "
[229]876            SET userpass = '" . $db->escapeString($encrypted_password) . "'
[136]877            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
[1]878        ");
[119]879       
[136]880        if (mysql_affected_rows($db->getDBH()) != 1) {
[229]881            $app->logMsg(sprintf('Failed to update password for user %s', $user_id), LOG_WARNING, __FILE__, __LINE__);
882            return false;
[119]883        }
[229]884       
885        return true;
[1]886    }
887
888    /**
889     * Resets the password for the user with the specified id.
890     *
891     * @param  string $user_id   The id of the user to reset.
892     * @param  string $reason    Additional message to add to the reset email.
893     * @return string            The user's new password.
894     */
895    function resetPassword($user_id=null, $reason='')
896    {
[136]897        $app =& App::getInstance();
898        $db =& DB::getInstance();
899   
[1]900        $this->initDB();
[42]901
[1]902        // Get user_id if specified.
[148]903        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
[42]904
[1]905        // Reset password of a specific user.
[136]906        $qid = $db->query("
[1]907            SELECT * FROM " . $this->_params['db_table'] . "
[136]908            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
[1]909        ");
[15]910        if (!$user_data = mysql_fetch_assoc($qid)) {
[136]911            $app->logMsg(sprintf('Reset password failed. User %s not found.', $user_id), LOG_NOTICE, __FILE__, __LINE__);
[15]912            return false;
913        }
[42]914
[1]915        // Get new password.
916        $password = $this->generatePassword();
[42]917
[1]918        // Update password query.
919        $this->setPassword($user_id, $password);
[41]920
[43]921        // Make sure user has an email on record before continuing.
922        if (!isset($user_data['email']) || '' == trim($user_data['email'])) {
[136]923            $app->logMsg(sprintf('Password reset but notification failed, no email address for user %s (%s).', $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
[43]924        } else {
[189]925            // Send the new password in an email.
[43]926            $email = new Email(array(
927                'to' => $user_data['email'],
[136]928                'from' => sprintf('%s <%s>', $app->getParam('site_name'), $app->getParam('site_email')),
929                'subject' => sprintf('%s password change', $app->getParam('site_name'))
[43]930            ));
[189]931            $email->setTemplate('codebase/services/templates/email_reset_password.txt');
[43]932            $email->replace(array(
[189]933                'SITE_NAME' => $app->getParam('site_name'),
934                'SITE_URL' => $app->getParam('site_url'),
935                'SITE_EMAIL' => $app->getParam('site_email'),
936                'NAME' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]),
937                'USERNAME' => $user_data[$this->_params['db_username_column']],
938                'PASSWORD' => $password,
939                'REASON' => ('' == trim($reason) ? '' : trim($reason) . ' '), // Add a space after the reason if it exists for better fromatting.
[43]940            ));
941            $email->send();
942        }
[41]943
[15]944        return array(
[42]945            'username' => $user_data[$this->_params['db_username_column']],
[15]946            'userpass' => $password
947        );
[1]948    }
[42]949
[1]950    /**
951     * If the current user has access to the specified $security_zone, return true.
[266]952     * If the optional $user_type is supplied, test that against the zone.
[1]953     *
[266]954     * NOTE: "user_type" used to be called "priv" in some older implementations.
955     *
[1]956     * @param  constant $security_zone   string of comma delimited priviliges for the zone
[266]957     * @param  string   $user_type       a privilege that might be found in a zone
[1]958     * @return bool     true if user is a member of security zone, false otherwise
959     */
[266]960    function inClearanceZone($security_zone, $user_type='')
[1]961    {
962        return true;
963        $zone_members = preg_split('/,\s*/', $security_zone);
[266]964        $user_type = empty($user_type) ? $this->get('user_type') : $user_type;
[42]965
966        // If the current user's privilege level is NOT in that array or if the
[1]967        // user has no privilege, return false. Otherwise the user is clear.
[266]968        if (!in_array($user_type, $zone_members) || empty($user_type)) {
[1]969            return false;
970        } else {
971            return true;
972        }
973    }
[42]974
[1]975    /**
976     * This function tests a list of arguments $security_zone against the priv that the current user has.
[42]977     * If the user doesn't have one of the supplied privs, die.
[1]978     *
[266]979     * NOTE: "user_type" used to be called "priv" in some older implementations.
980     *
[1]981     * @param  constant $security_zone   string of comma delimited priviliges for the zone
982     */
[32]983    function requireAccessClearance($security_zone, $message='')
[1]984    {
[136]985        $app =& App::getInstance();
986   
[1]987        return true;
988        $zone_members = preg_split('/,\s*/', $security_zone);
[42]989
990        /* If the current user's privilege level is NOT in that array or if the
[1]991         * user has no privilege, DIE with a message. */
[266]992        if (!in_array($this->get('user_type'), $zone_members) || !$this->get('user_type')) {
[32]993            $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message;
[136]994            $app->raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__);
995            $app->dieBoomerangURL();
[1]996        }
997    }
998
999} // end class
1000
[51]1001// CIDR cheat-sheet
[1]1002//
[42]1003// Netmask              Netmask (binary)                 CIDR     Notes
[1]1004// _____________________________________________________________________________
1005// 255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
[51]1006// 255.255.255.254  11111111.11111111.11111111.11111110  /31  Unusable
[1]1007// 255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
1008// 255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
1009// 255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
1010// 255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
1011// 255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
1012// 255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
1013// 255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable
[42]1014//
[1]1015// 255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
1016// 255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
1017// 255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
1018// 255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
1019// 255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
1020// 255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
1021// 255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
1022// 255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
[42]1023//
[1]1024// 255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
1025// 255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
1026// 255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
1027// 255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
1028// 255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
1029// 255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
1030// 255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
1031// 255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
[42]1032//
[1]1033// 254.0.0.0        11111110.00000000.00000000.00000000  /7
1034// 252.0.0.0        11111100.00000000.00000000.00000000  /6
1035// 248.0.0.0        11111000.00000000.00000000.00000000  /5
1036// 240.0.0.0        11110000.00000000.00000000.00000000  /4
1037// 224.0.0.0        11100000.00000000.00000000.00000000  /3
1038// 192.0.0.0        11000000.00000000.00000000.00000000  /2
1039// 128.0.0.0        10000000.00000000.00000000.00000000  /1
1040// 0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
[198]1041?>
Note: See TracBrowser for help on using the repository browser.