Subversion Repositories Applications.framework

Rev

Rev 263 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
260 gduche 1
<?php
263 gduche 2
/**
3
 * La classe OpenIdClient est une implémentation d'un client OpenId, depuis une classe Zend.
4
 * Elle permet d'établir une connexion avec un serveur, en fonction d'un identifiant OpenId.
5
 * Elle permet de communiquer de manière sécurisée avec ce serveur, et doit aboutir a une
6
 * identification centralisée.
7
 * */
8
 
260 gduche 9
class OpenIdClient {
10
 
11
	//OpenID 2.0 namespace. Tous les messages OpenID 2.0 DOIVENT contenir la variable openid.ns et sa valeur
12
    const NS_2_0 = 'http://specs.openid.net/auth/2.0';
13
 
14
 
263 gduche 15
    // TODO : remplacer _storage par une gestion par cache ?
260 gduche 16
	/**
263 gduche 17
     * Variable permettant le stockage d'informations, notammenent à propos des clés DiffieHellmann
18
     * @var Storage $_storage
260 gduche 19
     */
20
    protected $_storage = null;
21
 
22
    /**
263 gduche 23
     * Tableau "cache" interne permettant d'éviter des accès inutiles au fichier storage
260 gduche 24
     * @var array $_cache
25
     */
26
    protected $_cache = array();
263 gduche 27
 
28
    // Client pour les requetes.
260 gduche 29
    private $client;
30
 
31
	/**
32
	 * Constructeur de l'application
33
	 * */
34
	function __construct()	{
35
		$this->client = new Client();
36
		$this->_storage = new StorageFile();
37
	}
38
 
39
	/**
40
	 * Fonction login
41
	 *
42
	 * Return true ou false
43
	 * > Ne retourne rien si true car redirige vers l'adresse du serveur OID
44
	 * */
45
	//FIXME : le paramètre immediate ?
263 gduche 46
	// A vérifier mais ça doit permettre de passer directement le mot de passe. Il reste plus qu'à trouver le nom de la variable mot de passe.
260 gduche 47
 
48
	function login($id, $immediate = false)	{
49
 
263 gduche 50
		// L'original retourne la fonction checkId, avec le parametre immediate = true
260 gduche 51
		// Je ne comprends pas l'utilité, je fusionne les deux pour l'instant
263 gduche 52
		// FIXME : si pas de comportement étrange, valider.
260 gduche 53
 
263 gduche 54
 
260 gduche 55
		//Tests et arrêt si non validé :
56
			//Normaliser (traite si XRI ou URL, normalize URL)
57
			//FIXME : voir avec JP pour équivalent dans framework
58
			if (!$this->normalize($id))	{
59
				return false;
60
			}
61
 
62
			//Discovery
63
			// Récupérer les informations sur le serveur OPEN ID
64
			/*
65
			 FIXME : ca change la valeur de l'id !!!
66
			 if (!$this->_discovery($id, $server, $version)) {
67
		         trigger_error('Discovery failed');
68
		         return false;
69
		    }*/
70
 
71
		    $retour_url = $this->client->consulter($id);
72
		    //Le retour contient les balises suivantes :
73
			/*
74
			 *
75
			 * <link rel="openid.server" href="http://www.myopenid.com/server" />
76
			 * <link rel="openid2.provider" href="http://www.myopenid.com/server" />
77
			 */
78
			$metaServeur = $this->verifierVersion($retour_url);
79
 
80
			//TODO : Voir avec JP : la classe client ne permet pas de vérifer le statut ??
81
 
82
			if ($retour_url === false) {
83
				trigger_error('L\'adresse $id est inacessible', E_USER_ERROR);
84
				return false;
85
			}
86
			if ($metaServeur === false)	{
87
				return false;
88
			}
89
			if (!$this->_associate($metaServeur['serveur'], $metaServeur['version'])) {
90
            	trigger_error('Impossible de s\'associer avec le serveur');
91
        	}
92
 
93
 
94
        	/*TODO : fonctionnement différent
95
        	 if (!$this->_getAssociation(
96
                $server,
97
                $handle,
98
                $macFunc,
99
                $secret,
100
                $expires)) {
101
            /* Use dumb mode *
102
            unset($handle);
103
            unset($macFunc);
104
            unset($secret);
105
            unset($expires);*
106
        }*/
107
 
108
			//on a la version, l'adresse du serveur et le realId si c'est une 2.0 dans metaServeur
109
			if (isset($metaServeur['realId'])) {
110
				$id = $metaServeur['realId'];
111
			}
112
 
113
			//Associate
114
			//getAssociation
115
 
116
		//Organisation des paramètres :
117
		$params = array();
118
 
119
 
120
        if ($metaServeur['version'] >= 2.0) {
121
            $params['openid.ns'] = self::NS_2_0;
122
        }
123
 
124
        $params['openid.mode'] = $immediate ?
125
            'checkid_immediate' : 'checkid_setup';
126
 
127
        $params['openid.identity'] = $id;
128
 
129
        //FIXME : Ex : $params['openid.claimed_id'] = $claimedId; > jvois pas l'intéret
130
        $params['openid.claimed_id'] = $id;
131
        /*
132
         * TODO : gérer les sessions et namespace
133
         * if ($metaServeur['version'] <= 2.0) {
134
            if ($this->_session !== null) {
135
                $this->_session->identity = $id;
136
                $this->_session->claimed_id = $claimedId;
137
            } else if (defined('SID')) {
138
                $_SESSION["zend_openid"] = array(
139
                    "identity" => $id,
140
                    "claimed_id" => $claimedId);
141
            } else {
142
                require_once "Zend/Session/Namespace.php";
143
                $this->_session = new Zend_Session_Namespace("zend_openid");
144
                $this->_session->identity = $id;
145
                $this->_session->claimed_id = $claimedId;
146
            }
147
        }*/
148
 
149
        if (isset($handle)) {
150
            $params['openid.assoc_handle'] = $handle;
151
        }
152
 
153
        //FIXME : $params['openid.return_to'] = $this->absoluteUrl($returnTo);
154
        $params['openid.return_to'] = $this->absoluteUrl(null);
155
 
156
        if (empty($root)) {
157
            $root = $this->selfUrl();
158
            if ($root[strlen($root)-1] != '/') {
159
                $root = dirname($root);
160
            }
161
        }
162
        if ($metaServeur['version'] >= 2.0) {
163
            $params['openid.realm'] = $root;
164
        } else {
165
            $params['openid.trust_root'] = $root;
166
        }
167
 
168
        /*FIXME ::
169
 
170
         if (!Zend_OpenId_Extension::forAll($extensions, 'prepareRequest', $params)) {
171
            $this->_setError("Extension::prepareRequest failure");
172
            return false;
173
        }*/
174
 
175
        $this->redirect($metaServeur['serveur'], $params);
176
        return true;
177
		//Renvoyer vers l'url
178
	}
179
 
180
	/**
181
     * Verifies authentication response from OpenID server.
182
     *
183
     * This is the second step of OpenID authentication process.
184
     * The function returns true on successful authentication and false on
185
     * failure.
186
     *
187
     * @param array $params HTTP query data from OpenID server
188
     * @param string &$identity this argument is set to end-user's claimed
189
     *  identifier or OpenID provider local identifier.
190
     * @param mixed $extensions extension object or array of extensions objects
191
     * @return bool
192
     */
193
    public function verify($params, &$identity = "", $extensions = null)
194
    {
195
 
196
        if (isset($params['openid_ns']) &&
197
            $params['openid_ns'] == $this->NS_2_0) {
198
            $version = 2.0;
199
        }
200
 
201
        if (isset($params["openid_claimed_id"])) {
202
            $identity = $params["openid_claimed_id"];
203
        } else if (isset($params["openid_identity"])){
204
            $identity = $params["openid_identity"];
205
        } else {
206
            $identity = "";
207
        }
208
 
209
        if ($version < 2.0 && !isset($params["openid_claimed_id"])) {
210
            if ($this->_session !== null) {
211
                if ($this->_session->identity === $identity) {
212
                    $identity = $this->_session->claimed_id;
213
                }
214
            } else if (defined('SID')) {
215
                if (isset($_SESSION["zend_openid"]["identity"]) &&
216
                    isset($_SESSION["zend_openid"]["claimed_id"]) &&
217
                    $_SESSION["zend_openid"]["identity"] === $identity) {
218
                    $identity = $_SESSION["zend_openid"]["claimed_id"];
219
                }
220
            } else {
221
                require_once "Zend/Session/Namespace.php";
222
                $this->_session = new Zend_Session_Namespace("zend_openid");
223
                if ($this->_session->identity === $identity) {
224
                    $identity = $this->_session->claimed_id;
225
                }
226
            }
227
        }
228
 
229
        if (empty($params['openid_mode'])) {
230
            $this->_setError("Missing openid.mode");
231
            return false;
232
        }
233
        if (empty($params['openid_return_to'])) {
234
            $this->_setError("Missing openid.return_to");
235
            return false;
236
        }
237
        if (empty($params['openid_signed'])) {
238
            $this->_setError("Missing openid.signed");
239
            return false;
240
        }
241
        if (empty($params['openid_sig'])) {
242
            $this->_setError("Missing openid.sig");
243
            return false;
244
        }
245
        if ($params['openid_mode'] != 'id_res') {
246
            $this->_setError("Wrong openid.mode '".$params['openid_mode']."' != 'id_res'");
247
            return false;
248
        }
249
        if (empty($params['openid_assoc_handle'])) {
250
            $this->_setError("Missing openid.assoc_handle");
251
            return false;
252
        }
253
 
254
        if ($params['openid_return_to'] != $this->selfUrl()) {
255
            /* Ignore query part in openid.return_to */
256
            $pos = strpos($params['openid_return_to'], '?');
257
            if ($pos === false ||
258
                SUBSTR($params['openid_return_to'], 0 , $pos) != $this->selfUrl()) {
259
 
260
                /*$this->_setError("Wrong openid.return_to '".
261
                    $params['openid_return_to']."' != '" . $this->selfUrl() ."'");*/
262
                trigger_error('Wrong openid.return_to', E_USER_ERROR);
263
 
264
                return false;
265
            }
266
        }
267
 
268
        if ($version >= 2.0) {
269
            if (empty($params['openid_response_nonce'])) {
270
                trigger_error('Missing openid.response_nonce', E_USER_ERROR);
271
                return false;
272
            }
273
            if (empty($params['openid_op_endpoint'])) {
274
                trigger_error('Missing openid.op_endpoint', E_USER_ERROR);
275
                return false;
276
            /* OpenID 2.0 (11.3) Checking the Nonce */
277
            } else if (!$this->_storage->isUniqueNonce($params['openid_op_endpoint'], $params['openid_response_nonce'])) {
278
                trigger_error('Duplicate openid.response_nonce', E_USER_ERROR);
279
                return false;
280
            }
281
        }
282
 
283
        if (!empty($params['openid_invalidate_handle'])) {
284
            if ($this->_storage->getAssociationByHandle(
285
                $params['openid_invalidate_handle'],
286
                $url,
287
                $macFunc,
288
                $secret,
289
                $expires)) {
290
                $this->_storage->delAssociation($url);
291
            }
292
        }
293
 
294
        if ($this->_storage->getAssociationByHandle(
295
                $params['openid_assoc_handle'],
296
                $url,
297
                $macFunc,
298
                $secret,
299
                $expires)) {
300
            $signed = explode(',', $params['openid_signed']);
301
            $data = '';
302
            foreach ($signed as $key) {
303
                $data .= $key . ':' . $params['openid_' . strtr($key,'.','_')] . "\n";
304
            }
305
            if (base64_decode($params['openid_sig']) ==
306
                Zend_OpenId::hashHmac($macFunc, $data, $secret)) {
307
                /*
308
                 * FIXME dépendance je sais pas pour quoi : a voir :
309
                 * if (!Zend_OpenId_Extension::forAll($extensions, 'parseResponse', $params)) {
310
                    $this->_setError("Extension::parseResponse failure");
311
                    return false;
312
                }*/
313
                /* OpenID 2.0 (11.2) Verifying Discovered Information */
314
                if (isset($params['openid_claimed_id'])) {
315
                    $id = $params['openid_claimed_id'];
316
                    if (!$this->normalize($id)) {
317
                        $this->_setError("Normalization failed");
318
                        return false;
319
                    } else if (!$this->_discovery($id, $discovered_server, $discovered_version)) {
320
                        $this->_setError("Discovery failed: " . $this->getError());
321
                        return false;
322
                    } else if ((!empty($params['openid_identity']) &&
323
                                $params["openid_identity"] != $id) ||
324
                               (!empty($params['openid_op_endpoint']) &&
325
                                $params['openid_op_endpoint'] != $discovered_server) ||
326
                               $discovered_version != $version) {
327
                        $this->_setError("Discovery information verification failed");
328
                        return false;
329
                    }
330
                }
331
                return true;
332
            }
333
            $this->_storage->delAssociation($url);
334
            $this->_setError("Signature check failed");
335
            return false;
336
        }
337
        else
338
        {
339
            /* Use dumb mode */
340
            if (isset($params['openid_claimed_id'])) {
341
                $id = $params['openid_claimed_id'];
342
            } else if (isset($params['openid_identity'])) {
343
                $id = $params['openid_identity'];
344
            } else {
345
                $this->_setError("Missing openid.claimed_id and openid.identity");
346
                return false;
347
            }
348
 
349
            if (!$this->normalize($id)) {
350
              	trigger_error('Normalization failed', E_USER_ERROR);
351
                return false;
352
            } else if (!$this->_discovery($id, $server, $discovered_version)) {
353
                trigger_error('Discovery failed', E_USER_ERROR);
354
                return false;
355
            }
356
 
357
            /* OpenID 2.0 (11.2) Verifying Discovered Information */
358
            if ((isset($params['openid_identity']) &&
359
                 $params["openid_identity"] != $id) ||
360
                (isset($params['openid_op_endpoint']) &&
361
                 $params['openid_op_endpoint'] != $server) ||
362
                $discovered_version != $version) {
363
                trigger_error('Discovery information verification failed', E_USER_ERROR);
364
                return false;
365
            }
366
 
367
            $params2 = array();
368
            foreach ($params as $key => $val) {
369
                if (strpos($key, 'openid_ns_') === 0) {
370
                    $key = 'openid.ns.' . substr($key, strlen('openid_ns_'));
371
                } else if (strpos($key, 'openid_sreg_') === 0) {
372
                    $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_'));
373
                } else if (strpos($key, 'openid_') === 0) {
374
                    $key = 'openid.' . substr($key, strlen('openid_'));
375
                }
376
                $params2[$key] = $val;
377
            }
378
            $params2['openid.mode'] = 'check_authentication';
379
            $ret = $this->client->modifier($serveur, $params2);
380
 
381
            //_httpRequest($server, 'POST', $params2, $status);
382
            if ($ret === false) {
383
                trigger_error("'Dumb' signature verification HTTP request failed", E_USER_ERROR);
384
                return false;
385
            }
386
            $r = array();
387
            if (is_string($ret)) {
388
                foreach(explode("\n", $ret) as $line) {
389
                    $line = trim($line);
390
                    if (!empty($line)) {
391
                        $x = explode(':', $line, 2);
392
                        if (is_array($x) && count($x) == 2) {
393
                            list($key, $value) = $x;
394
                            $r[trim($key)] = trim($value);
395
                        }
396
                    }
397
                }
398
            }
399
            $ret = $r;
400
            if (!empty($ret['invalidate_handle'])) {
401
                if ($this->_storage->getAssociationByHandle(
402
                    $ret['invalidate_handle'],
403
                    $url,
404
                    $macFunc,
405
                    $secret,
406
                    $expires)) {
407
                    $this->_storage->delAssociation($url);
408
                }
409
            }
410
            if (isset($ret['is_valid']) && $ret['is_valid'] == 'true') {
411
                if (!Zend_OpenId_Extension::forAll($extensions, 'parseResponse', $params)) {
412
                    $this->_setError("Extension::parseResponse failure");
413
                    return false;
414
                }
415
                return true;
416
            }
417
            $this->_setError("'Dumb' signature verification failed");
418
            return false;
419
        }
420
    }
421
 
422
 
423
	/**
424
     * Performs discovery of identity and finds OpenID URL, OpenID server URL
425
     * and OpenID protocol version. Returns true on succees and false on
426
     * failure.
427
     *
428
     * @param string &$id OpenID identity URL
429
     * @param string &$server OpenID server URL
430
     * @param float &$version OpenID protocol version
431
     * @return bool
432
     * @todo OpenID 2.0 (7.3) XRI and Yadis discovery
433
     */
434
    protected function _discovery(&$id, &$server, &$version)
435
    {
436
        $realId = $id;
437
        if ($this->_storage->getDiscoveryInfo(
438
                $id,
439
                $realId,
440
                $server,
441
                $version,
442
                $expire)) {
443
            $id = $realId;
444
            return true;
445
        }
446
 
447
        /* TODO: OpenID 2.0 (7.3) XRI and Yadis discovery */
448
 
449
        /* HTML-based discovery */
450
        $clientDiscovery = new Client();
451
 
452
        //TODO : rajouter un test sur le statut de la réponse
453
        // Nécessite la prise en compte des entetes dans le framework
454
 
455
        /*if ($status != 200 || !is_string($response)) {
456
            return false;
457
        }*/
458
        $reponse = $clientDiscovery->consulter($id);
459
        $metaServeur = $this->verifierVersion($reponse);
460
        if (!isset($metaServeur) || empty($reponse))	{
461
        	trigger_error('Aucune donnée OpenId', E_USER_ERROR);
462
        	return false;
463
        }
464
 
465
        $expire = time() + 60 * 60;
466
        $this->_storage->addDiscoveryInfo($id, $metaServeur['realId'], $metaServeur['serveur'], $metaServeur['version'], $expire);
467
        if (!empty($metaServeur['realId'])) {
468
        	$id = $metaServeur['realId'];
469
        }
470
        return true;
471
    }
472
 
473
 
474
 
475
 
476
	//Parser l'HTML de réponse pour trouver la version du serveur OPEN ID
477
	function verifierVersion($reponseHtml)	{
478
 
479
		// TODO : remplacer l'arlgorythme suivant par cette solution :
480
		//1. Chercher l'existence d'une balise openidN.provider
481
		//2. Déterminer la version en fonction de la chaine : openid2.provider => 2.0; openid.provider => 1.1
482
		//3. Récupérer l'url du serveur href="serveur"
483
		//4. SI 2.0, récupérer la valeur réelle de l'ID
484
 
485
		//TODO : penser à tester les deux versions du serveur
486
 
487
		$metaServeur = Array();
488
 
489
		if (preg_match(
490
                '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
491
                $reponseHtml,
492
                $r)) {
493
            $metaServeur['version'] = 2.0;
494
            $metaServeur['serveur'] = $r[3];
495
        } else if (preg_match(
496
                '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.provider[ \t]*[^"\']*\\3[^>]*\/?>/i',
497
                $reponseHtml,
498
                $r)) {
499
            $metaServeur['version'] = 2.0;
500
            $metaServeur['serveur'] = $r[1];
501
        } else if (preg_match(
502
                '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
503
                $reponseHtml,
504
                $r)) {
505
            $metaServeur['version'] = 1.1;
506
            $metaServeur['serveur'] = $r[3];
507
        } else if (preg_match(
508
                '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.server[ \t]*[^"\']*\\3[^>]*\/?>/i',
509
                $reponseHtml,
510
                $r)) {
511
            $metaServeur['version'] = 1.1;
512
            $metaServeur['serveur'] = $r[2];
513
        } else {
514
            return false;
515
        }
516
 
517
        if ($metaServeur['version'] >= 2.0) {
518
            if (preg_match(
519
                    '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
520
                    $reponseHtml,
521
                    $r)) {
522
                $metaServeur['realId'] = $r[3];
523
            } else if (preg_match(
524
                    '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid2.local_id[ \t]*[^"\']*\\3[^>]*\/?>/i',
525
                    $reponseHtml,
526
                    $r)) {
527
                $metaServeur['realId'] = $r[2];
528
            }
529
        } else {
530
            if (preg_match(
531
                    '/<link[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\1[^>]*href=(["\'])([^"\']+)\\2[^>]*\/?>/i',
532
                    $reponseHtml,
533
                    $r)) {
534
                $metaServeur['realId'] = $r[3];
535
            } else if (preg_match(
536
                    '/<link[^>]*href=(["\'])([^"\']+)\\1[^>]*rel=(["\'])[ \t]*(?:[^ \t"\']+[ \t]+)*?openid.delegate[ \t]*[^"\']*\\3[^>]*\/?>/i',
537
                    $reponseHtml,
538
                    $r)) {
539
                $metaServeur['realId'] = $r[2];
540
            }
541
        }
542
 
543
        return $metaServeur;
544
	}
545
 
546
	/**
547
     * Create (or reuse existing) association between OpenID consumer and
548
     * OpenID server based on Diffie-Hellman key agreement. Returns true
549
     * on success and false on failure.
550
     *
551
     * @param string $url OpenID server url
552
     * @param float $version OpenID protocol version
553
     * @param string $priv_key for testing only
554
     * @return bool
555
     */
556
    protected function _associate($url, $version, $priv_key=null)
557
    {
558
        /* Check if we already have association in chace or storage */
559
       /*
560
        * TODO : Utiliser le stockage plutot
561
        * */
562
        if ($this->_getAssociation(
563
                $url,
564
                $handle,
565
                $macFunc,
566
                $secret,
567
                $expires)) {
568
            return true;
569
        }
570
 
571
       /*
572
        * TODO : utiliser le fichier de config
573
        * if ($this->_dumbMode) {
574
	       return true;
575
       }*/
576
 
577
        $params = array();
578
 
579
        if ($version >= 2.0) {
580
            $params = array(
581
                'openid.ns'           => $this->NS_2_0,
582
                'openid.mode'         => 'associate',
583
                'openid.assoc_type'   => 'HMAC-SHA256',
584
                'openid.session_type' => 'DH-SHA256',
585
            );
586
        } else {
587
            $params = array(
588
                'openid.mode'         => 'associate',
589
                'openid.assoc_type'   => 'HMAC-SHA1',
590
                'openid.session_type' => 'DH-SHA1',
591
            );
592
        }
593
 
594
        $dh = DiffieHellmanUtil::createDhKey(pack('H*', DiffieHellmanUtil::DH_P),
595
                                       pack('H*', DiffieHellmanUtil::DH_G),
596
                                       $priv_key);
597
        $dh_details = DiffieHellmanUtil::getDhKeyDetails($dh);
598
 
599
        $params['openid.dh_modulus']         = base64_encode(
600
            DiffieHellmanUtil::btwoc($dh_details['p']));
601
        $params['openid.dh_gen']             = base64_encode(
602
            DiffieHellmanUtil::btwoc($dh_details['g']));
603
        $params['openid.dh_consumer_public'] = base64_encode(
604
            DiffieHellmanUtil::btwoc($dh_details['pub_key']));
605
 
606
        while(1) {
607
        	//FIXME : c'est pas une modification ...
608
            $ret = $this->client->modifier($url, $params); // FIXME : a quoi sert status ?, $status);
609
            if ($ret === false) {
610
                //$this->_setError("HTTP request failed");
611
                trigger_error('La requête a échoué', E_USER_ERROR);
612
                return false;
613
            }
614
 
615
            $r = array();
616
            $bad_response = false;
617
            foreach(explode("\n", $ret) as $line) {
618
                $line = trim($line);
619
                if (!empty($line)) {
620
                    $x = explode(':', $line, 2);
621
                    if (is_array($x) && count($x) == 2) {
622
                        list($key, $value) = $x;
623
                        $r[trim($key)] = trim($value);
624
                    } else {
625
                        $bad_response = true;
626
                    }
627
                }
628
            }
629
            if ($bad_response && strpos($ret, 'Unknown session type') !== false) {
630
                $r['error_code'] = 'unsupported-type';
631
            }
632
            $ret = $r;
633
 
634
            if (isset($ret['error_code']) &&
635
                $ret['error_code'] == 'unsupported-type') {
636
                if ($params['openid.session_type'] == 'DH-SHA256') {
637
                    $params['openid.session_type'] = 'DH-SHA1';
638
                    $params['openid.assoc_type'] = 'HMAC-SHA1';
639
                } else if ($params['openid.session_type'] == 'DH-SHA1') {
640
                    $params['openid.session_type'] = 'no-encryption';
641
                } else {
642
                    trigger_error("The OpenID service responded with: " . $ret['error_code'], E_USER_ERROR);
643
                    return false;
644
                }
645
            } else {
646
                break;
647
            }
648
        }
649
 
650
        /*
651
        FIXME : gestion du statut avec la classe client ??
652
        if ($status != 200) {
653
            $this->_setError("The server responded with status code: " . $status);
654
            return false;
655
        }*/
656
 
657
        if ($version >= 2.0 &&
658
            isset($ret['ns']) &&
659
            $ret['ns'] != $this->NS_2_0) {
660
            $this->_setError("Wrong namespace definition in the server response");
661
            return false;
662
        }
663
 
664
        if (!isset($ret['assoc_handle']) ||
665
            !isset($ret['expires_in']) ||
666
            !isset($ret['assoc_type']) ||
667
            $params['openid.assoc_type'] != $ret['assoc_type']) {
668
            if ($params['openid.assoc_type'] != $ret['assoc_type']) {
669
                $this->_setError("The returned assoc_type differed from the supplied openid.assoc_type");
670
            } else {
671
                $this->_setError("Missing required data from provider (assoc_handle, expires_in, assoc_type are required)");
672
            }
673
            return false;
674
        }
675
 
676
        $handle     = $ret['assoc_handle'];
677
        $expiresIn = $ret['expires_in'];
678
 
679
        if ($ret['assoc_type'] == 'HMAC-SHA1') {
680
            $macFunc = 'sha1';
681
        } else if ($ret['assoc_type'] == 'HMAC-SHA256' &&
682
            $version >= 2.0) {
683
            $macFunc = 'sha256';
684
        } else {
685
            $this->_setError("Unsupported assoc_type");
686
            return false;
687
        }
688
 
689
        if ((empty($ret['session_type']) ||
690
             ($version >= 2.0 && $ret['session_type'] == 'no-encryption')) &&
691
             isset($ret['mac_key'])) {
692
            $secret = base64_decode($ret['mac_key']);
693
        } else if (isset($ret['session_type']) &&
694
            $ret['session_type'] == 'DH-SHA1' &&
695
            !empty($ret['dh_server_public']) &&
696
            !empty($ret['enc_mac_key'])) {
697
            $dhFunc = 'sha1';
698
        } else if (isset($ret['session_type']) &&
699
            $ret['session_type'] == 'DH-SHA256' &&
700
            $version >= 2.0 &&
701
            !empty($ret['dh_server_public']) &&
702
            !empty($ret['enc_mac_key'])) {
703
            $dhFunc = 'sha256';
704
        } else {
705
            $this->_setError("Unsupported session_type");
706
            return false;
707
        }
708
        if (isset($dhFunc)) {
709
            $serverPub = base64_decode($ret['dh_server_public']);
710
            $dhSec = DiffieHellmanUtil::computeDhSecret($serverPub, $dh);
711
            if ($dhSec === false) {
712
                $this->_setError("DH secret comutation failed");
713
                return false;
714
            }
715
            $sec = $this->digest($dhFunc, $dhSec);
716
            if ($sec === false) {
717
                $this->_setError("Could not create digest");
718
                return false;
719
            }
720
            $secret = $sec ^ base64_decode($ret['enc_mac_key']);
721
        }
722
        if ($macFunc == 'sha1') {
723
            if (DiffieHellmanUtil::strlen($secret) != 20) {
724
                $this->_setError("The length of the sha1 secret must be 20");
725
                return false;
726
            }
727
        } else if ($macFunc == 'sha256') {
728
            if (DiffieHellmanUtil::strlen($secret) != 32) {
729
                $this->_setError("The length of the sha256 secret must be 32");
730
                return false;
731
            }
732
        }
733
 
734
        $this->_addAssociation(
735
            $url,
736
            $handle,
737
            $macFunc,
738
            $secret,
739
            time() + $expiresIn);
740
       /* $this->association['url'] = $url;
741
        $this->association['handle'] = $handle;
742
        $this->association['macFunc'] = $macFunc;
743
        $this->association['secret'] = $secret;
744
        $this->association['expiresIn'] = time() + $expiresIn;*/
745
 
746
        return true;
747
    }
748
 
749
	/**
750
     * Store assiciation in internal chace and external storage
751
     *
752
     * @param string $url OpenID server url
753
     * @param string $handle association handle
754
     * @param string $macFunc HMAC function (sha1 or sha256)
755
     * @param string $secret shared secret
756
     * @param integer $expires expiration UNIX time
757
     * @return void
758
     */
759
    protected function _addAssociation($url, $handle, $macFunc, $secret, $expires)
760
    {
761
        $this->_cache[$url] = array($handle, $macFunc, $secret, $expires);
762
        return $this->_storage->addAssociation(
763
            $url,
764
            $handle,
765
            $macFunc,
766
            $secret,
767
            $expires);
768
    }
769
 
770
	/**
771
     * Retrive assiciation information for given $url from internal cahce or
772
     * external storage
773
     *
774
     * @param string $url OpenID server url
775
     * @param string &$handle association handle
776
     * @param string &$macFunc HMAC function (sha1 or sha256)
777
     * @param string &$secret shared secret
778
     * @param integer &$expires expiration UNIX time
779
     * @return void
780
     */
781
    protected function _getAssociation($url, &$handle, &$macFunc, &$secret, &$expires)
782
    {
783
        if (isset($this->_cache[$url])) {
784
            $handle   = $this->_cache[$url][0];
785
            $macFunc = $this->_cache[$url][1];
786
            $secret   = $this->_cache[$url][2];
787
            $expires  = $this->_cache[$url][3];
788
            return true;
789
        }
790
        if ($this->_storage->getAssociation(
791
                $url,
792
                $handle,
793
                $macFunc,
794
                $secret,
795
                $expires)) {
796
            $this->_cache[$url] = array($handle, $macFunc, $secret, $expires);
797
            return true;
798
        }
799
        return false;
800
    }
801
 
802
	/**
803
     * Normalizes URL according to RFC 3986 to use it in comparison operations.
804
     * The function gets URL argument by reference and modifies it.
805
     * It returns true on success and false of failure.
806
     *
807
     * @param string &$id url to be normalized
808
     * @return bool
809
     */
810
    static public function normalizeUrl(&$id)
811
    {
812
        // RFC 3986, 6.2.2.  Syntax-Based Normalization
813
 
814
        // RFC 3986, 6.2.2.2 Percent-Encoding Normalization
815
        $i = 0;
816
        $n = strlen($id);
817
        $res = '';
818
        while ($i < $n) {
819
            if ($id[$i] == '%') {
820
                if ($i + 2 >= $n) {
821
                    return false;
822
                }
823
                ++$i;
824
                if ($id[$i] >= '0' && $id[$i] <= '9') {
825
                    $c = ord($id[$i]) - ord('0');
826
                } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
827
                    $c = ord($id[$i]) - ord('A') + 10;
828
                } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
829
                    $c = ord($id[$i]) - ord('a') + 10;
830
                } else {
831
                    return false;
832
                }
833
                ++$i;
834
                if ($id[$i] >= '0' && $id[$i] <= '9') {
835
                    $c = ($c << 4) | (ord($id[$i]) - ord('0'));
836
                } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
837
                    $c = ($c << 4) | (ord($id[$i]) - ord('A') + 10);
838
                } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
839
                    $c = ($c << 4) | (ord($id[$i]) - ord('a') + 10);
840
                } else {
841
                    return false;
842
                }
843
                ++$i;
844
                $ch = chr($c);
845
                if (($ch >= 'A' && $ch <= 'Z') ||
846
                    ($ch >= 'a' && $ch <= 'z') ||
847
                    $ch == '-' ||
848
                    $ch == '.' ||
849
                    $ch == '_' ||
850
                    $ch == '~') {
851
                    $res .= $ch;
852
                } else {
853
                    $res .= '%';
854
                    if (($c >> 4) < 10) {
855
                        $res .= chr(($c >> 4) + ord('0'));
856
                    } else {
857
                        $res .= chr(($c >> 4) - 10 + ord('A'));
858
                    }
859
                    $c = $c & 0xf;
860
                    if ($c < 10) {
861
                        $res .= chr($c + ord('0'));
862
                    } else {
863
                        $res .= chr($c - 10 + ord('A'));
864
                    }
865
                }
866
            } else {
867
                $res .= $id[$i++];
868
            }
869
        }
870
 
871
        if (!preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?#]*)?((?:[?](?:[^#]*))?)((?:#.*)?)$|', $res, $reg)) {
872
            return false;
873
        }
874
        $scheme = $reg[1];
875
        $auth = $reg[2];
876
        $host = $reg[3];
877
        $port = $reg[4];
878
        $path = $reg[5];
879
        $query = $reg[6];
880
        $fragment = $reg[7]; /* strip it */
881
 
882
        if (empty($scheme) || empty($host)) {
883
            return false;
884
        }
885
 
886
        // RFC 3986, 6.2.2.1.  Case Normalization
887
        $scheme = strtolower($scheme);
888
        $host = strtolower($host);
889
 
890
        // RFC 3986, 6.2.2.3.  Path Segment Normalization
891
        if (!empty($path)) {
892
            $i = 0;
893
            $n = strlen($path);
894
            $res = "";
895
            while ($i < $n) {
896
                if ($path[$i] == '/') {
897
                    ++$i;
898
                    while ($i < $n && $path[$i] == '/') {
899
                        ++$i;
900
                    }
901
                    if ($i < $n && $path[$i] == '.') {
902
                        ++$i;
903
                        if ($i < $n && $path[$i] == '.') {
904
                            ++$i;
905
                            if ($i == $n || $path[$i] == '/') {
906
                                if (($pos = strrpos($res, '/')) !== false) {
907
                                    $res = substr($res, 0, $pos);
908
                                }
909
                            } else {
910
                                    $res .= '/..';
911
                            }
912
                        } else if ($i != $n && $path[$i] != '/') {
913
                            $res .= '/.';
914
                        }
915
                    } else {
916
                        $res .= '/';
917
                    }
918
                } else {
919
                    $res .= $path[$i++];
920
                }
921
            }
922
            $path = $res;
923
        }
924
 
925
        // RFC 3986,6.2.3.  Scheme-Based Normalization
926
        if ($scheme == 'http') {
927
            if ($port == 80) {
928
                $port = '';
929
            }
930
        } else if ($scheme == 'https') {
931
            if ($port == 443) {
932
                $port = '';
933
            }
934
        }
935
        if (empty($path)) {
936
            $path = '/';
937
        }
938
 
939
        $id = $scheme
940
            . '://'
941
            . $auth
942
            . $host
943
            . (empty($port) ? '' : (':' . $port))
944
            . $path
945
            . $query;
946
        return true;
947
    }
948
 
949
 
950
    /**
951
     * Normaliser l'identifiant OpenId qui peut être une URL ou nom XRI
952
     * Retourne true ou false en cas d'erreur.
953
     *
954
     * Règles de normalisation :
955
     * 1. If the user's input starts with one of the "xri://", "xri://$ip*",
956
     *    or "xri://$dns*" prefixes, they MUST be stripped off, so that XRIs
957
     *    are used in the canonical form, and URI-authority XRIs are further
958
     *    considered URL identifiers.
959
     * 2. If the first character of the resulting string is an XRI Global
960
     *    Context Symbol ("=", "@", "+", "$", "!"), then the input SHOULD be
961
     *    treated as an XRI.
962
     * 3. Otherwise, the input SHOULD be treated as an http URL; if it does
963
     *    not include a "http" or "https" scheme, the Identifier MUST be
964
     *    prefixed with the string "http://".
965
     * 4. URL identifiers MUST then be further normalized by both following
966
     *    redirects when retrieving their content and finally applying the
967
     *    rules in Section 6 of [RFC3986] to the final destination URL.
968
     * @param string &$id identifier to be normalized
969
     * @return bool
970
     */
971
    static public function normalize(&$id)
972
    {
973
        $id = trim($id);
974
        if (strlen($id) === 0) {
975
            return true;
976
        }
977
 
978
        // 7.2.1
979
        if (strpos($id, 'xri://$ip*') === 0) {
980
            $id = substr($id, strlen('xri://$ip*'));
981
        } else if (strpos($id, 'xri://$dns*') === 0) {
982
            $id = substr($id, strlen('xri://$dns*'));
983
        } else if (strpos($id, 'xri://') === 0) {
984
            $id = substr($id, strlen('xri://'));
985
        }
986
 
987
        // 7.2.2
988
        if ($id[0] == '=' ||
989
            $id[0] == '@' ||
990
            $id[0] == '+' ||
991
            $id[0] == '$' ||
992
            $id[0] == '!') {
993
            return true;
994
        }
995
 
996
        // 7.2.3
997
        if (strpos($id, "://") === false) {
998
            $id = 'http://' . $id;
999
        }
1000
 
1001
        // 7.2.4
1002
        return self::normalizeURL($id);
1003
    }
1004
 
1005
	/**
1006
     * Generates a hash value (message digest) according to given algorithm.
1007
     * It returns RAW binary string.
1008
     *
1009
     * This is a wrapper function that uses one of available internal function
1010
     * dependent on given PHP configuration. It may use various functions from
1011
     *  ext/openssl, ext/hash, ext/mhash or ext/standard.
1012
     *
1013
     * @param string $func digest algorithm
1014
     * @param string $data data to sign
1015
     * @return string RAW digital signature
1016
     * @throws Zend_OpenId_Exception
1017
     */
1018
    public function digest($func, $data)
1019
    {
1020
        if (function_exists('openssl_digest')) {
1021
            return openssl_digest($data, $func, true);
1022
        } else if (function_exists('hash')) {
1023
            return hash($func, $data, true);
1024
        } else if ($func === 'sha1') {
1025
            return sha1($data, true);
1026
        } else if ($func === 'sha256') {
1027
            if (function_exists('mhash')) {
1028
                return mhash(MHASH_SHA256 , $data);
1029
            }
1030
        }
1031
        /*require_once "Zend/OpenId/Exception.php";
1032
        throw new Zend_OpenId_Exception(
1033
            'Unsupported digest algorithm "' . $func . '".',
1034
            Zend_OpenId_Exception::UNSUPPORTED_DIGEST);*/
1035
        trigger_error('Unsupported digest algorithm '.$func , E_USER_ERROR);
1036
    }
1037
 
1038
 
1039
 
1040
 
1041
 
1042
 
1043
 
1044
 
1045
 	/**
1046
     * Returns a full URL that was requested on current HTTP request.
1047
     *
1048
     * @return string
1049
     */
1050
    public function selfUrl()
1051
    {
1052
        /*FIXME :
1053
         * if ($this->$selfUrl !== null) {
1054
            return $this->$selfUrl;
1055
        } */
1056
 
1057
        if (isset($_SERVER['SCRIPT_URI'])) {
1058
            return $_SERVER['SCRIPT_URI'];
1059
        }
1060
        $url = '';
1061
        $port = '';
1062
        if (isset($_SERVER['HTTP_HOST'])) {
1063
            if (($pos = strpos($_SERVER['HTTP_HOST'], ':')) === false) {
1064
                if (isset($_SERVER['SERVER_PORT'])) {
1065
                    $port = ':' . $_SERVER['SERVER_PORT'];
1066
                }
1067
                $url = $_SERVER['HTTP_HOST'];
1068
            } else {
1069
                $url = substr($_SERVER['HTTP_HOST'], 0, $pos);
1070
                $port = substr($_SERVER['HTTP_HOST'], $pos);
1071
            }
1072
        } else if (isset($_SERVER['SERVER_NAME'])) {
1073
            $url = $_SERVER['SERVER_NAME'];
1074
            if (isset($_SERVER['SERVER_PORT'])) {
1075
                $port = ':' . $_SERVER['SERVER_PORT'];
1076
            }
1077
        }
1078
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
1079
            $url = 'https://' . $url;
1080
            if ($port == ':443') {
1081
                $port = '';
1082
            }
1083
        } else {
1084
            $url = 'http://' . $url;
1085
            if ($port == ':80') {
1086
                $port = '';
1087
            }
1088
        }
1089
 
1090
        $url .= $port;
1091
        if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
1092
            $url .= $_SERVER['HTTP_X_REWRITE_URL'];
1093
        } elseif (isset($_SERVER['REQUEST_URI'])) {
1094
            $query = strpos($_SERVER['REQUEST_URI'], '?');
1095
            if ($query === false) {
1096
                $url .= $_SERVER['REQUEST_URI'];
1097
            } else {
1098
                $url .= substr($_SERVER['REQUEST_URI'], 0, $query);
1099
            }
1100
        } else if (isset($_SERVER['SCRIPT_URL'])) {
1101
            $url .= $_SERVER['SCRIPT_URL'];
1102
        } else if (isset($_SERVER['REDIRECT_URL'])) {
1103
            $url .= $_SERVER['REDIRECT_URL'];
1104
        } else if (isset($_SERVER['PHP_SELF'])) {
1105
            $url .= $_SERVER['PHP_SELF'];
1106
        } else if (isset($_SERVER['SCRIPT_NAME'])) {
1107
            $url .= $_SERVER['SCRIPT_NAME'];
1108
            if (isset($_SERVER['PATH_INFO'])) {
1109
                $url .= $_SERVER['PATH_INFO'];
1110
            }
1111
        }
1112
        return $url;
1113
    }
1114
 
1115
 
1116
    //TODO : vérifier si les fonctions FWK & ZEND sont bien équivalente
1117
	/**
1118
     * Retourne l'url absolue d'une url donnée
1119
     *
1120
     * @param string $url absilute or relative URL
1121
     * @return string
1122
     */
1123
    public function absoluteUrl($url)
1124
    {
1125
    	if (!empty($ur))	{
1126
    		$urlAbsolue = new Url($url);
1127
    		$urlAbsolue->normaliser();
1128
    		$url = $urlAbsolue->getUrl();
1129
    	} else {
1130
    		$url = $this->selfUrl();
1131
    	}
1132
 
1133
    	return $url;
1134
    	/*
1135
        if (empty($url)) {
1136
            return $this->selfUrl();
1137
        } else if (!preg_match('|^([^:]+)://|', $url)) {
1138
            if (preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?]*)?((?:[?](?:[^#]*))?(?:#.*)?)$|', $this->selfUrl(), $reg)) {
1139
                $scheme = $reg[1];
1140
                $auth = $reg[2];
1141
                $host = $reg[3];
1142
                $port = $reg[4];
1143
                $path = $reg[5];
1144
                $query = $reg[6];
1145
                if ($url[0] == '/') {
1146
                    return $scheme
1147
                        . '://'
1148
                        . $auth
1149
                        . $host
1150
                        . (empty($port) ? '' : (':' . $port))
1151
                        . $url;
1152
                } else {
1153
                    $dir = dirname($path);
1154
                    return $scheme
1155
                        . '://'
1156
                        . $auth
1157
                        . $host
1158
                        . (empty($port) ? '' : (':' . $port))
1159
                        . (strlen($dir) > 1 ? $dir : '')
1160
                        . '/'
1161
                        . $url;
1162
                }
1163
            }
1164
        }
1165
        return $url;*/
1166
    }
1167
 
1168
 
1169
    //TODO : voir si on ne peut pas glisser ça dans client ?
1170
	//FIXME : je met une fonction SIMPLISSIME a améliorer et reécrire
1171
    // La fonction de Zend est plus poussée est prend en compte le cas ou l'header ne peut pas etre envoyé
1172
    /**
1173
     * Rediriger vers la page du serveur avec les paramètres de confiration
1174
     *
1175
     * @param string $url URL de retour
1176
     * @param array $params paramètres additionnels
1177
     */
1178
    public function redirect($url, $params)	{
1179
    	//1. fabriquer l'url Get
1180
    	$urlRedirection = new Url($url);
1181
    	$urlRedirection->setRequete($params);
1182
    	//echo $urlRedirection->getUrl();
1183
    	try {
1184
    		header('Location:'.$urlRedirection->getUrl());
1185
    	} catch (Exception $e) {
1186
    		//TODO : voir autres méthodes de redirection
1187
    		// > balise META
1188
    		// > formulaire HTML
1189
    		// > JS
1190
    	}
1191
    }
1192
}
1193
?>