This commit is contained in:
2007-12-15 02:19:09 +00:00
commit 66ca16f8b0
24 changed files with 1199 additions and 0 deletions

38
code/auth_mysql.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
/*
* MySQL Authentication Module
*
*/
class Authenticator_mysql implements IAuthenticator {
public function __construct() {
}
public function initialise() {
// Nothing required
}
public function uninitialise() {
// Nothing required
}
public function user_exists( $username ) {
return true;
}
public function authenticate( $username, $password ) {
return true;
}
public function username2fullname( $username ) {
throw new NotImplementedException();
}
public function fullname2username( $fullname ) {
throw new NotImplementedException();
}
};
?>

103
code/config.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
/*
* Configuration
* This file contains all global configuration variables
*/
$_config = array();
// DEBUG MODE
// This config variable triggers verbose error output, which is useful for developers, but
// would be ugly or bad practice to show to users. Enable this variable on development systems
// and disable it for a production system.
$_config['DEBUG'] = true;
error_reporting( E_ALL | E_NOTICE | E_STRICT );
// Current timezone settings
date_default_timezone_set('Europe/London');
require_once('request_handler.php');
require_once("functions.php");
require_once("exceptions.php");
require_once("session_handler.php");
// Variable used for passing information around the script,
// Should not be used by the user
$_meta = array( 'base-dir' => get_web_base_dir(),
'script-dir' => get_fs_base_dir(),
'error-code' => 403,
'error-message' => 'Unknown Error');
// Placeholder for template fragments
$_template = array( 'messages' => array(),
'redirect-to' => false,
'head' => '',
'title' => 'Default',
'page' => '');
// Forward declarations
$_session = null;
// Templating
$_config['template-file'] = "{$_meta['script-dir']}/templates/default.php";
// Homepage
$_config['homepage'] = "{$_meta['script-dir']}/page-sources/home.php";
// Sessions
// How long a logged in session will last without activity from the user
$_config['session-login-timeout'] = 60*60*1; // One hour
// How long the username will be stoed in the users cookie
$_config['session-username-timeout'] = 60*60*24*7; // Seven days
// See session-handler.php for reasons behind the session-network-* config variables
// session-network-mask is used to determine how much of the users IP is hashed into the salt
$_config['session-network-mask'] = 24;
// session-network-mode defines whether the above parameter was passed as a CIDR form, or dotted decimal form
$_config['session-network-mode'] = MASK_CIDR;
// Maximum number of items to keep in the users session history
$_config['max-history'] = 5;
// Account Lockout
// How many incorrect authentication attempts before the user is locked out
$_config['lockout-attempts'] = 3;
// How long the account lockout period lasts. During this time, the user will not
// be able to authenticate, even witha valid passwords
$_config['lockout-duration'] = 60*10; // Ten minutes
// Authantication
require_once( "{$_meta['script-dir']}/code/iauthenticator.php" );
$_config['authentication-module'] = 'mysql';
// Database
// Mysql connections parameters
$_config['db'] = null;
$_config['mysql'] = array();
$_config['mysql']['host'] = 'localhost';
$_config['mysql']['port'] = 3306;
$_config['mysql']['username'] = '';
$_config['mysql']['password'] = '';
$_config['mysql']['database'] = '';
// Database tables
$_config['mysql']['prefix'] = '';
// Connecting to the database
$_db = null;
require_once( "{$_meta['script-dir']}/code/db_mysql.php" );
// Only show php error messages if the application is in debug mode
if( isset($_GET['nodebug']) ) $_config['DEBUG'] = false;
if( !$_config['DEBUG'] ) {
set_error_handler( "null_error_handler" );
set_exception_handler( "null_exception_handler" );
}
// Set the default template for all individual pages
$_template['template-file'] = $_config['template-file'];
?>

21
code/db_mysql.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
/*
* MySQL Database wrappper
*
*/
if( $_config['db'] == 'mysql' ) {
// Egress checking, make sure everyting we need is available
if( !class_exists( 'mysqli' ) ) throw new ConfigException('Missing required PHP Extension "mysqli".');
if( !is_array( $_config['mysql'] ) ) throw new ConfigException('Missing configuration data: mysql');
// Connect to the database using the config variables provided in _config
$_db = new mysqli( $_config['mysql']['host'], $_config['mysql']['username'], $_config['mysql']['password'], $_config['mysql']['database'], $_config['mysql']['port'] );
if( mysqli_connect_errno() ){
throw new ConfigException('Cannot connect to the mysql database');
}
}
?>

41
code/exceptions.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
/*
* Exceptions
*
*/
class BaseException extends Exception {
// Overridden constuctor with message and code parameters, which will optionally display error output
public function __construct( $message = '', $code = 0) {
global $_config;
parent::__construct( $message, $code );
// If debug mode is on, print out the raw data
if( $_config['DEBUG'] ) {
echo get_class($this) . ": Code='{$code}', Message='{$message}'<br />\n";
echo '<pre>';print_r($this->getTrace());echo '</pre>';
}
}
};
class FatalException extends BaseException {
// Overridden constructor prints an error message, then terminates the application
public function __construct( $message = '', $code = 0 ) {
parent::__construct( $message, $code );
die( 'FATAL EXCEPTION: ' . $message );
}
};
class ConfigException extends BaseException {};
class SessionException extends BaseException {};
class AccountLockoutException extends BaseException {};
class AuthenticationException extends BaseException {};
class ParameterException extends BaseException {};
class NotImplementedException extends BaseException {};
?>

76
code/functions.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
/*
* Utility Functions
*
*/
define("MASK_DOTTED_DECIMAL", 1);
define("MASK_CIDR", 2);
// Override the default error handler to prevent error messages being shown to the user
function null_error_handler( $errno, $errstr, $errfile, $errline, $errcontext ) {
switch( $errono ) {
case E_ERROR:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
case E_RECOVERABLE_ERROR: {
// Fatal error, cause the page to fail here
header("HTTP/1.1 500 Internal Server Error");
exit(0);
}
default: {
// non fatal error, let it pass
return true;
}
}
}
// Override the default exception handler to prevent error messages being shown to the user
function null_exception_handler( $exception ) {
return true;
}
function get_web_base_dir() {
static $Dir;
if (!isset($Dir))
$Dir = dirname($_SERVER['SCRIPT_NAME']);
return $Dir;
}
function get_fs_base_dir() {
static $Dir;
if (!isset($Dir))
$Dir = dirname($_SERVER['SCRIPT_FILENAME']);
return $Dir;
}
function get_network_mask( $ip, $subnet, $mode = MASK_DOTTED_DECIMAL ) {
$ip_l = ip2long( $ip );
if( $mode == MASK_DOTTED_DECIMAL )
// 255.255.255.0 type subnet
$subnet_l = ip2long( $subnet );
else
// CIDR type subnet
$subnet_l = (~0) << (32 - $subnet);
// Mask the two together
$network_l = $ip_l & $subnet_l;
return long2ip($network_l);
}
function array_clone( &$source, &$dest ) {
foreach( $source as $key => $value ) {
$dest[$key] = $value;
}
}
function print_rating_graph( $star_rating ) {
global $_meta;
?>
<img class="foreground" src="<?php echo $_meta['base-dir']; ?>/resources/ratings/<?php echo $star_rating; ?>" alt="Rated: <?php echo $star_rating; ?>" />
<?php
}
?>

60
code/iauthenticator.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
/*
* IAuthenticator
* Interface for authentication classes
*/
interface IAuthenticator {
// Do any module-specific initialisation
public function initialise();
// Shutdown the module cleanly when finished
public function uninitialise();
// Identifies whether a user with a given name exists
public function user_exists( $username );
// Checks a given username and password
public function authenticate( $username, $password );
// Does translation between a username and full name
public function username2fullname( $username );
// And vice versa
public function fullname2username( $fullname );
};
/*
* IAuthenticatorFactory
* Creates an instance of an Authenticator module, given its name
*/
class IAuthenticatorFactory {
// Prevent any instances of this class being constructed
private function __construct() {
}
public static function get( $module ) {
global $_meta;
// Get the filename and classnames for this module
$filename = "{$_meta['script-dir']}/code/auth_{$module}.php";
$classname = "Authenticator_{$module}";
// Check to make sure this module exists
if( !file_exists($filename) ) throw new ConfigException("Authentication module could not be found: '{$filename}.'", 500);
// Import the auth modules code
require_once( $filename );
// Ensure the class has been defined
if( !class_exists( $classname ) ) throw new ConfigException("Authentication module is invalid: '{$classname}'.", 500);
// Create an instance of the module, and return it
return new $classname();
}
};
?>

58
code/iauthorisor.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
/*
* IAuthorisor
* Interfakce for authorisation classes
*/
interface IAuthorisor {
// Do any module-specific initialisation
public function initialise();
// Shutdown the module cleanly when finished
public function uninitialise();
// Find out which usergroup a given member belongs
public function is_student( $username, $module, $year = null );
public function is_lecturer( $username, $module, $year = null );
public function is_sysadmin( $username );
// A sysadmin should not be able to administrate a course for which they are a student
public function may_sysadmin( $username, $module, $year = null );
};
/*
* IAuthorisor
* Interface for authorisation classes
*/
class IAuthorisorFactory {
public function __construct() {
}
public static function get( $module ) {
global $_meta;
// Get the filename and classnames for this module
$filename = "{$_meta['script-dir']}/code/auth_{$module}.php";
$classname = "Authorisor_{$module}";
// Check to make sure this module exists
if( !file_exists($filename) ) throw new ConfigException("Authorisation module could not be found: '{$filename}.'", 500);
// Import the auth modules code
require_once( $filename );
// Ensure the class has been defined
if( !class_exists( $classname ) ) throw new ConfigException("Authorisation module is invalid: '{$classname}'.", 500);
// Create an instance of the module, and return it
return new $classname();
}
};
?>

71
code/request_handler.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
/*
* RequestHandler
* This class decodes the Request URI in order to store multiple variables in the request URI itself
*/
class RequestHandler {
// The request uri
private $request_string;
// Stores a list of all the variables we've already found to avoid needing to
// find them using regular expressions many times.
private $cache;
public function __construct( $request_string ) {
$this->request_string = $request_string;
$this->cache = array();
}
public function current_page() {
return $this->request_string;
}
public function get( $key, $value_pattern = '[^/]*' ) {
// Look in the cache to see if weve already decoded this variable
if( in_array( $key, $this->cache ) ) return $this->cache[ $key ];
// Construct the regex to search for /$key/$value/ pairs, and return the $value part
$key = str_replace('£', '\£', $key);
$value_pattern = str_replace('£', '\£', $value_pattern);
$pattern = "£/{$key}/({$value_pattern})(/|\$";
// Look to see if this variable is in the request string
$count = preg_match( $pattern, $this->request_string, $matches );
// See if the variable was set
if( $count == 0 ) return null;
// Store the result for next time
$this->cache[ $key ] = $matches[1];
// And return it to the user
return $matches[1];
}
public function construct() {
global $_meta;
// Varargsy
$args = func_get_args();
// Construct the proper request string for these arguments
$request_string = "{$_meta['base-dir']}/";
$count = count( $args );
for( $i = 0 ; $i < $count; $i++ ) {
$arg = $args[ $i ];
// If this item is null, try to find the value of the previous key from the current
// request object as a convenience to the user. It assumes the default value pattern.
if( $arg === null && $i > 0) {
$arg = $this->get( $args[ $i -1 ] );
}
$request_string .= urlencode($arg) . '/';
}
return $request_string;
}
};
?>

229
code/session_handler.php Normal file
View File

@@ -0,0 +1,229 @@
<?php
/*
* SessionHandler
* This class handles using php sessions to maintain user state across
* multiple sessions
*/
class SessionHandler {
private $authentication;
public function __construct() {
global $_config;
// Initialise PHP's Session subsystem
session_start();
// Check to see if this is a new session
if( !isset( $_SESSION['initialised'] ) || !$_SESSION['initialised'] ) {
$this->start_new_session();
} else {
// The session already exists, check its validity
if( $this->is_session_valid() ) {
} else {
// BAD USER SESSION, start a new one
$this->start_new_session();
// And inform the user
$_template['messages'][] = "Bad session, starting a new one";
}
}
// Load the Authentication modules
$this->authentication = IAuthenticatorFactory::get( $_config['authentication-module'] );
$this->authentication->initialise();
}
public function __destruct() {
// Shut down the authentication modules
$this->authentication->uninitialise();
}
/*
* Accessors
*/
public function authenticator() { return $this->authentication; }
/*
* The following methods deal with the user session
*/
// Initialises all the variables we require in each user session
public function start_new_session() {
global $_config;
// Set up all the session variables
$_SESSION['initialised'] = true;
$_SESSION['logged_in'] = false;
$_SESSION['username'] = '';
$_SESSION['hash'] = $this->generate_hash();
$_SESSION['previous_page'] = $_config['homepage'];
$_SESSION['requested_page'] = '';
$_SESSION['history'] = array('home');
$_SESSION['lockout'] = false;
$_SESSION['lockout-attempts'] = 0;
// Be paranoid, change session id
$this->auth_state_changed();
}
public function is_session_valid() {
// Check that the identifying information given by the user matches that which was
// saved in the session when it was created
return ($_SESSION['hash'] == $this->generate_hash());
}
/*
* This function should be called whenever the authorisation level
* changes in order to keep the session secure.
* It prevents against Session Fixation (http://www.acros.si/papers/session_fixation.pdf)
*/
public function auth_state_changed() {
session_regenerate_id();
}
/*
* This function marks a user as having been logged into the system
*/
private function mark_logged_in_as( $username ) {
$_SESSION['username'] = $username;
$_SESSION['logged_in'] = true;
// The login state has changed
$this->auth_state_changed();
}
/*
* This function marks a user as having been logged out of the system
*/
private function mark_logged_out() {
$_SESSION['logged_in'] = false;
// The login state has changedd
$this->auth_state_changed();
}
/*
* This function returns whether or not the user is logged in
*/
public function is_logged_in() {
return $_SESSION['logged_in'];
}
/*
* This function returns the username of the currently logged in user
*/
public function username() {
return $_SESSION['username'];
}
/*
* This function requires a user to be logged in, else an exception is thrown
*/
public function require_logged_in() {
if( !$_SESSION['logged_in'] ) throw new AuthenticationException('You must be logged on to view this resource');
}
/*
* This function generates a hash from user identifiable information
* to try and prevent session theft.
* If the session id is stolen by an attacker, chances are some of the
* information used to generate the hash will change, and the session
* will be immediately marked as invalid. This hash is checked for
* consistency on every request.
*/
public function generate_hash() {
global $_config;
// Hash together the following pieces of identifying information which remain constant throughout the session:
// User Agent string - This will be constant for the user, but might not be for the attacker
// Netork Mask - ensures each request is coming from the same network. We may not be able to use the
// while ip, because some ISPs use load-balanced proxies, so subsequent requests may come from a
// different machine ip, but we can still use at least part of the address to filter out would be attackers.
$key = $_SERVER['HTTP_USER_AGENT'] . get_network_mask( $_SERVER['REMOTE_ADDR'], $_config['session-network-mask'], $_config['session-network-mode']);
return md5( $key );
}
/*
* The following methods deal with the users history
* These will be used to set up redirection after special pages, such as login
*/
// Add a new item to the user history
public function history_add_request( $page ) {
global $_config;
// Add this item to the beginning of the history array
array_unshift( $_SESSION['history'], $page );
// Keep the size below a fixed limit
if( count($_SESSION['history']) > $_config['max-history'] ) {
array_pop( $_SESSION['history'] );
}
}
// Remove the current item from the user history
public function history_drop_request() {
// remove the item from the beginning of the array
array_shift( $_SESSION['history'] );
}
// Return an item from the user history
public function history_get( $index ) {
if( is_numeric($index) ) {
if( $index < count($_SESSION['history']) ) {
return $_SESSION['history'][$index];
}
}
}
/*
* The following methods deal with user authentication and authorisation
*/
// Log the user in
public function login( $username, $password ) {
global $_config;
// Check the user hasnt been locked out before trying to login
if( $_SESSION['lockout'] == true ) {
// See if the lockout has expired
if($_SESSION['lockout-expiry'] > time() ) throw new AccountLockoutException('Your session is currently locked as a result of enterring too many incorrect passwords. You will not be able to attempt a login for another ' . date('i \m\i\n(\s), s \s\e\c(\s)', $_SESSION['lockout-expiry']-time()));
else {
// Unset the lockout
$_SESSION['lockout'] = false;
$_SESSION['lockout-expiry'] = 0;
$_SESSION['lockout-attempts'] = 0;
}
}
try {
// Attempt to authenticate with these credentials
$this->authentication->authenticate( $username, $password );
} catch( Exception $e ) {
// Increment the number of failed authentication attempts
$_SESSION['lockout-attempts']++;
// Check the lockout attempts
if( $_SESSION['lockout-attempts'] >= $_config['lockout-attempts'] ) {
$_SESSION['lockout'] = true;
$_SESSION['lockout-expiry'] = time() + $_config['lockout-duration'];
throw new AccountLockoutException('You have enterred an incorrect password too many times, and your session has been locked. You will not be able to attempt another login for the next 10 minutes.');
}
// The login failed, rethrow the original exception
throw $e;
}
// Successful login, update the session state
$this->mark_logged_in_as( $username );
}
// Log the current user out
public function logout() {
$this->mark_logged_out();
}
};
?>