[DOCS] fixed code comment in one programlisting
[zend/radio.git] / library / Zend / OpenId.php
blob51032872aa8af7792254cf2fd9aef1ad79fc05d7
1 <?php
3 /**
4 * Zend Framework
6 * LICENSE
8 * This source file is subject to the new BSD license that is bundled
9 * with this package in the file LICENSE.txt.
10 * It is also available through the world-wide-web at this URL:
11 * http://framework.zend.com/license/new-bsd
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@zend.com so we can send you a copy immediately.
16 * @category Zend
17 * @package Zend_OpenId
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
19 * @license http://framework.zend.com/license/new-bsd New BSD License
20 * @version $Id$
23 /**
24 * @see Zend_Controller_Response_Abstract
26 require_once "Zend/Controller/Response/Abstract.php";
28 /**
29 * Static class that contains common utility functions for
30 * {@link Zend_OpenId_Consumer} and {@link Zend_OpenId_Provider}.
32 * This class implements common utility functions that are used by both
33 * Consumer and Provider. They include functions for Diffie-Hellman keys
34 * generation and exchange, URL normalization, HTTP redirection and some others.
36 * @category Zend
37 * @package Zend_OpenId
38 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
39 * @license http://framework.zend.com/license/new-bsd New BSD License
41 class Zend_OpenId
43 /**
44 * Default Diffie-Hellman key generator (1024 bit)
46 const DH_P = 'dcf93a0b883972ec0e19989ac5a2ce310e1d37717e8d9571bb7623731866e61ef75a2e27898b057f9891c2e27a639c3f29b60814581cd3b2ca3986d2683705577d45c2e7e52dc81c7a171876e5cea74b1448bfdfaf18828efd2519f14e45e3826634af1949e5b535cc829a483b8a76223e5d490a257f05bdff16f2fb22c583ab';
48 /**
49 * Default Diffie-Hellman prime number (should be 2 or 5)
51 const DH_G = '02';
53 /**
54 * OpenID 2.0 namespace. All OpenID 2.0 messages MUST contain variable
55 * openid.ns with its value.
57 const NS_2_0 = 'http://specs.openid.net/auth/2.0';
59 /**
60 * Allows enable/disable stoping execution of PHP script after redirect()
62 static public $exitOnRedirect = true;
64 /**
65 * Alternative request URL that can be used to override the default
66 * selfUrl() response
68 static public $selfUrl = null;
70 /**
71 * Sets alternative request URL that can be used to override the default
72 * selfUrl() response
74 * @param string $selfUrl the URL to be set
75 * @return string the old value of overriding URL
77 static public function setSelfUrl($selfUrl = null)
79 $ret = self::$selfUrl;
80 self::$selfUrl = $selfUrl;
81 return $ret;
84 /**
85 * Returns a full URL that was requested on current HTTP request.
87 * @return string
89 static public function selfUrl()
91 if (self::$selfUrl !== null) {
92 return self::$selfUrl;
93 } if (isset($_SERVER['SCRIPT_URI'])) {
94 return $_SERVER['SCRIPT_URI'];
96 $url = '';
97 $port = '';
98 if (isset($_SERVER['HTTP_HOST'])) {
99 if (($pos = strpos($_SERVER['HTTP_HOST'], ':')) === false) {
100 if (isset($_SERVER['SERVER_PORT'])) {
101 $port = ':' . $_SERVER['SERVER_PORT'];
103 $url = $_SERVER['HTTP_HOST'];
104 } else {
105 $url = substr($_SERVER['HTTP_HOST'], 0, $pos);
106 $port = substr($_SERVER['HTTP_HOST'], $pos);
108 } else if (isset($_SERVER['SERVER_NAME'])) {
109 $url = $_SERVER['SERVER_NAME'];
110 if (isset($_SERVER['SERVER_PORT'])) {
111 $port = ':' . $_SERVER['SERVER_PORT'];
114 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
115 $url = 'https://' . $url;
116 if ($port == ':443') {
117 $port = '';
119 } else {
120 $url = 'http://' . $url;
121 if ($port == ':80') {
122 $port = '';
126 $url .= $port;
127 if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
128 $url .= $_SERVER['HTTP_X_REWRITE_URL'];
129 } elseif (isset($_SERVER['REQUEST_URI'])) {
130 $query = strpos($_SERVER['REQUEST_URI'], '?');
131 if ($query === false) {
132 $url .= $_SERVER['REQUEST_URI'];
133 } else {
134 $url .= substr($_SERVER['REQUEST_URI'], 0, $query);
136 } else if (isset($_SERVER['SCRIPT_URL'])) {
137 $url .= $_SERVER['SCRIPT_URL'];
138 } else if (isset($_SERVER['REDIRECT_URL'])) {
139 $url .= $_SERVER['REDIRECT_URL'];
140 } else if (isset($_SERVER['PHP_SELF'])) {
141 $url .= $_SERVER['PHP_SELF'];
142 } else if (isset($_SERVER['SCRIPT_NAME'])) {
143 $url .= $_SERVER['SCRIPT_NAME'];
144 if (isset($_SERVER['PATH_INFO'])) {
145 $url .= $_SERVER['PATH_INFO'];
148 return $url;
152 * Returns an absolute URL for the given one
154 * @param string $url absilute or relative URL
155 * @return string
157 static public function absoluteUrl($url)
159 if (empty($url)) {
160 return Zend_OpenId::selfUrl();
161 } else if (!preg_match('|^([^:]+)://|', $url)) {
162 if (preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?]*)?((?:[?](?:[^#]*))?(?:#.*)?)$|', Zend_OpenId::selfUrl(), $reg)) {
163 $scheme = $reg[1];
164 $auth = $reg[2];
165 $host = $reg[3];
166 $port = $reg[4];
167 $path = $reg[5];
168 $query = $reg[6];
169 if ($url[0] == '/') {
170 return $scheme
171 . '://'
172 . $auth
173 . $host
174 . (empty($port) ? '' : (':' . $port))
175 . $url;
176 } else {
177 $dir = dirname($path);
178 return $scheme
179 . '://'
180 . $auth
181 . $host
182 . (empty($port) ? '' : (':' . $port))
183 . (strlen($dir) > 1 ? $dir : '')
184 . '/'
185 . $url;
189 return $url;
193 * Converts variable/value pairs into URL encoded query string
195 * @param array $params variable/value pairs
196 * @return string URL encoded query string
198 static public function paramsToQuery($params)
200 foreach($params as $key => $value) {
201 if (isset($query)) {
202 $query .= '&' . $key . '=' . urlencode($value);
203 } else {
204 $query = $key . '=' . urlencode($value);
207 return isset($query) ? $query : '';
211 * Normalizes URL according to RFC 3986 to use it in comparison operations.
212 * The function gets URL argument by reference and modifies it.
213 * It returns true on success and false of failure.
215 * @param string &$id url to be normalized
216 * @return bool
218 static public function normalizeUrl(&$id)
220 // RFC 3986, 6.2.2. Syntax-Based Normalization
222 // RFC 3986, 6.2.2.2 Percent-Encoding Normalization
223 $i = 0;
224 $n = strlen($id);
225 $res = '';
226 while ($i < $n) {
227 if ($id[$i] == '%') {
228 if ($i + 2 >= $n) {
229 return false;
231 ++$i;
232 if ($id[$i] >= '0' && $id[$i] <= '9') {
233 $c = ord($id[$i]) - ord('0');
234 } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
235 $c = ord($id[$i]) - ord('A') + 10;
236 } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
237 $c = ord($id[$i]) - ord('a') + 10;
238 } else {
239 return false;
241 ++$i;
242 if ($id[$i] >= '0' && $id[$i] <= '9') {
243 $c = ($c << 4) | (ord($id[$i]) - ord('0'));
244 } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
245 $c = ($c << 4) | (ord($id[$i]) - ord('A') + 10);
246 } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
247 $c = ($c << 4) | (ord($id[$i]) - ord('a') + 10);
248 } else {
249 return false;
251 ++$i;
252 $ch = chr($c);
253 if (($ch >= 'A' && $ch <= 'Z') ||
254 ($ch >= 'a' && $ch <= 'z') ||
255 $ch == '-' ||
256 $ch == '.' ||
257 $ch == '_' ||
258 $ch == '~') {
259 $res .= $ch;
260 } else {
261 $res .= '%';
262 if (($c >> 4) < 10) {
263 $res .= chr(($c >> 4) + ord('0'));
264 } else {
265 $res .= chr(($c >> 4) - 10 + ord('A'));
267 $c = $c & 0xf;
268 if ($c < 10) {
269 $res .= chr($c + ord('0'));
270 } else {
271 $res .= chr($c - 10 + ord('A'));
274 } else {
275 $res .= $id[$i++];
279 if (!preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?#]*)?((?:[?](?:[^#]*))?)((?:#.*)?)$|', $res, $reg)) {
280 return false;
282 $scheme = $reg[1];
283 $auth = $reg[2];
284 $host = $reg[3];
285 $port = $reg[4];
286 $path = $reg[5];
287 $query = $reg[6];
288 $fragment = $reg[7]; /* strip it */
290 if (empty($scheme) || empty($host)) {
291 return false;
294 // RFC 3986, 6.2.2.1. Case Normalization
295 $scheme = strtolower($scheme);
296 $host = strtolower($host);
298 // RFC 3986, 6.2.2.3. Path Segment Normalization
299 if (!empty($path)) {
300 $i = 0;
301 $n = strlen($path);
302 $res = "";
303 while ($i < $n) {
304 if ($path[$i] == '/') {
305 ++$i;
306 while ($i < $n && $path[$i] == '/') {
307 ++$i;
309 if ($i < $n && $path[$i] == '.') {
310 ++$i;
311 if ($i < $n && $path[$i] == '.') {
312 ++$i;
313 if ($i == $n || $path[$i] == '/') {
314 if (($pos = strrpos($res, '/')) !== false) {
315 $res = substr($res, 0, $pos);
317 } else {
318 $res .= '/..';
320 } else if ($i != $n && $path[$i] != '/') {
321 $res .= '/.';
323 } else {
324 $res .= '/';
326 } else {
327 $res .= $path[$i++];
330 $path = $res;
333 // RFC 3986,6.2.3. Scheme-Based Normalization
334 if ($scheme == 'http') {
335 if ($port == 80) {
336 $port = '';
338 } else if ($scheme == 'https') {
339 if ($port == 443) {
340 $port = '';
343 if (empty($path)) {
344 $path = '/';
347 $id = $scheme
348 . '://'
349 . $auth
350 . $host
351 . (empty($port) ? '' : (':' . $port))
352 . $path
353 . $query;
354 return true;
358 * Normalizes OpenID identifier that can be URL or XRI name.
359 * Returns true on success and false of failure.
361 * Normalization is performed according to the following rules:
362 * 1. If the user's input starts with one of the "xri://", "xri://$ip*",
363 * or "xri://$dns*" prefixes, they MUST be stripped off, so that XRIs
364 * are used in the canonical form, and URI-authority XRIs are further
365 * considered URL identifiers.
366 * 2. If the first character of the resulting string is an XRI Global
367 * Context Symbol ("=", "@", "+", "$", "!"), then the input SHOULD be
368 * treated as an XRI.
369 * 3. Otherwise, the input SHOULD be treated as an http URL; if it does
370 * not include a "http" or "https" scheme, the Identifier MUST be
371 * prefixed with the string "http://".
372 * 4. URL identifiers MUST then be further normalized by both following
373 * redirects when retrieving their content and finally applying the
374 * rules in Section 6 of [RFC3986] to the final destination URL.
375 * @param string &$id identifier to be normalized
376 * @return bool
378 static public function normalize(&$id)
380 $id = trim($id);
381 if (strlen($id) === 0) {
382 return true;
385 // 7.2.1
386 if (strpos($id, 'xri://$ip*') === 0) {
387 $id = substr($id, strlen('xri://$ip*'));
388 } else if (strpos($id, 'xri://$dns*') === 0) {
389 $id = substr($id, strlen('xri://$dns*'));
390 } else if (strpos($id, 'xri://') === 0) {
391 $id = substr($id, strlen('xri://'));
394 // 7.2.2
395 if ($id[0] == '=' ||
396 $id[0] == '@' ||
397 $id[0] == '+' ||
398 $id[0] == '$' ||
399 $id[0] == '!') {
400 return true;
403 // 7.2.3
404 if (strpos($id, "://") === false) {
405 $id = 'http://' . $id;
408 // 7.2.4
409 return self::normalizeURL($id);
413 * Performs a HTTP redirection to specified URL with additional data.
414 * It may generate redirected request using GET or POST HTTP method.
415 * The function never returns.
417 * @param string $url URL to redirect to
418 * @param array $params additional variable/value pairs to send
419 * @param Zend_Controller_Response_Abstract $response
420 * @param string $method redirection method ('GET' or 'POST')
422 static public function redirect($url, $params = null,
423 Zend_Controller_Response_Abstract $response = null, $method = 'GET')
425 $url = Zend_OpenId::absoluteUrl($url);
426 $body = "";
427 if (null === $response) {
428 require_once "Zend/Controller/Response/Http.php";
429 $response = new Zend_Controller_Response_Http();
432 if ($method == 'POST') {
433 $body = "<html><body onLoad=\"document.forms[0].submit();\">\n";
434 $body .= "<form method=\"POST\" action=\"$url\">\n";
435 if (is_array($params) && count($params) > 0) {
436 foreach($params as $key => $value) {
437 $body .= '<input type="hidden" name="' . $key . '" value="' . $value . "\">\n";
440 $body .= "<input type=\"submit\" value=\"Continue OpenID transaction\">\n";
441 $body .= "</form></body></html>\n";
442 } else if (is_array($params) && count($params) > 0) {
443 if (strpos($url, '?') === false) {
444 $url .= '?' . self::paramsToQuery($params);
445 } else {
446 $url .= '&' . self::paramsToQuery($params);
449 if (!empty($body)) {
450 $response->setBody($body);
451 } else if (!$response->canSendHeaders()) {
452 $response->setBody("<script language=\"JavaScript\"" .
453 " type=\"text/javascript\">window.location='$url';" .
454 "</script>");
455 } else {
456 $response->setRedirect($url);
458 $response->sendResponse();
459 if (self::$exitOnRedirect) {
460 exit();
465 * Produces string of random byte of given length.
467 * @param integer $len length of requested string
468 * @return string RAW random binary string
470 static public function randomBytes($len)
472 $key = '';
473 for($i=0; $i < $len; $i++) {
474 $key .= chr(mt_rand(0, 255));
476 return $key;
480 * Generates a hash value (message digest) according to given algorithm.
481 * It returns RAW binary string.
483 * This is a wrapper function that uses one of available internal function
484 * dependent on given PHP configuration. It may use various functions from
485 * ext/openssl, ext/hash, ext/mhash or ext/standard.
487 * @param string $func digest algorithm
488 * @param string $data data to sign
489 * @return string RAW digital signature
490 * @throws Zend_OpenId_Exception
492 static public function digest($func, $data)
494 if (function_exists('openssl_digest')) {
495 return openssl_digest($data, $func, true);
496 } else if (function_exists('hash')) {
497 return hash($func, $data, true);
498 } else if ($func === 'sha1') {
499 return sha1($data, true);
500 } else if ($func === 'sha256') {
501 if (function_exists('mhash')) {
502 return mhash(MHASH_SHA256 , $data);
505 require_once "Zend/OpenId/Exception.php";
506 throw new Zend_OpenId_Exception(
507 'Unsupported digest algorithm "' . $func . '".',
508 Zend_OpenId_Exception::UNSUPPORTED_DIGEST);
512 * Generates a keyed hash value using the HMAC method. It uses ext/hash
513 * if available or user-level PHP implementation, that is not significantly
514 * slower.
516 * @param string $macFunc name of selected hashing algorithm (sha1, sha256)
517 * @param string $data data to sign
518 * @param string $secret shared secret key used for generating the HMAC
519 * variant of the message digest
520 * @return string RAW HMAC value
522 static public function hashHmac($macFunc, $data, $secret)
524 // require_once "Zend/Crypt/Hmac.php";
525 // return Zend_Crypt_Hmac::compute($secret, $macFunc, $data, Zend_Crypt_Hmac::BINARY);
526 if (function_exists('hash_hmac')) {
527 return hash_hmac($macFunc, $data, $secret, 1);
528 } else {
529 if (Zend_OpenId::strlen($secret) > 64) {
530 $secret = self::digest($macFunc, $secret);
532 $secret = str_pad($secret, 64, chr(0x00));
533 $ipad = str_repeat(chr(0x36), 64);
534 $opad = str_repeat(chr(0x5c), 64);
535 $hash1 = self::digest($macFunc, ($secret ^ $ipad) . $data);
536 return self::digest($macFunc, ($secret ^ $opad) . $hash1);
541 * Converts binary representation into ext/gmp or ext/bcmath big integer
542 * representation.
544 * @param string $bin binary representation of big number
545 * @return mixed
546 * @throws Zend_OpenId_Exception
548 static protected function binToBigNum($bin)
550 if (extension_loaded('gmp')) {
551 return gmp_init(bin2hex($bin), 16);
552 } else if (extension_loaded('bcmath')) {
553 $bn = 0;
554 $len = Zend_OpenId::strlen($bin);
555 for ($i = 0; $i < $len; $i++) {
556 $bn = bcmul($bn, 256);
557 $bn = bcadd($bn, ord($bin[$i]));
559 return $bn;
561 require_once "Zend/OpenId/Exception.php";
562 throw new Zend_OpenId_Exception(
563 'The system doesn\'t have proper big integer extension',
564 Zend_OpenId_Exception::UNSUPPORTED_LONG_MATH);
568 * Converts internal ext/gmp or ext/bcmath big integer representation into
569 * binary string.
571 * @param mixed $bn big number
572 * @return string
573 * @throws Zend_OpenId_Exception
575 static protected function bigNumToBin($bn)
577 if (extension_loaded('gmp')) {
578 $s = gmp_strval($bn, 16);
579 if (strlen($s) % 2 != 0) {
580 $s = '0' . $s;
581 } else if ($s[0] > '7') {
582 $s = '00' . $s;
584 return pack("H*", $s);
585 } else if (extension_loaded('bcmath')) {
586 $cmp = bccomp($bn, 0);
587 if ($cmp == 0) {
588 return (chr(0));
589 } else if ($cmp < 0) {
590 require_once "Zend/OpenId/Exception.php";
591 throw new Zend_OpenId_Exception(
592 'Big integer arithmetic error',
593 Zend_OpenId_Exception::ERROR_LONG_MATH);
595 $bin = "";
596 while (bccomp($bn, 0) > 0) {
597 $bin = chr(bcmod($bn, 256)) . $bin;
598 $bn = bcdiv($bn, 256);
600 if (ord($bin[0]) > 127) {
601 $bin = chr(0) . $bin;
603 return $bin;
605 require_once "Zend/OpenId/Exception.php";
606 throw new Zend_OpenId_Exception(
607 'The system doesn\'t have proper big integer extension',
608 Zend_OpenId_Exception::UNSUPPORTED_LONG_MATH);
612 * Performs the first step of a Diffie-Hellman key exchange by generating
613 * private and public DH values based on given prime number $p and
614 * generator $g. Both sides of key exchange MUST have the same prime number
615 * and generator. In this case they will able to create a random shared
616 * secret that is never send from one to the other.
618 * @param string $p prime number in binary representation
619 * @param string $g generator in binary representation
620 * @param string $priv_key private key in binary representation
621 * @return mixed
623 static public function createDhKey($p, $g, $priv_key = null)
625 if (function_exists('openssl_dh_compute_key')) {
626 $dh_details = array(
627 'p' => $p,
628 'g' => $g
630 if ($priv_key !== null) {
631 $dh_details['priv_key'] = $priv_key;
633 return openssl_pkey_new(array('dh'=>$dh_details));
634 } else {
635 $bn_p = self::binToBigNum($p);
636 $bn_g = self::binToBigNum($g);
637 if ($priv_key === null) {
638 $priv_key = self::randomBytes(Zend_OpenId::strlen($p));
640 $bn_priv_key = self::binToBigNum($priv_key);
641 if (extension_loaded('gmp')) {
642 $bn_pub_key = gmp_powm($bn_g, $bn_priv_key, $bn_p);
643 } else if (extension_loaded('bcmath')) {
644 $bn_pub_key = bcpowmod($bn_g, $bn_priv_key, $bn_p);
646 $pub_key = self::bigNumToBin($bn_pub_key);
648 return array(
649 'p' => $bn_p,
650 'g' => $bn_g,
651 'priv_key' => $bn_priv_key,
652 'pub_key' => $bn_pub_key,
653 'details' => array(
654 'p' => $p,
655 'g' => $g,
656 'priv_key' => $priv_key,
657 'pub_key' => $pub_key));
662 * Returns an associative array with Diffie-Hellman key components in
663 * binary representation. The array includes original prime number 'p' and
664 * generator 'g', random private key 'priv_key' and corresponding public
665 * key 'pub_key'.
667 * @param mixed $dh Diffie-Hellman key
668 * @return array
670 static public function getDhKeyDetails($dh)
672 if (function_exists('openssl_dh_compute_key')) {
673 $details = openssl_pkey_get_details($dh);
674 if (isset($details['dh'])) {
675 return $details['dh'];
677 } else {
678 return $dh['details'];
683 * Computes the shared secret from the private DH value $dh and the other
684 * party's public value in $pub_key
686 * @param string $pub_key other party's public value
687 * @param mixed $dh Diffie-Hellman key
688 * @return string
689 * @throws Zend_OpenId_Exception
691 static public function computeDhSecret($pub_key, $dh)
693 if (function_exists('openssl_dh_compute_key')) {
694 $ret = openssl_dh_compute_key($pub_key, $dh);
695 if (ord($ret[0]) > 127) {
696 $ret = chr(0) . $ret;
698 return $ret;
699 } else if (extension_loaded('gmp')) {
700 $bn_pub_key = self::binToBigNum($pub_key);
701 $bn_secret = gmp_powm($bn_pub_key, $dh['priv_key'], $dh['p']);
702 return self::bigNumToBin($bn_secret);
703 } else if (extension_loaded('bcmath')) {
704 $bn_pub_key = self::binToBigNum($pub_key);
705 $bn_secret = bcpowmod($bn_pub_key, $dh['priv_key'], $dh['p']);
706 return self::bigNumToBin($bn_secret);
708 require_once "Zend/OpenId/Exception.php";
709 throw new Zend_OpenId_Exception(
710 'The system doesn\'t have proper big integer extension',
711 Zend_OpenId_Exception::UNSUPPORTED_LONG_MATH);
715 * Takes an arbitrary precision integer and returns its shortest big-endian
716 * two's complement representation.
718 * Arbitrary precision integers MUST be encoded as big-endian signed two's
719 * complement binary strings. Henceforth, "btwoc" is a function that takes
720 * an arbitrary precision integer and returns its shortest big-endian two's
721 * complement representation. All integers that are used with
722 * Diffie-Hellman Key Exchange are positive. This means that the left-most
723 * bit of the two's complement representation MUST be zero. If it is not,
724 * implementations MUST add a zero byte at the front of the string.
726 * @param string $str binary representation of arbitrary precision integer
727 * @return string big-endian signed representation
729 static public function btwoc($str)
731 if (ord($str[0]) > 127) {
732 return chr(0) . $str;
734 return $str;
738 * Returns lenght of binary string in bytes
740 * @param string $str
741 * @return int the string lenght
743 static public function strlen($str)
745 if (extension_loaded('mbstring') &&
746 (((int)ini_get('mbstring.func_overload')) & 2)) {
747 return mb_strlen($str, 'latin1');
748 } else {
749 return strlen($str);