Subversion Repositories Applications.annuaire

Rev

Rev 42 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
42 aurelien 1
<?php
2
 
3
/**
4
 * This file supplies a Memcached store backend for OpenID servers and
5
 * consumers.
6
 *
7
 * PHP versions 4 and 5
8
 *
9
 * LICENSE: See the COPYING file included in this distribution.
10
 *
11
 * @package OpenID
12
 * @author JanRain, Inc. <openid@janrain.com>
13
 * @copyright 2005 Janrain, Inc.
14
 * @license http://www.gnu.org/copyleft/lesser.html LGPL
15
 *
16
 */
17
 
18
/**
19
 * Require base class for creating a new interface.
20
 */
21
require_once 'Auth/OpenID.php';
22
require_once 'Auth/OpenID/Interface.php';
23
require_once 'Auth/OpenID/HMACSHA1.php';
24
 
25
/**
26
 * This is a filesystem-based store for OpenID associations and
27
 * nonces.  This store should be safe for use in concurrent systems on
28
 * both windows and unix (excluding NFS filesystems).  There are a
29
 * couple race conditions in the system, but those failure cases have
30
 * been set up in such a way that the worst-case behavior is someone
31
 * having to try to log in a second time.
32
 *
33
 * Most of the methods of this class are implementation details.
34
 * People wishing to just use this store need only pay attention to
35
 * the constructor.
36
 *
37
 * @package OpenID
38
 */
39
class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore {
40
 
41
    /**
42
     * Initializes a new {@link Auth_OpenID_FileStore}.  This
43
     * initializes the nonce and association directories, which are
44
     * subdirectories of the directory passed in.
45
     *
46
     * @param string $directory This is the directory to put the store
47
     * directories in.
48
     */
49
    function Auth_OpenID_FileStore($directory)
50
    {
51
        if (!Auth_OpenID::ensureDir($directory)) {
52
            trigger_error('Not a directory and failed to create: '
53
                          . $directory, E_USER_ERROR);
54
        }
55
        $directory = realpath($directory);
56
 
57
        $this->directory = $directory;
58
        $this->active = true;
59
 
60
        $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces';
61
 
62
        $this->association_dir = $directory . DIRECTORY_SEPARATOR .
63
            'associations';
64
 
65
        // Temp dir must be on the same filesystem as the assciations
66
        // $directory and the $directory containing the auth key file.
67
        $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp';
68
 
69
        $this->auth_key_name = $directory . DIRECTORY_SEPARATOR . 'auth_key';
70
 
71
        $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds
72
 
73
        if (!$this->_setup()) {
74
            trigger_error('Failed to initialize OpenID file store in ' .
75
                          $directory, E_USER_ERROR);
76
        }
77
    }
78
 
79
    function destroy()
80
    {
81
        Auth_OpenID_FileStore::_rmtree($this->directory);
82
        $this->active = false;
83
    }
84
 
85
    /**
86
     * Make sure that the directories in which we store our data
87
     * exist.
88
     *
89
     * @access private
90
     */
91
    function _setup()
92
    {
93
        return (Auth_OpenID::ensureDir(dirname($this->auth_key_name)) &&
94
                Auth_OpenID::ensureDir($this->nonce_dir) &&
95
                Auth_OpenID::ensureDir($this->association_dir) &&
96
                Auth_OpenID::ensureDir($this->temp_dir));
97
    }
98
 
99
    /**
100
     * Create a temporary file on the same filesystem as
101
     * $this->auth_key_name and $this->association_dir.
102
     *
103
     * The temporary directory should not be cleaned if there are any
104
     * processes using the store. If there is no active process using
105
     * the store, it is safe to remove all of the files in the
106
     * temporary directory.
107
     *
108
     * @return array ($fd, $filename)
109
     * @access private
110
     */
111
    function _mktemp()
112
    {
113
        $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir);
114
        $file_obj = @fopen($name, 'wb');
115
        if ($file_obj !== false) {
116
            return array($file_obj, $name);
117
        } else {
118
            Auth_OpenID_FileStore::_removeIfPresent($name);
119
        }
120
    }
121
 
122
    /**
123
     * Read the auth key from the auth key file. Will return None if
124
     * there is currently no key.
125
     *
126
     * @return mixed
127
     */
128
    function readAuthKey()
129
    {
130
        if (!$this->active) {
131
            trigger_error("FileStore no longer active", E_USER_ERROR);
132
            return null;
133
        }
134
 
135
        $auth_key_file = @fopen($this->auth_key_name, 'rb');
136
        if ($auth_key_file === false) {
137
            return null;
138
        }
139
 
140
        $key = fread($auth_key_file, filesize($this->auth_key_name));
141
        fclose($auth_key_file);
142
 
143
        return $key;
144
    }
145
 
146
    /**
147
     * Generate a new random auth key and safely store it in the
148
     * location specified by $this->auth_key_name.
149
     *
150
     * @return string $key
151
     */
152
    function createAuthKey()
153
    {
154
        if (!$this->active) {
155
            trigger_error("FileStore no longer active", E_USER_ERROR);
156
            return null;
157
        }
158
 
159
        $auth_key = Auth_OpenID_CryptUtil::randomString($this->AUTH_KEY_LEN);
160
 
161
        list($file_obj, $tmp) = $this->_mktemp();
162
 
163
        fwrite($file_obj, $auth_key);
164
        fflush($file_obj);
165
        fclose($file_obj);
166
 
167
        if (function_exists('link')) {
168
            // Posix filesystem
169
            $saved = link($tmp, $this->auth_key_name);
170
            Auth_OpenID_FileStore::_removeIfPresent($tmp);
171
        } else {
172
            // Windows filesystem
173
            $saved = rename($tmp, $this->auth_key_name);
174
        }
175
 
176
        if (!$saved) {
177
            // The link failed, either because we lack the permission,
178
            // or because the file already exists; try to read the key
179
            // in case the file already existed.
180
            $auth_key = $this->readAuthKey();
181
        }
182
 
183
        return $auth_key;
184
    }
185
 
186
    /**
187
     * Retrieve the auth key from the file specified by
188
     * $this->auth_key_name, creating it if it does not exist.
189
     *
190
     * @return string $key
191
     */
192
    function getAuthKey()
193
    {
194
        if (!$this->active) {
195
            trigger_error("FileStore no longer active", E_USER_ERROR);
196
            return null;
197
        }
198
 
199
        $auth_key = $this->readAuthKey();
200
        if ($auth_key === null) {
201
            $auth_key = $this->createAuthKey();
202
 
203
            if (strlen($auth_key) != $this->AUTH_KEY_LEN) {
204
                $fmt = 'Got an invalid auth key from %s. Expected '.
205
                    '%d-byte string. Got: %s';
206
                $msg = sprintf($fmt, $this->auth_key_name, $this->AUTH_KEY_LEN,
207
                               $auth_key);
208
                trigger_error($msg, E_USER_WARNING);
209
                return null;
210
            }
211
        }
212
        return $auth_key;
213
    }
214
 
215
    /**
216
     * Create a unique filename for a given server url and
217
     * handle. This implementation does not assume anything about the
218
     * format of the handle. The filename that is returned will
219
     * contain the domain name from the server URL for ease of human
220
     * inspection of the data directory.
221
     *
222
     * @return string $filename
223
     */
224
    function getAssociationFilename($server_url, $handle)
225
    {
226
        if (!$this->active) {
227
            trigger_error("FileStore no longer active", E_USER_ERROR);
228
            return null;
229
        }
230
 
231
        if (strpos($server_url, '://') === false) {
232
            trigger_error(sprintf("Bad server URL: %s", $server_url),
233
                          E_USER_WARNING);
234
            return null;
235
        }
236
 
237
        list($proto, $rest) = explode('://', $server_url, 2);
238
        $parts = explode('/', $rest);
239
        $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]);
240
        $url_hash = Auth_OpenID_FileStore::_safe64($server_url);
241
        if ($handle) {
242
            $handle_hash = Auth_OpenID_FileStore::_safe64($handle);
243
        } else {
244
            $handle_hash = '';
245
        }
246
 
247
        $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash,
248
                            $handle_hash);
249
 
250
        return $this->association_dir. DIRECTORY_SEPARATOR . $filename;
251
    }
252
 
253
    /**
254
     * Store an association in the association directory.
255
     */
256
    function storeAssociation($server_url, $association)
257
    {
258
        if (!$this->active) {
259
            trigger_error("FileStore no longer active", E_USER_ERROR);
260
            return false;
261
        }
262
 
263
        $association_s = $association->serialize();
264
        $filename = $this->getAssociationFilename($server_url,
265
                                                  $association->handle);
266
        list($tmp_file, $tmp) = $this->_mktemp();
267
 
268
        if (!$tmp_file) {
269
            trigger_error("_mktemp didn't return a valid file descriptor",
270
                          E_USER_WARNING);
271
            return false;
272
        }
273
 
274
        fwrite($tmp_file, $association_s);
275
 
276
        fflush($tmp_file);
277
 
278
        fclose($tmp_file);
279
 
280
        if (@rename($tmp, $filename)) {
281
            return true;
282
        } else {
283
            // In case we are running on Windows, try unlinking the
284
            // file in case it exists.
285
            @unlink($filename);
286
 
287
            // Now the target should not exist. Try renaming again,
288
            // giving up if it fails.
289
            if (@rename($tmp, $filename)) {
290
                return true;
291
            }
292
        }
293
 
294
        // If there was an error, don't leave the temporary file
295
        // around.
296
        Auth_OpenID_FileStore::_removeIfPresent($tmp);
297
        return false;
298
    }
299
 
300
    /**
301
     * Retrieve an association. If no handle is specified, return the
302
     * association with the most recent issue time.
303
     *
304
     * @return mixed $association
305
     */
306
    function getAssociation($server_url, $handle = null)
307
    {
308
        if (!$this->active) {
309
            trigger_error("FileStore no longer active", E_USER_ERROR);
310
            return null;
311
        }
312
 
313
        if ($handle === null) {
314
            $handle = '';
315
        }
316
 
317
        // The filename with the empty handle is a prefix of all other
318
        // associations for the given server URL.
319
        $filename = $this->getAssociationFilename($server_url, $handle);
320
 
321
        if ($handle) {
322
            return $this->_getAssociation($filename);
323
        } else {
324
            $association_files =
325
                Auth_OpenID_FileStore::_listdir($this->association_dir);
326
            $matching_files = array();
327
 
328
            // strip off the path to do the comparison
329
            $name = basename($filename);
330
            foreach ($association_files as $association_file) {
331
                if (strpos($association_file, $name) === 0) {
332
                    $matching_files[] = $association_file;
333
                }
334
            }
335
 
336
            $matching_associations = array();
337
            // read the matching files and sort by time issued
338
            foreach ($matching_files as $name) {
339
                $full_name = $this->association_dir . DIRECTORY_SEPARATOR .
340
                    $name;
341
                $association = $this->_getAssociation($full_name);
342
                if ($association !== null) {
343
                    $matching_associations[] = array($association->issued,
344
                                                     $association);
345
                }
346
            }
347
 
348
            $issued = array();
349
            $assocs = array();
350
            foreach ($matching_associations as $key => $assoc) {
351
                $issued[$key] = $assoc[0];
352
                $assocs[$key] = $assoc[1];
353
            }
354
 
355
            array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
356
                            $matching_associations);
357
 
358
            // return the most recently issued one.
359
            if ($matching_associations) {
360
                list($issued, $assoc) = $matching_associations[0];
361
                return $assoc;
362
            } else {
363
                return null;
364
            }
365
        }
366
    }
367
 
368
    /**
369
     * @access private
370
     */
371
    function _getAssociation($filename)
372
    {
373
        if (!$this->active) {
374
            trigger_error("FileStore no longer active", E_USER_ERROR);
375
            return null;
376
        }
377
 
378
        $assoc_file = @fopen($filename, 'rb');
379
 
380
        if ($assoc_file === false) {
381
            return null;
382
        }
383
 
384
        $assoc_s = fread($assoc_file, filesize($filename));
385
        fclose($assoc_file);
386
 
387
        if (!$assoc_s) {
388
            return null;
389
        }
390
 
391
        $association =
392
            Auth_OpenID_Association::deserialize('Auth_OpenID_Association',
393
                                                $assoc_s);
394
 
395
        if (!$association) {
396
            Auth_OpenID_FileStore::_removeIfPresent($filename);
397
            return null;
398
        }
399
 
400
        if ($association->getExpiresIn() == 0) {
401
            Auth_OpenID_FileStore::_removeIfPresent($filename);
402
            return null;
403
        } else {
404
            return $association;
405
        }
406
    }
407
 
408
    /**
409
     * Remove an association if it exists. Do nothing if it does not.
410
     *
411
     * @return bool $success
412
     */
413
    function removeAssociation($server_url, $handle)
414
    {
415
        if (!$this->active) {
416
            trigger_error("FileStore no longer active", E_USER_ERROR);
417
            return null;
418
        }
419
 
420
        $assoc = $this->getAssociation($server_url, $handle);
421
        if ($assoc === null) {
422
            return false;
423
        } else {
424
            $filename = $this->getAssociationFilename($server_url, $handle);
425
            return Auth_OpenID_FileStore::_removeIfPresent($filename);
426
        }
427
    }
428
 
429
    /**
430
     * Mark this nonce as present.
431
     */
432
    function storeNonce($nonce)
433
    {
434
        if (!$this->active) {
435
            trigger_error("FileStore no longer active", E_USER_ERROR);
436
            return null;
437
        }
438
 
439
        $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
440
        $nonce_file = fopen($filename, 'w');
441
        if ($nonce_file === false) {
442
            return false;
443
        }
444
        fclose($nonce_file);
445
        return true;
446
    }
447
 
448
    /**
449
     * Return whether this nonce is present. As a side effect, mark it
450
     * as no longer present.
451
     *
452
     * @return bool $present
453
     */
454
    function useNonce($nonce)
455
    {
456
        if (!$this->active) {
457
            trigger_error("FileStore no longer active", E_USER_ERROR);
458
            return null;
459
        }
460
 
461
        $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
462
        $st = @stat($filename);
463
 
464
        if ($st === false) {
465
            return false;
466
        }
467
 
468
        // Either it is too old or we are using it. Either way, we
469
        // must remove the file.
470
        if (!unlink($filename)) {
471
            return false;
472
        }
473
 
474
        $now = time();
475
        $nonce_age = $now - $st[9];
476
 
477
        // We can us it if the age of the file is less than the
478
        // expiration time.
479
        return $nonce_age <= $this->max_nonce_age;
480
    }
481
 
482
    /**
483
     * Remove expired entries from the database. This is potentially
484
     * expensive, so only run when it is acceptable to take time.
485
     */
486
    function clean()
487
    {
488
        if (!$this->active) {
489
            trigger_error("FileStore no longer active", E_USER_ERROR);
490
            return null;
491
        }
492
 
493
        $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
494
        $now = time();
495
 
496
        // Check all nonces for expiry
497
        foreach ($nonces as $nonce) {
498
            $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
499
            $st = @stat($filename);
500
 
501
            if ($st !== false) {
502
                // Remove the nonce if it has expired
503
                $nonce_age = $now - $st[9];
504
                if ($nonce_age > $this->max_nonce_age) {
505
                    Auth_OpenID_FileStore::_removeIfPresent($filename);
506
                }
507
            }
508
        }
509
 
510
        $association_filenames =
511
            Auth_OpenID_FileStore::_listdir($this->association_dir);
512
 
513
        foreach ($association_filenames as $association_filename) {
514
            $association_file = fopen($association_filename, 'rb');
515
 
516
            if ($association_file !== false) {
517
                $assoc_s = fread($association_file,
518
                                 filesize($association_filename));
519
                fclose($association_file);
520
 
521
                // Remove expired or corrupted associations
522
                $association =
523
                  Auth_OpenID_Association::deserialize(
524
                         'Auth_OpenID_Association', $assoc_s);
525
 
526
                if ($association === null) {
527
                    Auth_OpenID_FileStore::_removeIfPresent(
528
                                                 $association_filename);
529
                } else {
530
                    if ($association->getExpiresIn() == 0) {
531
                        Auth_OpenID_FileStore::_removeIfPresent(
532
                                                 $association_filename);
533
                    }
534
                }
535
            }
536
        }
537
    }
538
 
539
    /**
540
     * @access private
541
     */
542
    function _rmtree($dir)
543
    {
544
        if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) {
545
            $dir .= DIRECTORY_SEPARATOR;
546
        }
547
 
548
        if ($handle = opendir($dir)) {
549
            while ($item = readdir($handle)) {
550
                if (!in_array($item, array('.', '..'))) {
551
                    if (is_dir($dir . $item)) {
552
 
553
                        if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) {
554
                            return false;
555
                        }
556
                    } else if (is_file($dir . $item)) {
557
                        if (!unlink($dir . $item)) {
558
                            return false;
559
                        }
560
                    }
561
                }
562
            }
563
 
564
            closedir($handle);
565
 
566
            if (!@rmdir($dir)) {
567
                return false;
568
            }
569
 
570
            return true;
571
        } else {
572
            // Couldn't open directory.
573
            return false;
574
        }
575
    }
576
 
577
    /**
578
     * @access private
579
     */
580
    function _mkstemp($dir)
581
    {
582
        foreach (range(0, 4) as $i) {
583
            $name = tempnam($dir, "php_openid_filestore_");
584
 
585
            if ($name !== false) {
586
                return $name;
587
            }
588
        }
589
        return false;
590
    }
591
 
592
    /**
593
     * @access private
594
     */
595
    function _mkdtemp($dir)
596
    {
597
        foreach (range(0, 4) as $i) {
598
            $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) .
599
                "-" . strval(rand(1, time()));
600
            if (!mkdir($name, 0700)) {
601
                return false;
602
            } else {
603
                return $name;
604
            }
605
        }
606
        return false;
607
    }
608
 
609
    /**
610
     * @access private
611
     */
612
    function _listdir($dir)
613
    {
614
        $handle = opendir($dir);
615
        $files = array();
616
        while (false !== ($filename = readdir($handle))) {
617
            $files[] = $filename;
618
        }
619
        return $files;
620
    }
621
 
622
    /**
623
     * @access private
624
     */
625
    function _isFilenameSafe($char)
626
    {
627
        $_Auth_OpenID_filename_allowed = Auth_OpenID_letters .
628
            Auth_OpenID_digits . ".";
629
        return (strpos($_Auth_OpenID_filename_allowed, $char) !== false);
630
    }
631
 
632
    /**
633
     * @access private
634
     */
635
    function _safe64($str)
636
    {
637
        $h64 = base64_encode(Auth_OpenID_SHA1($str));
638
        $h64 = str_replace('+', '_', $h64);
639
        $h64 = str_replace('/', '.', $h64);
640
        $h64 = str_replace('=', '', $h64);
641
        return $h64;
642
    }
643
 
644
    /**
645
     * @access private
646
     */
647
    function _filenameEscape($str)
648
    {
649
        $filename = "";
650
        for ($i = 0; $i < strlen($str); $i++) {
651
            $c = $str[$i];
652
            if (Auth_OpenID_FileStore::_isFilenameSafe($c)) {
653
                $filename .= $c;
654
            } else {
655
                $filename .= sprintf("_%02X", ord($c));
656
            }
657
        }
658
        return $filename;
659
    }
660
 
661
    /**
662
     * Attempt to remove a file, returning whether the file existed at
663
     * the time of the call.
664
     *
665
     * @access private
666
     * @return bool $result True if the file was present, false if not.
667
     */
668
    function _removeIfPresent($filename)
669
    {
670
        return @unlink($filename);
671
    }
672
}
673
 
674
?>