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

Last change on this file since 396 was 396, checked in by anonymous, 12 years ago

Updated copyright date; comments elaboration; spelling fixes.

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