source: tags/2.1.5/lib/Auth_SQL.inc.php

Last change on this file was 377, checked in by quinn, 14 years ago

Releasing trunk as stable version 2.1.5

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