PageRenderTime 53ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/pacore/ext/Auth/OpenID/FileStore.php

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