MemoryCacheBackend.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
  5. *
  6. * @author Joas Schilling <coding@schilljs.com>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OC\Security\Bruteforce\Backend;
  25. use OCP\AppFramework\Utility\ITimeFactory;
  26. use OCP\ICache;
  27. use OCP\ICacheFactory;
  28. class MemoryCacheBackend implements IBackend {
  29. private ICache $cache;
  30. public function __construct(
  31. ICacheFactory $cacheFactory,
  32. private ITimeFactory $timeFactory,
  33. ) {
  34. $this->cache = $cacheFactory->createDistributed(__CLASS__);
  35. }
  36. private function hash(
  37. null|string|array $data,
  38. ): ?string {
  39. if ($data === null) {
  40. return null;
  41. }
  42. if (!is_string($data)) {
  43. $data = json_encode($data);
  44. }
  45. return hash('sha1', $data);
  46. }
  47. private function getExistingAttempts(string $identifier): array {
  48. $cachedAttempts = $this->cache->get($identifier);
  49. if ($cachedAttempts === null) {
  50. return [];
  51. }
  52. $cachedAttempts = json_decode($cachedAttempts, true);
  53. if (\is_array($cachedAttempts)) {
  54. return $cachedAttempts;
  55. }
  56. return [];
  57. }
  58. /**
  59. * {@inheritDoc}
  60. */
  61. public function getAttempts(
  62. string $ipSubnet,
  63. int $maxAgeTimestamp,
  64. ?string $action = null,
  65. ?array $metadata = null,
  66. ): int {
  67. $identifier = $this->hash($ipSubnet);
  68. $actionHash = $this->hash($action);
  69. $metadataHash = $this->hash($metadata);
  70. $existingAttempts = $this->getExistingAttempts($identifier);
  71. $count = 0;
  72. foreach ($existingAttempts as $info) {
  73. [$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
  74. if ($action === null || $attemptAction === $actionHash) {
  75. if ($metadata === null || $attemptMetadata === $metadataHash) {
  76. if ($occurredTime > $maxAgeTimestamp) {
  77. $count++;
  78. }
  79. }
  80. }
  81. }
  82. return $count;
  83. }
  84. /**
  85. * {@inheritDoc}
  86. */
  87. public function resetAttempts(
  88. string $ipSubnet,
  89. ?string $action = null,
  90. ?array $metadata = null,
  91. ): void {
  92. $identifier = $this->hash($ipSubnet);
  93. if ($action === null) {
  94. $this->cache->remove($identifier);
  95. } else {
  96. $actionHash = $this->hash($action);
  97. $metadataHash = $this->hash($metadata);
  98. $existingAttempts = $this->getExistingAttempts($identifier);
  99. $maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
  100. foreach ($existingAttempts as $key => $info) {
  101. [$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
  102. if ($attemptAction === $actionHash) {
  103. if ($metadata === null || $attemptMetadata === $metadataHash) {
  104. unset($existingAttempts[$key]);
  105. } elseif ($occurredTime < $maxAgeTimestamp) {
  106. unset($existingAttempts[$key]);
  107. }
  108. }
  109. }
  110. if (!empty($existingAttempts)) {
  111. $this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
  112. } else {
  113. $this->cache->remove($identifier);
  114. }
  115. }
  116. }
  117. /**
  118. * {@inheritDoc}
  119. */
  120. public function registerAttempt(
  121. string $ip,
  122. string $ipSubnet,
  123. int $timestamp,
  124. string $action,
  125. array $metadata = [],
  126. ): void {
  127. $identifier = $this->hash($ipSubnet);
  128. $existingAttempts = $this->getExistingAttempts($identifier);
  129. $maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
  130. // Unset all attempts that are already expired
  131. foreach ($existingAttempts as $key => $info) {
  132. [$occurredTime,] = explode('#', $info, 3);
  133. if ($occurredTime < $maxAgeTimestamp) {
  134. unset($existingAttempts[$key]);
  135. }
  136. }
  137. $existingAttempts = array_values($existingAttempts);
  138. // Store the new attempt
  139. $existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata);
  140. $this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
  141. }
  142. }