PageRenderTime 46ms CodeModel.GetById 25ms app.highlight 15ms RepoModel.GetById 2ms app.codeStats 0ms

/code/ryzom/tools/server/www/webtt/cake/libs/controller/components/security.php

https://bitbucket.org/SirCotare/ryzom
PHP | 747 lines | 368 code | 73 blank | 306 comment | 106 complexity | 95442ef058cfacaf8fb481b76452d35a MD5 | raw file
  1<?php
  2/**
  3 * Security Component
  4 *
  5 * PHP versions 4 and 5
  6 *
  7 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8 * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9 *
 10 * Licensed under The MIT License
 11 * Redistributions of files must retain the above copyright notice.
 12 *
 13 * @copyright     Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
 14 * @link          http://cakephp.org CakePHP(tm) Project
 15 * @package       cake
 16 * @subpackage    cake.cake.libs.controller.components
 17 * @since         CakePHP(tm) v 0.10.8.2156
 18 * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
 19 */
 20App::import('Core', array('String', 'Security'));
 21
 22/**
 23 * SecurityComponent
 24 *
 25 * @package       cake
 26 * @subpackage    cake.cake.libs.controller.components
 27 * @link http://book.cakephp.org/view/1296/Security-Component
 28 */
 29class SecurityComponent extends Object {
 30
 31/**
 32 * The controller method that will be called if this request is black-hole'd
 33 *
 34 * @var string
 35 * @access public
 36 */
 37	var $blackHoleCallback = null;
 38
 39/**
 40 * List of controller actions for which a POST request is required
 41 *
 42 * @var array
 43 * @access public
 44 * @see SecurityComponent::requirePost()
 45 */
 46	var $requirePost = array();
 47
 48/**
 49 * List of controller actions for which a GET request is required
 50 *
 51 * @var array
 52 * @access public
 53 * @see SecurityComponent::requireGet()
 54 */
 55	var $requireGet = array();
 56
 57/**
 58 * List of controller actions for which a PUT request is required
 59 *
 60 * @var array
 61 * @access public
 62 * @see SecurityComponent::requirePut()
 63 */
 64	var $requirePut = array();
 65
 66/**
 67 * List of controller actions for which a DELETE request is required
 68 *
 69 * @var array
 70 * @access public
 71 * @see SecurityComponent::requireDelete()
 72 */
 73	var $requireDelete = array();
 74
 75/**
 76 * List of actions that require an SSL-secured connection
 77 *
 78 * @var array
 79 * @access public
 80 * @see SecurityComponent::requireSecure()
 81 */
 82	var $requireSecure = array();
 83
 84/**
 85 * List of actions that require a valid authentication key
 86 *
 87 * @var array
 88 * @access public
 89 * @see SecurityComponent::requireAuth()
 90 */
 91	var $requireAuth = array();
 92
 93/**
 94 * List of actions that require an HTTP-authenticated login (basic or digest)
 95 *
 96 * @var array
 97 * @access public
 98 * @see SecurityComponent::requireLogin()
 99 */
100	var $requireLogin = array();
101
102/**
103 * Login options for SecurityComponent::requireLogin()
104 *
105 * @var array
106 * @access public
107 * @see SecurityComponent::requireLogin()
108 */
109	var $loginOptions = array('type' => '', 'prompt' => null);
110
111/**
112 * An associative array of usernames/passwords used for HTTP-authenticated logins.
113 *
114 * @var array
115 * @access public
116 * @see SecurityComponent::requireLogin()
117 */
118	var $loginUsers = array();
119
120/**
121 * Controllers from which actions of the current controller are allowed to receive
122 * requests.
123 *
124 * @var array
125 * @access public
126 * @see SecurityComponent::requireAuth()
127 */
128	var $allowedControllers = array();
129
130/**
131 * Actions from which actions of the current controller are allowed to receive
132 * requests.
133 *
134 * @var array
135 * @access public
136 * @see SecurityComponent::requireAuth()
137 */
138	var $allowedActions = array();
139
140/**
141 * Form fields to disable
142 *
143 * @var array
144 * @access public
145 */
146	var $disabledFields = array();
147
148/**
149 * Whether to validate POST data.  Set to false to disable for data coming from 3rd party
150 * services, etc.
151 *
152 * @var boolean
153 * @access public
154 */
155	var $validatePost = true;
156
157/**
158 * Other components used by the Security component
159 *
160 * @var array
161 * @access public
162 */
163	var $components = array('RequestHandler', 'Session');
164
165/**
166 * Holds the current action of the controller
167 *
168 * @var string
169 */
170	var $_action = null;
171
172/**
173 * Initialize the SecurityComponent
174 *
175 * @param object $controller Controller instance for the request
176 * @param array $settings Settings to set to the component
177 * @return void
178 * @access public
179 */
180	function initialize(&$controller, $settings = array()) {
181		$this->_set($settings);
182	}
183
184/**
185 * Component startup. All security checking happens here.
186 *
187 * @param object $controller Instantiating controller
188 * @return void
189 * @access public
190 */
191	function startup(&$controller) {
192		$this->_action = strtolower($controller->action);
193		$this->_methodsRequired($controller);
194		$this->_secureRequired($controller);
195		$this->_authRequired($controller);
196		$this->_loginRequired($controller);
197
198		$isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut());
199		$isRequestAction = (
200			!isset($controller->params['requested']) ||
201			$controller->params['requested'] != 1
202		);
203
204		if ($isPost && $isRequestAction && $this->validatePost) {
205			if ($this->_validatePost($controller) === false) {
206				if (!$this->blackHole($controller, 'auth')) {
207					return null;
208				}
209			}
210		}
211		$this->_generateToken($controller);
212	}
213
214/**
215 * Sets the actions that require a POST request, or empty for all actions
216 *
217 * @return void
218 * @access public
219 * @link http://book.cakephp.org/view/1299/requirePost
220 */
221	function requirePost() {
222		$args = func_get_args();
223		$this->_requireMethod('Post', $args);
224	}
225
226/**
227 * Sets the actions that require a GET request, or empty for all actions
228 *
229 * @return void
230 * @access public
231 */
232	function requireGet() {
233		$args = func_get_args();
234		$this->_requireMethod('Get', $args);
235	}
236
237/**
238 * Sets the actions that require a PUT request, or empty for all actions
239 *
240 * @return void
241 * @access public
242 */
243	function requirePut() {
244		$args = func_get_args();
245		$this->_requireMethod('Put', $args);
246	}
247
248/**
249 * Sets the actions that require a DELETE request, or empty for all actions
250 *
251 * @return void
252 * @access public
253 */
254	function requireDelete() {
255		$args = func_get_args();
256		$this->_requireMethod('Delete', $args);
257	}
258
259/**
260 * Sets the actions that require a request that is SSL-secured, or empty for all actions
261 *
262 * @return void
263 * @access public
264 * @link http://book.cakephp.org/view/1300/requireSecure
265 */
266	function requireSecure() {
267		$args = func_get_args();
268		$this->_requireMethod('Secure', $args);
269	}
270
271/**
272 * Sets the actions that require an authenticated request, or empty for all actions
273 *
274 * @return void
275 * @access public
276 * @link http://book.cakephp.org/view/1301/requireAuth
277 */
278	function requireAuth() {
279		$args = func_get_args();
280		$this->_requireMethod('Auth', $args);
281	}
282
283/**
284 * Sets the actions that require an HTTP-authenticated request, or empty for all actions
285 *
286 * @return void
287 * @access public
288 * @link http://book.cakephp.org/view/1302/requireLogin
289 */
290	function requireLogin() {
291		$args = func_get_args();
292		$base = $this->loginOptions;
293
294		foreach ($args as $i => $arg) {
295			if (is_array($arg)) {
296				$this->loginOptions = $arg;
297				unset($args[$i]);
298			}
299		}
300		$this->loginOptions = array_merge($base, $this->loginOptions);
301		$this->_requireMethod('Login', $args);
302
303		if (isset($this->loginOptions['users'])) {
304			$this->loginUsers =& $this->loginOptions['users'];
305		}
306	}
307
308/**
309 * Attempts to validate the login credentials for an HTTP-authenticated request
310 *
311 * @param string $type Either 'basic', 'digest', or null. If null/empty, will try both.
312 * @return mixed If successful, returns an array with login name and password, otherwise null.
313 * @access public
314 * @link http://book.cakephp.org/view/1303/loginCredentials-string-type
315 */
316	function loginCredentials($type = null) {
317		switch (strtolower($type)) {
318			case 'basic':
319				$login = array('username' => env('PHP_AUTH_USER'), 'password' => env('PHP_AUTH_PW'));
320				if (!empty($login['username'])) {
321					return $login;
322				}
323			break;
324			case 'digest':
325			default:
326				$digest = null;
327
328				if (version_compare(PHP_VERSION, '5.1') != -1) {
329					$digest = env('PHP_AUTH_DIGEST');
330				} elseif (function_exists('apache_request_headers')) {
331					$headers = apache_request_headers();
332					if (isset($headers['Authorization']) && !empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) == 'Digest ') {
333						$digest = substr($headers['Authorization'], 7);
334					}
335				} else {
336					// Server doesn't support digest-auth headers
337					trigger_error(__('SecurityComponent::loginCredentials() - Server does not support digest authentication', true), E_USER_WARNING);
338				}
339
340				if (!empty($digest)) {
341					return $this->parseDigestAuthData($digest);
342				}
343			break;
344		}
345		return null;
346	}
347
348/**
349 * Generates the text of an HTTP-authentication request header from an array of options.
350 *
351 * @param array $options Set of options for header
352 * @return string HTTP-authentication request header
353 * @access public
354 * @link http://book.cakephp.org/view/1304/loginRequest-array-options
355 */
356	function loginRequest($options = array()) {
357		$options = array_merge($this->loginOptions, $options);
358		$this->_setLoginDefaults($options);
359		$auth = 'WWW-Authenticate: ' . ucfirst($options['type']);
360		$out = array('realm="' . $options['realm'] . '"');
361
362		if (strtolower($options['type']) == 'digest') {
363			$out[] = 'qop="auth"';
364			$out[] = 'nonce="' . uniqid("") . '"';
365			$out[] = 'opaque="' . md5($options['realm']) . '"';
366		}
367
368		return $auth . ' ' . implode(',', $out);
369	}
370
371/**
372 * Parses an HTTP digest authentication response, and returns an array of the data, or null on failure.
373 *
374 * @param string $digest Digest authentication response
375 * @return array Digest authentication parameters
376 * @access public
377 * @link http://book.cakephp.org/view/1305/parseDigestAuthData-string-digest
378 */
379	function parseDigestAuthData($digest) {
380		if (substr($digest, 0, 7) == 'Digest ') {
381			$digest = substr($digest, 7);
382		}
383		$keys = array();
384		$match = array();
385		$req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);
386		preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9@=.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
387
388		foreach ($match as $i) {
389			$keys[$i[1]] = $i[3];
390			unset($req[$i[1]]);
391		}
392
393		if (empty($req)) {
394			return $keys;
395		}
396		return null;
397	}
398
399/**
400 * Generates a hash to be compared with an HTTP digest-authenticated response
401 *
402 * @param array $data HTTP digest response data, as parsed by SecurityComponent::parseDigestAuthData()
403 * @return string Digest authentication hash
404 * @access public
405 * @see SecurityComponent::parseDigestAuthData()
406 * @link http://book.cakephp.org/view/1306/generateDigestResponseHash-array-data
407 */
408	function generateDigestResponseHash($data) {
409		return md5(
410			md5($data['username'] . ':' . $this->loginOptions['realm'] . ':' . $this->loginUsers[$data['username']]) .
411			':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' .
412			md5(env('REQUEST_METHOD') . ':' . $data['uri'])
413		);
414	}
415
416/**
417 * Black-hole an invalid request with a 404 error or custom callback. If SecurityComponent::$blackHoleCallback
418 * is specified, it will use this callback by executing the method indicated in $error
419 *
420 * @param object $controller Instantiating controller
421 * @param string $error Error method
422 * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
423 * @access public
424 * @see SecurityComponent::$blackHoleCallback
425 * @link http://book.cakephp.org/view/1307/blackHole-object-controller-string-error
426 */
427	function blackHole(&$controller, $error = '') {
428		if ($this->blackHoleCallback == null) {
429			$code = 404;
430			if ($error == 'login') {
431				$code = 401;
432				$controller->header($this->loginRequest());
433			}
434			$controller->redirect(null, $code, true);
435		} else {
436			return $this->_callback($controller, $this->blackHoleCallback, array($error));
437		}
438	}
439
440/**
441 * Sets the actions that require a $method HTTP request, or empty for all actions
442 *
443 * @param string $method The HTTP method to assign controller actions to
444 * @param array $actions Controller actions to set the required HTTP method to.
445 * @return void
446 * @access protected
447 */
448	function _requireMethod($method, $actions = array()) {
449		if (isset($actions[0]) && is_array($actions[0])) {
450			$actions = $actions[0];
451		}
452		$this->{'require' . $method} = (empty($actions)) ? array('*'): $actions;
453	}
454
455/**
456 * Check if HTTP methods are required
457 *
458 * @param object $controller Instantiating controller
459 * @return bool true if $method is required
460 * @access protected
461 */
462	function _methodsRequired(&$controller) {
463		foreach (array('Post', 'Get', 'Put', 'Delete') as $method) {
464			$property = 'require' . $method;
465			if (is_array($this->$property) && !empty($this->$property)) {
466				$require = array_map('strtolower', $this->$property);
467
468				if (in_array($this->_action, $require) || $this->$property == array('*')) {
469					if (!$this->RequestHandler->{'is' . $method}()) {
470						if (!$this->blackHole($controller, strtolower($method))) {
471							return null;
472						}
473					}
474				}
475			}
476		}
477		return true;
478	}
479
480/**
481 * Check if access requires secure connection
482 *
483 * @param object $controller Instantiating controller
484 * @return bool true if secure connection required
485 * @access protected
486 */
487	function _secureRequired(&$controller) {
488		if (is_array($this->requireSecure) && !empty($this->requireSecure)) {
489			$requireSecure = array_map('strtolower', $this->requireSecure);
490
491			if (in_array($this->_action, $requireSecure) || $this->requireSecure == array('*')) {
492				if (!$this->RequestHandler->isSSL()) {
493					if (!$this->blackHole($controller, 'secure')) {
494						return null;
495					}
496				}
497			}
498		}
499		return true;
500	}
501
502/**
503 * Check if authentication is required
504 *
505 * @param object $controller Instantiating controller
506 * @return bool true if authentication required
507 * @access protected
508 */
509	function _authRequired(&$controller) {
510		if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->data)) {
511			$requireAuth = array_map('strtolower', $this->requireAuth);
512
513			if (in_array($this->_action, $requireAuth) || $this->requireAuth == array('*')) {
514				if (!isset($controller->data['_Token'] )) {
515					if (!$this->blackHole($controller, 'auth')) {
516						return null;
517					}
518				}
519
520				if ($this->Session->check('_Token')) {
521					$tData = unserialize($this->Session->read('_Token'));
522
523					if (!empty($tData['allowedControllers']) && !in_array($controller->params['controller'], $tData['allowedControllers']) || !empty($tData['allowedActions']) && !in_array($controller->params['action'], $tData['allowedActions'])) {
524						if (!$this->blackHole($controller, 'auth')) {
525							return null;
526						}
527					}
528				} else {
529					if (!$this->blackHole($controller, 'auth')) {
530						return null;
531					}
532				}
533			}
534		}
535		return true;
536	}
537
538/**
539 * Check if login is required
540 *
541 * @param object $controller Instantiating controller
542 * @return bool true if login is required
543 * @access protected
544 */
545	function _loginRequired(&$controller) {
546		if (is_array($this->requireLogin) && !empty($this->requireLogin)) {
547			$requireLogin = array_map('strtolower', $this->requireLogin);
548
549			if (in_array($this->_action, $requireLogin) || $this->requireLogin == array('*')) {
550				$login = $this->loginCredentials($this->loginOptions['type']);
551
552				if ($login == null) {
553					$controller->header($this->loginRequest());
554
555					if (!empty($this->loginOptions['prompt'])) {
556						$this->_callback($controller, $this->loginOptions['prompt']);
557					} else {
558						$this->blackHole($controller, 'login');
559					}
560				} else {
561					if (isset($this->loginOptions['login'])) {
562						$this->_callback($controller, $this->loginOptions['login'], array($login));
563					} else {
564						if (strtolower($this->loginOptions['type']) == 'digest') {
565							if ($login && isset($this->loginUsers[$login['username']])) {
566								if ($login['response'] == $this->generateDigestResponseHash($login)) {
567									return true;
568								}
569							}
570							$this->blackHole($controller, 'login');
571						} else {
572							if (
573								!(in_array($login['username'], array_keys($this->loginUsers)) &&
574								$this->loginUsers[$login['username']] == $login['password'])
575							) {
576								$this->blackHole($controller, 'login');
577							}
578						}
579					}
580				}
581			}
582		}
583		return true;
584	}
585
586/**
587 * Validate submitted form
588 *
589 * @param object $controller Instantiating controller
590 * @return bool true if submitted form is valid
591 * @access protected
592 */
593	function _validatePost(&$controller) {
594		if (empty($controller->data)) {
595			return true;
596		}
597		$data = $controller->data;
598
599		if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['key'])) {
600			return false;
601		}
602		$token = $data['_Token']['key'];
603
604		if ($this->Session->check('_Token')) {
605			$tokenData = unserialize($this->Session->read('_Token'));
606
607			if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
608				return false;
609			}
610		}
611
612		$locked = null;
613		$check = $controller->data;
614		$token = urldecode($check['_Token']['fields']);
615
616		if (strpos($token, ':')) {
617			list($token, $locked) = explode(':', $token, 2);
618		}
619		unset($check['_Token']);
620
621		$locked = explode('|', $locked);
622
623		$lockedFields = array();
624		$fields = Set::flatten($check);
625		$fieldList = array_keys($fields);
626		$multi = array();
627
628		foreach ($fieldList as $i => $key) {
629			if (preg_match('/\.\d+$/', $key)) {
630				$multi[$i] = preg_replace('/\.\d+$/', '', $key);
631				unset($fieldList[$i]);
632			}
633		}
634		if (!empty($multi)) {
635			$fieldList += array_unique($multi);
636		}
637
638		foreach ($fieldList as $i => $key) {
639			$isDisabled = false;
640			$isLocked = (is_array($locked) && in_array($key, $locked));
641
642			if (!empty($this->disabledFields)) {
643				foreach ((array)$this->disabledFields as $disabled) {
644					$disabled = explode('.', $disabled);
645					$field = array_values(array_intersect(explode('.', $key), $disabled));
646					$isDisabled = ($field === $disabled);
647					if ($isDisabled) {
648						break;
649					}
650				}
651			}
652
653			if ($isDisabled || $isLocked) {
654				unset($fieldList[$i]);
655				if ($isLocked) {
656					$lockedFields[$key] = $fields[$key];
657				}
658			}
659		}
660		sort($fieldList, SORT_STRING);
661		ksort($lockedFields, SORT_STRING);
662
663		$fieldList += $lockedFields;
664		$check = Security::hash(serialize($fieldList) . Configure::read('Security.salt'));
665		return ($token === $check);
666	}
667
668/**
669 * Add authentication key for new form posts
670 *
671 * @param object $controller Instantiating controller
672 * @return bool Success
673 * @access protected
674 */
675	function _generateToken(&$controller) {
676		if (isset($controller->params['requested']) && $controller->params['requested'] === 1) {
677			if ($this->Session->check('_Token')) {
678				$tokenData = unserialize($this->Session->read('_Token'));
679				$controller->params['_Token'] = $tokenData;
680			}
681			return false;
682		}
683		$authKey = Security::generateAuthKey();
684		$expires = strtotime('+' . Security::inactiveMins() . ' minutes');
685		$token = array(
686			'key' => $authKey,
687			'expires' => $expires,
688			'allowedControllers' => $this->allowedControllers,
689			'allowedActions' => $this->allowedActions,
690			'disabledFields' => $this->disabledFields
691		);
692
693		if (!isset($controller->data)) {
694			$controller->data = array();
695		}
696
697		if ($this->Session->check('_Token')) {
698			$tokenData = unserialize($this->Session->read('_Token'));
699			$valid = (
700				isset($tokenData['expires']) &&
701				$tokenData['expires'] > time() &&
702				isset($tokenData['key'])
703			);
704
705			if ($valid) {
706				$token['key'] = $tokenData['key'];
707			}
708		}
709		$controller->params['_Token'] = $token;
710		$this->Session->write('_Token', serialize($token));
711		return true;
712	}
713
714/**
715 * Sets the default login options for an HTTP-authenticated request
716 *
717 * @param array $options Default login options
718 * @return void
719 * @access protected
720 */
721	function _setLoginDefaults(&$options) {
722		$options = array_merge(array(
723			'type' => 'basic',
724			'realm' => env('SERVER_NAME'),
725			'qop' => 'auth',
726			'nonce' => String::uuid()
727		), array_filter($options));
728		$options = array_merge(array('opaque' => md5($options['realm'])), $options);
729	}
730
731/**
732 * Calls a controller callback method
733 *
734 * @param object $controller Controller to run callback on
735 * @param string $method Method to execute
736 * @param array $params Parameters to send to method
737 * @return mixed Controller callback method's response
738 * @access protected
739 */
740	function _callback(&$controller, $method, $params = array()) {
741		if (is_callable(array($controller, $method))) {
742			return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
743		} else {
744			return null;
745		}
746	}
747}