1<?php
2//
3// +----------------------------------------------------------------------+
4// | PHP Version 5                                                        |
5// +----------------------------------------------------------------------+
6// | Copyright (c) 1997-2012 The PHP Group                                |
7// +----------------------------------------------------------------------+
8// | This source file is subject to version 3.01 of the PHP license,      |
9// | that is bundled with this package in the file LICENSE, and is        |
10// | available at through the world-wide-web at                           |
11// | http://www.php.net/license/3_01.txt.                                 |
12// | If you did not receive a copy of the PHP license and are unable to   |
13// | obtain it through the world-wide-web, please send a note to          |
14// | license@php.net so we can mail you a copy immediately.               |
15// +----------------------------------------------------------------------+
16// | Authors: Martin Jansen <mj@php.net>                                  |
17// |          Rui Hirokawa <hirokawa@php.net>                             |
18// |          David Costa  <gurugeek@php.net>                             |
19// +----------------------------------------------------------------------+
20//
21//  $Id$
22//
23
24require_once "Auth/Auth.php";
25
26define('AUTH_HTTP_NONCE_TIME_LEN', 16);
27define('AUTH_HTTP_NONCE_HASH_LEN', 32);
28
29// {{{ class Auth_HTTP
30
31/**
32 * PEAR::Auth_HTTP
33 *
34 * The PEAR::Auth_HTTP class provides methods for creating an
35 * HTTP authentication system based on RFC-2617 using PHP.
36 *
37 * Instead of generating an HTML driven form like PEAR::Auth
38 * does, this class sends header commands to the clients which
39 * cause them to present a login box like they are e.g. used
40 * in Apache's .htaccess mechanism.
41 *
42 * This class requires the PEAR::Auth package.
43 *
44 * @notes The HTTP Digest Authentication part is based on
45 *  authentication class written by Tom Pike <tom.pike@xiven.com>
46 *
47 * @author  Martin Jansen <mj@php.net>
48 * @author  Rui Hirokawa <hirokawa@php.net>
49 * @author  David Costa <gurugeek@php.net>
50 * @package Auth_HTTP
51 * @extends Auth
52 * @version $Revision$
53 */
54class Auth_HTTP extends Auth
55{
56
57    // {{{ properties
58
59    /**
60     * Authorization method: 'basic' or 'digest'
61     *
62     * @access public
63     * @var    string
64     */
65    var $authType = 'basic';
66
67    /**
68     * Name of the realm for Basic Authentication
69     *
70     * @access public
71     * @var    string
72     * @see    drawLogin()
73     */
74    var $realm = "protected area";
75
76    /**
77     * Text to send if user hits cancel button
78     *
79     * @access public
80     * @var    string
81     * @see    drawLogin()
82     */
83    var $CancelText = "Error 401 - Access denied";
84
85    /**
86     * option array
87     *
88     * @access public
89     * @var    array
90     */
91    var $options = array();
92
93    /**
94     * flag to indicate the nonce was stale.
95     *
96     * @access public
97     * @var    bool
98     */
99    var $stale = false;
100
101    /**
102     * opaque string for digest authentication
103     *
104     * @access public
105     * @var    string
106     */
107    var $opaque = 'dummy';
108
109    /**
110     * digest URI
111     *
112     * @access public
113     * @var    string
114     */
115    var $uri = '';
116
117    /**
118     * authorization info returned by the client
119     *
120     * @access public
121     * @var    array
122     */
123    var $auth = array();
124
125    /**
126     * next nonce value
127     *
128     * @access public
129     * @var    string
130     */
131    var $nextNonce = '';
132
133    /**
134     * nonce value
135     *
136     * @access public
137     * @var    string
138     */
139    var $nonce = '';
140
141    /**
142     * Holds a reference to the global server variable
143     * @var array
144     */
145    var $server;
146
147    /**
148     * Holds a reference to the global post variable
149     * @var array
150     */
151    var $post;
152
153    /**
154     * Holds a reference to the global cookie variable
155     * @var array
156     */
157    var $cookie;
158
159
160    // }}}
161    // {{{ Constructor
162
163    /**
164     * Constructor
165     *
166     * @param string    Type of the storage driver
167     * @param mixed     Additional options for the storage driver
168     *                  (example: if you are using DB as the storage
169     *                   driver, you have to pass the dsn string here)
170     *
171     * @return void
172     */
173    function Auth_HTTP($storageDriver, $options = '')
174    {
175        /* set default values for options */
176        $this->options = array('cryptType' => 'md5',
177                               'algorithm' => 'MD5',
178                               'qop' => 'auth-int,auth',
179                               'opaquekey' => 'moo',
180                               'noncekey' => 'moo',
181                               'digestRealm' => 'protected area',
182                               'forceDigestOnly' => false,
183                               'nonceLife' => 300,
184                               'sessionSharing' => false,
185                               );
186
187        if (!empty($options['authType'])) {
188            $this->authType = strtolower($options['authType']);
189        }
190
191        if (is_array($options)) {
192            foreach($options as $key => $value) {
193                if (array_key_exists( $key, $this->options)) {
194                    $this->options[$key] = $value;
195                }
196            }
197
198            if (!empty($this->options['opaquekey'])) {
199                $this->opaque = md5($this->options['opaquekey']);
200            }
201        }
202
203		$this->Auth($storageDriver, $options);
204	}
205
206	// }}}
207    // {{{ assignData()
208
209    /**
210     * Assign values from $PHP_AUTH_USER and $PHP_AUTH_PW or 'Authorization' header
211     * to internal variables and sets the session id based
212     * on them
213     *
214     * @access public
215     * @return void
216     */
217    function assignData()
218    {
219        if (method_exists($this, '_importGlobalVariable')) {
220            $this->server = &$this->_importGlobalVariable('server');
221        }
222
223
224        if ($this->authType == 'basic') {
225            if (!empty($this->server['PHP_AUTH_USER'])) {
226                $this->username = $this->server['PHP_AUTH_USER'];
227            }
228
229            if (!empty($this->server['PHP_AUTH_PW'])) {
230                $this->password = $this->server['PHP_AUTH_PW'];
231            }
232
233            /**
234             * Try to get authentication information from IIS
235             */
236            if  (empty($this->username) && empty($this->password)) {
237                if (!empty($this->server['HTTP_AUTHORIZATION'])) {
238                    list($this->username, $this->password) =
239                        explode(':', base64_decode(substr($this->server['HTTP_AUTHORIZATION'], 6)));
240                }
241            }
242        } elseif ($this->authType == 'digest') {
243            $this->username = '';
244            $this->password = '';
245
246            $this->digest_header = null;
247            if (!empty($this->server['PHP_AUTH_DIGEST'])) {
248                $this->digest_header = $this->server['PHP_AUTH_DIGEST'];
249                $headers = getallheaders();
250            } else {
251                $headers = getallheaders();
252                if(isset($headers['Authorization']) && !empty($headers['Authorization'])) {
253                    $this->digest_header = substr($headers['Authorization'],
254                                                  strpos($headers['Authorization'],' ')+1);
255                }
256            }
257
258            if($this->digest_header) {
259                $authtemp = explode(',', $this->digest_header);
260                $auth = array();
261                foreach($authtemp as $key => $value) {
262                    $value = trim($value);
263                    if(strpos($value,'=') !== false) {
264                        $lhs = substr($value,0,strpos($value,'='));
265                        $rhs = substr($value,strpos($value,'=')+1);
266                        if(substr($rhs,0,1) == '"' && substr($rhs,-1,1) == '"') {
267                            $rhs = substr($rhs,1,-1);
268                        }
269                        $auth[$lhs] = $rhs;
270                    }
271                }
272            }
273            if (!isset($auth['uri']) || !isset($auth['realm'])) {
274                return;
275            }
276
277            if ($this->selfURI() == $auth['uri']) {
278                $this->uri = $auth['uri'];
279                if (substr($headers['Authorization'],0,7) == 'Digest ') {
280
281                    $this->authType = 'digest';
282
283                    if (!isset($auth['nonce']) || !isset($auth['username']) ||
284                  !isset($auth['response']) || !isset($auth['qop']) ||
285                  !isset($auth['nc']) || !isset($auth['cnonce'])){
286                        return;
287                    }
288
289               if ($auth['qop'] != 'auth' && $auth['qop'] != 'auth-int') {
290                        return;
291               }
292
293                    $this->stale = $this->_judgeStale($auth['nonce']);
294
295               if ($this->nextNonce == false) {
296                  return;
297               }
298
299                    $this->username = $auth['username'];
300                    $this->password = $auth['response'];
301                    $this->auth['nonce'] = $auth['nonce'];
302
303               $this->auth['qop'] = $auth['qop'];
304               $this->auth['nc'] = $auth['nc'];
305               $this->auth['cnonce'] = $auth['cnonce'];
306
307                    if (isset($auth['opaque'])) {
308                        $this->auth['opaque'] = $auth['opaque'];
309                    }
310
311                } elseif (substr($headers['Authorization'],0,6) == 'Basic ') {
312                    if ($this->options['forceDigestOnly']) {
313                        return; // Basic authentication is not allowed.
314                    }
315
316                    $this->authType = 'basic';
317                    list($username, $password) =
318                        explode(':',base64_decode(substr($headers['Authorization'],6)));
319                    $this->username = $username;
320                    $this->password = $password;
321                }
322            }
323        } else {
324            include_once 'PEAR.php';
325            return PEAR::throwError('authType is invalid.');
326        }
327
328        if ($this->options['sessionSharing'] &&
329            isset($this->username) && isset($this->password)) {
330            session_id(md5('Auth_HTTP' . $this->username . $this->password));
331        }
332
333        /**
334         * set sessionName for AUTH, so that the sessionName is different
335         * for distinct realms
336         */
337         $this->_sessionName = "_authhttp".md5($this->realm);
338    }
339
340    // }}}
341    // {{{ login()
342
343    /**
344     * Login function
345     *
346     * @access private
347     * @return void
348     */
349    function login()
350    {
351        $login_ok = false;
352        if (method_exists($this, '_loadStorage')) {
353            $this->_loadStorage();
354        }
355        $this->storage->_auth_obj->_sessionName =& $this->_sessionName;
356
357        /**
358         * When the user has already entered a username,
359         * we have to validate it.
360         */
361        if (!empty($this->username) && !empty($this->password)) {
362            if ($this->authType == 'basic' && !$this->options['forceDigestOnly']) {
363                if (true === $this->storage->fetchData($this->username, $this->password)) {
364                    $login_ok = true;
365                }
366            } else { /* digest authentication */
367
368                if (!$this->getAuth() || $this->getAuthData('a1') == null) {
369                    /*
370                     * note:
371                     *  - only PEAR::DB is supported as container.
372                     *  - password should be stored in container as plain-text
373                     *    (if $options['cryptType'] == 'none') or
374                     *     A1 hashed form (md5('username:realm:password'))
375                     *    (if $options['cryptType'] == 'md5')
376                     */
377                    $dbs = $this->storage;
378                    if (!DB::isConnection($dbs->db)) {
379                        $dbs->_connect($dbs->options['dsn']);
380                    }
381
382                    $query = 'SELECT '.$dbs->options['passwordcol']." FROM ".$dbs->options['table'].
383                        ' WHERE '.$dbs->options['usernamecol']." = '".
384                        $dbs->db->quoteString($this->username)."' ";
385
386                    $pwd = $dbs->db->getOne($query); // password stored in container.
387
388                    if (DB::isError($pwd)) {
389                        include_once 'PEAR.php';
390                        return PEAR::throwError($pwd->getMessage(), $pwd->getCode());
391                    }
392
393                    if ($this->options['cryptType'] == 'none') {
394                        $a1 = md5($this->username.':'.$this->options['digestRealm'].':'.$pwd);
395                    } else {
396                        $a1 = $pwd;
397                    }
398
399                    $this->setAuthData('a1', $a1, true);
400                } else {
401                    $a1 = $this->getAuthData('a1');
402                }
403
404                $login_ok = $this->validateDigest($this->password, $a1);
405                if ($this->nextNonce == false) {
406                    $login_ok = false;
407                }
408            }
409
410            if (!$login_ok && is_callable($this->loginFailedCallback)) {
411                call_user_func($this->loginFailedCallback,$this->username, $this);
412            }
413        }
414
415        if (!empty($this->username) && $login_ok) {
416            $this->setAuth($this->username);
417            if (is_callable($this->loginCallback)) {
418                call_user_func($this->loginCallback,$this->username, $this);
419            }
420        }
421
422        /**
423         * If the login failed or the user entered no username,
424         * output the login screen again.
425         */
426        if (!empty($this->username) && !$login_ok) {
427            $this->status = AUTH_WRONG_LOGIN;
428        }
429
430        if ((empty($this->username) || !$login_ok) && $this->showLogin) {
431            $this->drawLogin($this->storage->activeUser);
432            return;
433        }
434
435      if (!empty($this->username) && $login_ok && $this->authType == 'digest'
436         && $this->auth['qop'] == 'auth') {
437         $this->authenticationInfo();
438      }
439    }
440
441    // }}}
442    // {{{ drawLogin()
443
444    /**
445     * Launch the login box
446     *
447     * @param  string $username  Username
448     * @return void
449     * @access private
450     */
451    function drawLogin($username = "")
452    {
453        /**
454         * Send the header commands
455         */
456        if ($this->authType == 'basic') {
457            header("WWW-Authenticate: Basic realm=\"".$this->realm."\"");
458            header('HTTP/1.0 401 Unauthorized');
459        } else if ($this->authType == 'digest') {
460            $this->nonce = $this->_getNonce();
461
462            $wwwauth = 'WWW-Authenticate: Digest ';
463            $wwwauth .= 'qop="'.$this->options['qop'].'", ';
464            $wwwauth .= 'algorithm='.$this->options['algorithm'].', ';
465            $wwwauth .= 'realm="'.$this->options['digestRealm'].'", ';
466            $wwwauth .= 'nonce="'.$this->nonce.'", ';
467            if ($this->stale) {
468                $wwwauth .= 'stale=true, ';
469            }
470            if (!empty($this->opaque)) {
471                $wwwauth .= 'opaque="'.$this->opaque.'"' ;
472            }
473            $wwwauth .= "\r\n";
474            if (!$this->options['forceDigestOnly']) {
475                $wwwauth .= 'WWW-Authenticate: Basic realm="'.$this->realm.'"';
476            }
477            header($wwwauth);
478            header('HTTP/1.0 401 Unauthorized');
479        }
480
481        /**
482         * This code is only executed if the user hits the cancel
483         * button or if he enters wrong data 3 times.
484         */
485        if ($this->stale) {
486            echo 'Stale nonce value, please re-authenticate.';
487        } else {
488            echo $this->CancelText;
489        }
490        exit;
491    }
492
493    // }}}
494    // {{{ setRealm()
495
496    /**
497     * Set name of the current realm
498     *
499     * @access public
500     * @param  string $realm  Name of the realm
501     * @param  string $digestRealm  Name of the realm for digest authentication
502     * @return void
503     */
504    function setRealm($realm, $digestRealm = '')
505    {
506        $this->realm = $realm;
507        if (!empty($digestRealm)) {
508            $this->options['digestRealm'] = $digestRealm;
509        }
510    }
511
512    // }}}
513    // {{{ setCancelText()
514
515    /**
516     * Set the text to send if user hits the cancel button
517     *
518     * @access public
519     * @param  string $text  Text to send
520     * @return void
521     */
522    function setCancelText($text)
523    {
524        $this->CancelText = $text;
525    }
526
527    // }}}
528    // {{{ validateDigest()
529
530    /**
531     * judge if the client response is valid.
532     *
533     * @access private
534     * @param  string $response  client response
535     * @param  string $a1 password or hashed password stored in container
536     * @return bool true if success, false otherwise
537     */
538    function validateDigest($response, $a1)
539    {
540        if (method_exists($this, '_importGlobalVariable')) {
541            $this->server = &$this->_importGlobalVariable('server');
542        }
543
544        $a2unhashed = $this->server['REQUEST_METHOD'].":".$this->selfURI();
545        if($this->auth['qop'] == 'auth-int') {
546            if(isset($GLOBALS["HTTP_RAW_POST_DATA"])) {
547                // In PHP < 4.3 get raw POST data from this variable
548                $body = $GLOBALS["HTTP_RAW_POST_DATA"];
549            } else if($lines = @file('php://input')) {
550                // In PHP >= 4.3 get raw POST data from this file
551                $body = implode("\n", $lines);
552            } else {
553                if (method_exists($this, '_importGlobalVariable')) {
554                    $this->post = &$this->_importGlobalVariable('post');
555                }
556                $body = '';
557                foreach($this->post as $key => $value) {
558                    if($body != '') $body .= '&';
559                    $body .= rawurlencode($key) . '=' . rawurlencode($value);
560                }
561            }
562
563            $a2unhashed .= ':'.md5($body);
564        }
565
566        $a2 = md5($a2unhashed);
567        $combined = $a1.':'.
568            $this->auth['nonce'].':'.
569            $this->auth['nc'].':'.
570            $this->auth['cnonce'].':'.
571            $this->auth['qop'].':'.
572            $a2;
573        $expectedResponse = md5($combined);
574
575        if(!isset($this->auth['opaque']) || $this->auth['opaque'] == $this->opaque) {
576            if($response == $expectedResponse) { // password is valid
577                if(!$this->stale) {
578                    return true;
579                } else {
580                    $this->drawLogin();
581                }
582            }
583        }
584
585        return false;
586    }
587
588    // }}}
589    // {{{ _judgeStale()
590
591    /**
592     * judge if nonce from client is stale.
593     *
594     * @access private
595     * @param  string $nonce  nonce value from client
596     * @return bool stale
597     */
598    function _judgeStale($nonce)
599    {
600        $stale = false;
601
602        if(!$this->_decodeNonce($nonce, $time, $hash_cli)) {
603         $this->nextNonce = false;
604         $stale = true;
605            return $stale;
606        }
607
608        if ($time < time() - $this->options['nonceLife']) {
609         $this->nextNonce = $this->_getNonce();
610            $stale = true;
611        } else {
612         $this->nextNonce = $nonce;
613      }
614
615        return $stale;
616    }
617
618    // }}}
619    // {{{ _nonceDecode()
620
621    /**
622     * decode nonce string
623     *
624     * @access private
625     * @param  string $nonce nonce value from client
626     * @param  string $time decoded time
627     * @param  string $hash decoded hash
628     * @return bool false if nonce is invalid
629     */
630    function _decodeNonce($nonce, &$time, &$hash)
631    {
632        if (method_exists($this, '_importGlobalVariable')) {
633            $this->server = &$this->_importGlobalVariable('server');
634        }
635
636        if (strlen($nonce) != AUTH_HTTP_NONCE_TIME_LEN + AUTH_HTTP_NONCE_HASH_LEN) {
637            return false;
638        }
639
640        $time =  base64_decode(substr($nonce, 0, AUTH_HTTP_NONCE_TIME_LEN));
641        $hash_cli = substr($nonce, AUTH_HTTP_NONCE_TIME_LEN, AUTH_HTTP_NONCE_HASH_LEN);
642
643        $hash = md5($time . $this->server['HTTP_USER_AGENT'] . $this->options['noncekey']);
644
645        if ($hash_cli != $hash) {
646            return false;
647        }
648
649        return true;
650    }
651
652    // }}}
653    // {{{ _getNonce()
654
655    /**
656     * return nonce to detect timeout
657     *
658     * @access private
659     * @return string nonce value
660     */
661    function _getNonce()
662    {
663        if (method_exists($this, '_importGlobalVariable')) {
664            $this->server = &$this->_importGlobalVariable('server');
665        }
666
667        $time = time();
668        $hash = md5($time . $this->server['HTTP_USER_AGENT'] . $this->options['noncekey']);
669
670        return base64_encode($time) . $hash;
671    }
672
673    // }}}
674    // {{{ authenticationInfo()
675
676    /**
677     * output HTTP Authentication-Info header
678     *
679     * @notes md5 hash of contents is required if 'qop' is 'auth-int'
680     *
681     * @access private
682     * @param string MD5 hash of content
683     */
684    function authenticationInfo($contentMD5 = '') {
685
686        if($this->getAuth() && ($this->getAuthData('a1') != null)) {
687            $a1 = $this->getAuthData('a1');
688
689            // Work out authorisation response
690            $a2unhashed = ":".$this->selfURI();
691            if($this->auth['qop'] == 'auth-int') {
692                $a2unhashed .= ':'.$contentMD5;
693            }
694            $a2 = md5($a2unhashed);
695            $combined = $a1.':'.
696                        $this->nonce.':'.
697                        $this->auth['nc'].':'.
698                        $this->auth['cnonce'].':'.
699                        $this->auth['qop'].':'.
700                        $a2;
701
702            // Send authentication info
703            $wwwauth = 'Authentication-Info: ';
704            if($this->nonce != $this->nextNonce) {
705                $wwwauth .= 'nextnonce="'.$this->nextNonce.'", ';
706            }
707            $wwwauth .= 'qop='.$this->auth['qop'].', ';
708            $wwwauth .= 'rspauth="'.md5($combined).'", ';
709            $wwwauth .= 'cnonce="'.$this->auth['cnonce'].'", ';
710            $wwwauth .= 'nc='.$this->auth['nc'].'';
711            header($wwwauth);
712        }
713    }
714    // }}}
715    // {{{ setOption()
716    /**
717     * set authentication option
718     *
719     * @access public
720     * @param mixed $name key of option
721     * @param mixed $value value of option
722     * @return void
723     */
724    function setOption($name, $value = null)
725    {
726        if (is_array($name)) {
727            foreach($name as $key => $value) {
728                if (array_key_exists( $key, $this->options)) {
729                    $this->options[$key] = $value;
730                }
731            }
732        } else {
733            if (array_key_exists( $name, $this->options)) {
734                    $this->options[$name] = $value;
735            }
736        }
737    }
738
739    // }}}
740    // {{{ getOption()
741    /**
742     * get authentication option
743     *
744     * @access public
745     * @param string $name key of option
746     * @return mixed option value
747     */
748    function getOption($name)
749    {
750        if (array_key_exists( $name, $this->options)) {
751            return $this->options[$name];
752        }
753        if ($name == 'CancelText') {
754            return $this->CancelText;
755        }
756        if ($name == 'Realm') {
757            return $this->realm;
758        }
759        return false;
760    }
761
762    // }}}
763    // {{{ selfURI()
764    /**
765     * get self URI
766     *
767     * @access public
768     * @return string self URI
769     */
770    function selfURI()
771    {
772        if (method_exists($this, '_importGlobalVariable')) {
773            $this->server = &$this->_importGlobalVariable('server');
774        }
775
776        if (preg_match("/MSIE/",$this->server['HTTP_USER_AGENT'])) {
777            // query string should be removed for MSIE
778            $uri = preg_replace("/^(.*)\?/","\\1",$this->server['REQUEST_URI']);
779        } else {
780            $uri = $this->server['REQUEST_URI'];
781        }
782        return $uri;
783    }
784
785    // }}}
786
787}
788
789// }}}
790
791/*
792 * Local variables:
793 * tab-width: 4
794 * c-basic-offset: 4
795 * End:
796 */
797?>
798