MemoryCacheBackend.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Security\Bruteforce\Backend;
  8. use OCP\AppFramework\Utility\ITimeFactory;
  9. use OCP\ICache;
  10. use OCP\ICacheFactory;
  11. class MemoryCacheBackend implements IBackend {
  12. private ICache $cache;
  13. public function __construct(
  14. ICacheFactory $cacheFactory,
  15. private ITimeFactory $timeFactory,
  16. ) {
  17. $this->cache = $cacheFactory->createDistributed(self::class);
  18. }
  19. private function hash(
  20. null|string|array $data,
  21. ): ?string {
  22. if ($data === null) {
  23. return null;
  24. }
  25. if (!is_string($data)) {
  26. $data = json_encode($data);
  27. }
  28. return hash('sha1', $data);
  29. }
  30. private function getExistingAttempts(string $identifier): array {
  31. $cachedAttempts = $this->cache->get($identifier);
  32. if ($cachedAttempts === null) {
  33. return [];
  34. }
  35. $cachedAttempts = json_decode($cachedAttempts, true);
  36. if (\is_array($cachedAttempts)) {
  37. return $cachedAttempts;
  38. }
  39. return [];
  40. }
  41. /**
  42. * {@inheritDoc}
  43. */
  44. public function getAttempts(
  45. string $ipSubnet,
  46. int $maxAgeTimestamp,
  47. ?string $action = null,
  48. ?array $metadata = null,
  49. ): int {
  50. $identifier = $this->hash($ipSubnet);
  51. $actionHash = $this->hash($action);
  52. $metadataHash = $this->hash($metadata);
  53. $existingAttempts = $this->getExistingAttempts($identifier);
  54. $count = 0;
  55. foreach ($existingAttempts as $info) {
  56. [$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
  57. if ($action === null || $attemptAction === $actionHash) {
  58. if ($metadata === null || $attemptMetadata === $metadataHash) {
  59. if ($occurredTime > $maxAgeTimestamp) {
  60. $count++;
  61. }
  62. }
  63. }
  64. }
  65. return $count;
  66. }
  67. /**
  68. * {@inheritDoc}
  69. */
  70. public function resetAttempts(
  71. string $ipSubnet,
  72. ?string $action = null,
  73. ?array $metadata = null,
  74. ): void {
  75. $identifier = $this->hash($ipSubnet);
  76. if ($action === null) {
  77. $this->cache->remove($identifier);
  78. } else {
  79. $actionHash = $this->hash($action);
  80. $metadataHash = $this->hash($metadata);
  81. $existingAttempts = $this->getExistingAttempts($identifier);
  82. $maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
  83. foreach ($existingAttempts as $key => $info) {
  84. [$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
  85. if ($attemptAction === $actionHash) {
  86. if ($metadata === null || $attemptMetadata === $metadataHash) {
  87. unset($existingAttempts[$key]);
  88. } elseif ($occurredTime < $maxAgeTimestamp) {
  89. unset($existingAttempts[$key]);
  90. }
  91. }
  92. }
  93. if (!empty($existingAttempts)) {
  94. $this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
  95. } else {
  96. $this->cache->remove($identifier);
  97. }
  98. }
  99. }
  100. /**
  101. * {@inheritDoc}
  102. */
  103. public function registerAttempt(
  104. string $ip,
  105. string $ipSubnet,
  106. int $timestamp,
  107. string $action,
  108. array $metadata = [],
  109. ): void {
  110. $identifier = $this->hash($ipSubnet);
  111. $existingAttempts = $this->getExistingAttempts($identifier);
  112. $maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
  113. // Unset all attempts that are already expired
  114. foreach ($existingAttempts as $key => $info) {
  115. [$occurredTime,] = explode('#', $info, 3);
  116. if ($occurredTime < $maxAgeTimestamp) {
  117. unset($existingAttempts[$key]);
  118. }
  119. }
  120. $existingAttempts = array_values($existingAttempts);
  121. // Store the new attempt
  122. $existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata);
  123. $this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
  124. }
  125. }