@@ -31,204 +31,204 @@ |
||
| 31 | 31 | * @package OC\Security\Bruteforce |
| 32 | 32 | */ |
| 33 | 33 | class Throttler implements IThrottler { |
| 34 | - /** @var bool[] */ |
|
| 35 | - private array $hasAttemptsDeleted = []; |
|
| 36 | - |
|
| 37 | - public function __construct( |
|
| 38 | - private ITimeFactory $timeFactory, |
|
| 39 | - private LoggerInterface $logger, |
|
| 40 | - private IConfig $config, |
|
| 41 | - private IBackend $backend, |
|
| 42 | - private BruteforceAllowList $allowList, |
|
| 43 | - ) { |
|
| 44 | - } |
|
| 45 | - |
|
| 46 | - /** |
|
| 47 | - * {@inheritDoc} |
|
| 48 | - */ |
|
| 49 | - public function registerAttempt(string $action, |
|
| 50 | - string $ip, |
|
| 51 | - array $metadata = []): void { |
|
| 52 | - // No need to log if the bruteforce protection is disabled |
|
| 53 | - if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { |
|
| 54 | - return; |
|
| 55 | - } |
|
| 56 | - |
|
| 57 | - $ipAddress = new IpAddress($ip); |
|
| 58 | - if ($this->isBypassListed((string)$ipAddress)) { |
|
| 59 | - return; |
|
| 60 | - } |
|
| 61 | - |
|
| 62 | - $this->logger->notice( |
|
| 63 | - sprintf( |
|
| 64 | - 'Bruteforce attempt from "%s" detected for action "%s".', |
|
| 65 | - $ip, |
|
| 66 | - $action |
|
| 67 | - ), |
|
| 68 | - [ |
|
| 69 | - 'app' => 'core', |
|
| 70 | - ] |
|
| 71 | - ); |
|
| 72 | - |
|
| 73 | - $this->backend->registerAttempt( |
|
| 74 | - (string)$ipAddress, |
|
| 75 | - $ipAddress->getSubnet(), |
|
| 76 | - $this->timeFactory->getTime(), |
|
| 77 | - $action, |
|
| 78 | - $metadata |
|
| 79 | - ); |
|
| 80 | - } |
|
| 81 | - |
|
| 82 | - /** |
|
| 83 | - * Check if the IP is whitelisted |
|
| 84 | - */ |
|
| 85 | - public function isBypassListed(string $ip): bool { |
|
| 86 | - return $this->allowList->isBypassListed($ip); |
|
| 87 | - } |
|
| 88 | - |
|
| 89 | - /** |
|
| 90 | - * {@inheritDoc} |
|
| 91 | - */ |
|
| 92 | - public function showBruteforceWarning(string $ip, string $action = ''): bool { |
|
| 93 | - $attempts = $this->getAttempts($ip, $action); |
|
| 94 | - // 4 failed attempts is the last delay below 5 seconds |
|
| 95 | - return $attempts >= 4; |
|
| 96 | - } |
|
| 97 | - |
|
| 98 | - /** |
|
| 99 | - * {@inheritDoc} |
|
| 100 | - */ |
|
| 101 | - public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int { |
|
| 102 | - if ($maxAgeHours > 48) { |
|
| 103 | - $this->logger->error('Bruteforce has to use less than 48 hours'); |
|
| 104 | - $maxAgeHours = 48; |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) { |
|
| 108 | - return 0; |
|
| 109 | - } |
|
| 110 | - |
|
| 111 | - $ipAddress = new IpAddress($ip); |
|
| 112 | - if ($this->isBypassListed((string)$ipAddress)) { |
|
| 113 | - return 0; |
|
| 114 | - } |
|
| 115 | - |
|
| 116 | - $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours); |
|
| 117 | - |
|
| 118 | - return $this->backend->getAttempts( |
|
| 119 | - $ipAddress->getSubnet(), |
|
| 120 | - $maxAgeTimestamp, |
|
| 121 | - $action !== '' ? $action : null, |
|
| 122 | - ); |
|
| 123 | - } |
|
| 124 | - |
|
| 125 | - /** |
|
| 126 | - * {@inheritDoc} |
|
| 127 | - */ |
|
| 128 | - public function getDelay(string $ip, string $action = ''): int { |
|
| 129 | - $attempts = $this->getAttempts($ip, $action); |
|
| 130 | - return $this->calculateDelay($attempts); |
|
| 131 | - } |
|
| 132 | - |
|
| 133 | - /** |
|
| 134 | - * {@inheritDoc} |
|
| 135 | - */ |
|
| 136 | - public function calculateDelay(int $attempts): int { |
|
| 137 | - if ($attempts === 0) { |
|
| 138 | - return 0; |
|
| 139 | - } |
|
| 140 | - |
|
| 141 | - $firstDelay = 0.1; |
|
| 142 | - if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) { |
|
| 143 | - // Don't ever overflow. Just assume the maxDelay time:s |
|
| 144 | - return self::MAX_DELAY_MS; |
|
| 145 | - } |
|
| 146 | - |
|
| 147 | - $delay = $firstDelay * 2 ** $attempts; |
|
| 148 | - if ($delay > self::MAX_DELAY) { |
|
| 149 | - return self::MAX_DELAY_MS; |
|
| 150 | - } |
|
| 151 | - return (int)\ceil($delay * 1000); |
|
| 152 | - } |
|
| 153 | - |
|
| 154 | - /** |
|
| 155 | - * {@inheritDoc} |
|
| 156 | - */ |
|
| 157 | - public function resetDelay(string $ip, string $action, array $metadata): void { |
|
| 158 | - // No need to log if the bruteforce protection is disabled |
|
| 159 | - if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { |
|
| 160 | - return; |
|
| 161 | - } |
|
| 162 | - |
|
| 163 | - $ipAddress = new IpAddress($ip); |
|
| 164 | - if ($this->isBypassListed((string)$ipAddress)) { |
|
| 165 | - return; |
|
| 166 | - } |
|
| 167 | - |
|
| 168 | - $this->backend->resetAttempts( |
|
| 169 | - $ipAddress->getSubnet(), |
|
| 170 | - $action, |
|
| 171 | - $metadata, |
|
| 172 | - ); |
|
| 173 | - |
|
| 174 | - $this->hasAttemptsDeleted[$action] = true; |
|
| 175 | - } |
|
| 176 | - |
|
| 177 | - /** |
|
| 178 | - * {@inheritDoc} |
|
| 179 | - */ |
|
| 180 | - public function resetDelayForIP(string $ip): void { |
|
| 181 | - // No need to log if the bruteforce protection is disabled |
|
| 182 | - if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { |
|
| 183 | - return; |
|
| 184 | - } |
|
| 185 | - |
|
| 186 | - $ipAddress = new IpAddress($ip); |
|
| 187 | - if ($this->isBypassListed((string)$ipAddress)) { |
|
| 188 | - return; |
|
| 189 | - } |
|
| 190 | - |
|
| 191 | - $this->backend->resetAttempts($ipAddress->getSubnet()); |
|
| 192 | - } |
|
| 193 | - |
|
| 194 | - /** |
|
| 195 | - * {@inheritDoc} |
|
| 196 | - */ |
|
| 197 | - public function sleepDelay(string $ip, string $action = ''): int { |
|
| 198 | - $delay = $this->getDelay($ip, $action); |
|
| 199 | - if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) { |
|
| 200 | - usleep($delay * 1000); |
|
| 201 | - } |
|
| 202 | - return $delay; |
|
| 203 | - } |
|
| 204 | - |
|
| 205 | - /** |
|
| 206 | - * {@inheritDoc} |
|
| 207 | - */ |
|
| 208 | - public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int { |
|
| 209 | - $attempts = $this->getAttempts($ip, $action, 0.5); |
|
| 210 | - if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) { |
|
| 211 | - $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, attempts: {attempts}, ip: {ip}]', [ |
|
| 212 | - 'action' => $action, |
|
| 213 | - 'ip' => $ip, |
|
| 214 | - 'attempts' => $attempts, |
|
| 215 | - ]); |
|
| 216 | - // If the ip made too many attempts within the last 30 mins we don't execute anymore |
|
| 217 | - throw new MaxDelayReached('Reached maximum delay'); |
|
| 218 | - } |
|
| 219 | - |
|
| 220 | - $attempts = $this->getAttempts($ip, $action); |
|
| 221 | - if ($attempts > 10) { |
|
| 222 | - $this->logger->info('IP address throttled because it reached the attempts limit in the last 12 hours [action: {action}, attempts: {attempts}, ip: {ip}]', [ |
|
| 223 | - 'action' => $action, |
|
| 224 | - 'ip' => $ip, |
|
| 225 | - 'attempts' => $attempts, |
|
| 226 | - ]); |
|
| 227 | - } |
|
| 228 | - if ($attempts > 0) { |
|
| 229 | - return $this->calculateDelay($attempts); |
|
| 230 | - } |
|
| 231 | - |
|
| 232 | - return 0; |
|
| 233 | - } |
|
| 34 | + /** @var bool[] */ |
|
| 35 | + private array $hasAttemptsDeleted = []; |
|
| 36 | + |
|
| 37 | + public function __construct( |
|
| 38 | + private ITimeFactory $timeFactory, |
|
| 39 | + private LoggerInterface $logger, |
|
| 40 | + private IConfig $config, |
|
| 41 | + private IBackend $backend, |
|
| 42 | + private BruteforceAllowList $allowList, |
|
| 43 | + ) { |
|
| 44 | + } |
|
| 45 | + |
|
| 46 | + /** |
|
| 47 | + * {@inheritDoc} |
|
| 48 | + */ |
|
| 49 | + public function registerAttempt(string $action, |
|
| 50 | + string $ip, |
|
| 51 | + array $metadata = []): void { |
|
| 52 | + // No need to log if the bruteforce protection is disabled |
|
| 53 | + if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { |
|
| 54 | + return; |
|
| 55 | + } |
|
| 56 | + |
|
| 57 | + $ipAddress = new IpAddress($ip); |
|
| 58 | + if ($this->isBypassListed((string)$ipAddress)) { |
|
| 59 | + return; |
|
| 60 | + } |
|
| 61 | + |
|
| 62 | + $this->logger->notice( |
|
| 63 | + sprintf( |
|
| 64 | + 'Bruteforce attempt from "%s" detected for action "%s".', |
|
| 65 | + $ip, |
|
| 66 | + $action |
|
| 67 | + ), |
|
| 68 | + [ |
|
| 69 | + 'app' => 'core', |
|
| 70 | + ] |
|
| 71 | + ); |
|
| 72 | + |
|
| 73 | + $this->backend->registerAttempt( |
|
| 74 | + (string)$ipAddress, |
|
| 75 | + $ipAddress->getSubnet(), |
|
| 76 | + $this->timeFactory->getTime(), |
|
| 77 | + $action, |
|
| 78 | + $metadata |
|
| 79 | + ); |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + /** |
|
| 83 | + * Check if the IP is whitelisted |
|
| 84 | + */ |
|
| 85 | + public function isBypassListed(string $ip): bool { |
|
| 86 | + return $this->allowList->isBypassListed($ip); |
|
| 87 | + } |
|
| 88 | + |
|
| 89 | + /** |
|
| 90 | + * {@inheritDoc} |
|
| 91 | + */ |
|
| 92 | + public function showBruteforceWarning(string $ip, string $action = ''): bool { |
|
| 93 | + $attempts = $this->getAttempts($ip, $action); |
|
| 94 | + // 4 failed attempts is the last delay below 5 seconds |
|
| 95 | + return $attempts >= 4; |
|
| 96 | + } |
|
| 97 | + |
|
| 98 | + /** |
|
| 99 | + * {@inheritDoc} |
|
| 100 | + */ |
|
| 101 | + public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int { |
|
| 102 | + if ($maxAgeHours > 48) { |
|
| 103 | + $this->logger->error('Bruteforce has to use less than 48 hours'); |
|
| 104 | + $maxAgeHours = 48; |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) { |
|
| 108 | + return 0; |
|
| 109 | + } |
|
| 110 | + |
|
| 111 | + $ipAddress = new IpAddress($ip); |
|
| 112 | + if ($this->isBypassListed((string)$ipAddress)) { |
|
| 113 | + return 0; |
|
| 114 | + } |
|
| 115 | + |
|
| 116 | + $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours); |
|
| 117 | + |
|
| 118 | + return $this->backend->getAttempts( |
|
| 119 | + $ipAddress->getSubnet(), |
|
| 120 | + $maxAgeTimestamp, |
|
| 121 | + $action !== '' ? $action : null, |
|
| 122 | + ); |
|
| 123 | + } |
|
| 124 | + |
|
| 125 | + /** |
|
| 126 | + * {@inheritDoc} |
|
| 127 | + */ |
|
| 128 | + public function getDelay(string $ip, string $action = ''): int { |
|
| 129 | + $attempts = $this->getAttempts($ip, $action); |
|
| 130 | + return $this->calculateDelay($attempts); |
|
| 131 | + } |
|
| 132 | + |
|
| 133 | + /** |
|
| 134 | + * {@inheritDoc} |
|
| 135 | + */ |
|
| 136 | + public function calculateDelay(int $attempts): int { |
|
| 137 | + if ($attempts === 0) { |
|
| 138 | + return 0; |
|
| 139 | + } |
|
| 140 | + |
|
| 141 | + $firstDelay = 0.1; |
|
| 142 | + if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) { |
|
| 143 | + // Don't ever overflow. Just assume the maxDelay time:s |
|
| 144 | + return self::MAX_DELAY_MS; |
|
| 145 | + } |
|
| 146 | + |
|
| 147 | + $delay = $firstDelay * 2 ** $attempts; |
|
| 148 | + if ($delay > self::MAX_DELAY) { |
|
| 149 | + return self::MAX_DELAY_MS; |
|
| 150 | + } |
|
| 151 | + return (int)\ceil($delay * 1000); |
|
| 152 | + } |
|
| 153 | + |
|
| 154 | + /** |
|
| 155 | + * {@inheritDoc} |
|
| 156 | + */ |
|
| 157 | + public function resetDelay(string $ip, string $action, array $metadata): void { |
|
| 158 | + // No need to log if the bruteforce protection is disabled |
|
| 159 | + if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { |
|
| 160 | + return; |
|
| 161 | + } |
|
| 162 | + |
|
| 163 | + $ipAddress = new IpAddress($ip); |
|
| 164 | + if ($this->isBypassListed((string)$ipAddress)) { |
|
| 165 | + return; |
|
| 166 | + } |
|
| 167 | + |
|
| 168 | + $this->backend->resetAttempts( |
|
| 169 | + $ipAddress->getSubnet(), |
|
| 170 | + $action, |
|
| 171 | + $metadata, |
|
| 172 | + ); |
|
| 173 | + |
|
| 174 | + $this->hasAttemptsDeleted[$action] = true; |
|
| 175 | + } |
|
| 176 | + |
|
| 177 | + /** |
|
| 178 | + * {@inheritDoc} |
|
| 179 | + */ |
|
| 180 | + public function resetDelayForIP(string $ip): void { |
|
| 181 | + // No need to log if the bruteforce protection is disabled |
|
| 182 | + if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) { |
|
| 183 | + return; |
|
| 184 | + } |
|
| 185 | + |
|
| 186 | + $ipAddress = new IpAddress($ip); |
|
| 187 | + if ($this->isBypassListed((string)$ipAddress)) { |
|
| 188 | + return; |
|
| 189 | + } |
|
| 190 | + |
|
| 191 | + $this->backend->resetAttempts($ipAddress->getSubnet()); |
|
| 192 | + } |
|
| 193 | + |
|
| 194 | + /** |
|
| 195 | + * {@inheritDoc} |
|
| 196 | + */ |
|
| 197 | + public function sleepDelay(string $ip, string $action = ''): int { |
|
| 198 | + $delay = $this->getDelay($ip, $action); |
|
| 199 | + if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) { |
|
| 200 | + usleep($delay * 1000); |
|
| 201 | + } |
|
| 202 | + return $delay; |
|
| 203 | + } |
|
| 204 | + |
|
| 205 | + /** |
|
| 206 | + * {@inheritDoc} |
|
| 207 | + */ |
|
| 208 | + public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int { |
|
| 209 | + $attempts = $this->getAttempts($ip, $action, 0.5); |
|
| 210 | + if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) { |
|
| 211 | + $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, attempts: {attempts}, ip: {ip}]', [ |
|
| 212 | + 'action' => $action, |
|
| 213 | + 'ip' => $ip, |
|
| 214 | + 'attempts' => $attempts, |
|
| 215 | + ]); |
|
| 216 | + // If the ip made too many attempts within the last 30 mins we don't execute anymore |
|
| 217 | + throw new MaxDelayReached('Reached maximum delay'); |
|
| 218 | + } |
|
| 219 | + |
|
| 220 | + $attempts = $this->getAttempts($ip, $action); |
|
| 221 | + if ($attempts > 10) { |
|
| 222 | + $this->logger->info('IP address throttled because it reached the attempts limit in the last 12 hours [action: {action}, attempts: {attempts}, ip: {ip}]', [ |
|
| 223 | + 'action' => $action, |
|
| 224 | + 'ip' => $ip, |
|
| 225 | + 'attempts' => $attempts, |
|
| 226 | + ]); |
|
| 227 | + } |
|
| 228 | + if ($attempts > 0) { |
|
| 229 | + return $this->calculateDelay($attempts); |
|
| 230 | + } |
|
| 231 | + |
|
| 232 | + return 0; |
|
| 233 | + } |
|
| 234 | 234 | } |
@@ -20,107 +20,107 @@ |
||
| 20 | 20 | |
| 21 | 21 | class PublicShareMiddleware extends Middleware { |
| 22 | 22 | |
| 23 | - public function __construct( |
|
| 24 | - private IRequest $request, |
|
| 25 | - private ISession $session, |
|
| 26 | - private IConfig $config, |
|
| 27 | - private IThrottler $throttler, |
|
| 28 | - ) { |
|
| 29 | - } |
|
| 30 | - |
|
| 31 | - public function beforeController($controller, $methodName) { |
|
| 32 | - if (!($controller instanceof PublicShareController)) { |
|
| 33 | - return; |
|
| 34 | - } |
|
| 35 | - |
|
| 36 | - $controllerClassPath = explode('\\', get_class($controller)); |
|
| 37 | - $controllerShortClass = end($controllerClassPath); |
|
| 38 | - $bruteforceProtectionAction = $controllerShortClass . '::' . $methodName; |
|
| 39 | - $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $bruteforceProtectionAction); |
|
| 40 | - |
|
| 41 | - if (!$this->isLinkSharingEnabled()) { |
|
| 42 | - throw new NotFoundException('Link sharing is disabled'); |
|
| 43 | - } |
|
| 44 | - |
|
| 45 | - // We require the token parameter to be set |
|
| 46 | - $token = $this->request->getParam('token'); |
|
| 47 | - if ($token === null) { |
|
| 48 | - throw new NotFoundException(); |
|
| 49 | - } |
|
| 50 | - |
|
| 51 | - // Set the token |
|
| 52 | - $controller->setToken($token); |
|
| 53 | - |
|
| 54 | - if (!$controller->isValidToken()) { |
|
| 55 | - $this->throttle($bruteforceProtectionAction, $token); |
|
| 56 | - |
|
| 57 | - $controller->shareNotFound(); |
|
| 58 | - throw new NotFoundException(); |
|
| 59 | - } |
|
| 60 | - |
|
| 61 | - // No need to check for authentication when we try to authenticate |
|
| 62 | - if ($methodName === 'authenticate' || $methodName === 'showAuthenticate') { |
|
| 63 | - return; |
|
| 64 | - } |
|
| 65 | - |
|
| 66 | - // If authentication succeeds just continue |
|
| 67 | - if ($controller->isAuthenticated()) { |
|
| 68 | - return; |
|
| 69 | - } |
|
| 70 | - |
|
| 71 | - // If we can authenticate to this controller do it else we throw a 404 to not leak any info |
|
| 72 | - if ($controller instanceof AuthPublicShareController) { |
|
| 73 | - $this->session->set('public_link_authenticate_redirect', json_encode($this->request->getParams())); |
|
| 74 | - throw new NeedAuthenticationException(); |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - $this->throttle($bruteforceProtectionAction, $token); |
|
| 78 | - throw new NotFoundException(); |
|
| 79 | - } |
|
| 80 | - |
|
| 81 | - public function afterException($controller, $methodName, \Exception $exception) { |
|
| 82 | - if (!($controller instanceof PublicShareController)) { |
|
| 83 | - throw $exception; |
|
| 84 | - } |
|
| 85 | - |
|
| 86 | - if ($exception instanceof NotFoundException) { |
|
| 87 | - return new TemplateResponse(Application::APP_ID, 'sharenotfound', [ |
|
| 88 | - 'message' => $exception->getMessage(), |
|
| 89 | - ], 'guest', Http::STATUS_NOT_FOUND); |
|
| 90 | - } |
|
| 91 | - |
|
| 92 | - if ($controller instanceof AuthPublicShareController && $exception instanceof NeedAuthenticationException) { |
|
| 93 | - return $controller->getAuthenticationRedirect($this->getFunctionForRoute($this->request->getParam('_route'))); |
|
| 94 | - } |
|
| 95 | - |
|
| 96 | - throw $exception; |
|
| 97 | - } |
|
| 98 | - |
|
| 99 | - private function getFunctionForRoute(string $route): string { |
|
| 100 | - $tmp = explode('.', $route); |
|
| 101 | - return array_pop($tmp); |
|
| 102 | - } |
|
| 103 | - |
|
| 104 | - /** |
|
| 105 | - * Check if link sharing is allowed |
|
| 106 | - */ |
|
| 107 | - private function isLinkSharingEnabled(): bool { |
|
| 108 | - // Check if the shareAPI is enabled |
|
| 109 | - if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') { |
|
| 110 | - return false; |
|
| 111 | - } |
|
| 112 | - |
|
| 113 | - // Check whether public sharing is enabled |
|
| 114 | - if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') { |
|
| 115 | - return false; |
|
| 116 | - } |
|
| 117 | - |
|
| 118 | - return true; |
|
| 119 | - } |
|
| 120 | - |
|
| 121 | - private function throttle($bruteforceProtectionAction, $token): void { |
|
| 122 | - $ip = $this->request->getRemoteAddress(); |
|
| 123 | - $this->throttler->sleepDelayOrThrowOnMax($ip, $bruteforceProtectionAction); |
|
| 124 | - $this->throttler->registerAttempt($bruteforceProtectionAction, $ip, ['token' => $token]); |
|
| 125 | - } |
|
| 23 | + public function __construct( |
|
| 24 | + private IRequest $request, |
|
| 25 | + private ISession $session, |
|
| 26 | + private IConfig $config, |
|
| 27 | + private IThrottler $throttler, |
|
| 28 | + ) { |
|
| 29 | + } |
|
| 30 | + |
|
| 31 | + public function beforeController($controller, $methodName) { |
|
| 32 | + if (!($controller instanceof PublicShareController)) { |
|
| 33 | + return; |
|
| 34 | + } |
|
| 35 | + |
|
| 36 | + $controllerClassPath = explode('\\', get_class($controller)); |
|
| 37 | + $controllerShortClass = end($controllerClassPath); |
|
| 38 | + $bruteforceProtectionAction = $controllerShortClass . '::' . $methodName; |
|
| 39 | + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $bruteforceProtectionAction); |
|
| 40 | + |
|
| 41 | + if (!$this->isLinkSharingEnabled()) { |
|
| 42 | + throw new NotFoundException('Link sharing is disabled'); |
|
| 43 | + } |
|
| 44 | + |
|
| 45 | + // We require the token parameter to be set |
|
| 46 | + $token = $this->request->getParam('token'); |
|
| 47 | + if ($token === null) { |
|
| 48 | + throw new NotFoundException(); |
|
| 49 | + } |
|
| 50 | + |
|
| 51 | + // Set the token |
|
| 52 | + $controller->setToken($token); |
|
| 53 | + |
|
| 54 | + if (!$controller->isValidToken()) { |
|
| 55 | + $this->throttle($bruteforceProtectionAction, $token); |
|
| 56 | + |
|
| 57 | + $controller->shareNotFound(); |
|
| 58 | + throw new NotFoundException(); |
|
| 59 | + } |
|
| 60 | + |
|
| 61 | + // No need to check for authentication when we try to authenticate |
|
| 62 | + if ($methodName === 'authenticate' || $methodName === 'showAuthenticate') { |
|
| 63 | + return; |
|
| 64 | + } |
|
| 65 | + |
|
| 66 | + // If authentication succeeds just continue |
|
| 67 | + if ($controller->isAuthenticated()) { |
|
| 68 | + return; |
|
| 69 | + } |
|
| 70 | + |
|
| 71 | + // If we can authenticate to this controller do it else we throw a 404 to not leak any info |
|
| 72 | + if ($controller instanceof AuthPublicShareController) { |
|
| 73 | + $this->session->set('public_link_authenticate_redirect', json_encode($this->request->getParams())); |
|
| 74 | + throw new NeedAuthenticationException(); |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + $this->throttle($bruteforceProtectionAction, $token); |
|
| 78 | + throw new NotFoundException(); |
|
| 79 | + } |
|
| 80 | + |
|
| 81 | + public function afterException($controller, $methodName, \Exception $exception) { |
|
| 82 | + if (!($controller instanceof PublicShareController)) { |
|
| 83 | + throw $exception; |
|
| 84 | + } |
|
| 85 | + |
|
| 86 | + if ($exception instanceof NotFoundException) { |
|
| 87 | + return new TemplateResponse(Application::APP_ID, 'sharenotfound', [ |
|
| 88 | + 'message' => $exception->getMessage(), |
|
| 89 | + ], 'guest', Http::STATUS_NOT_FOUND); |
|
| 90 | + } |
|
| 91 | + |
|
| 92 | + if ($controller instanceof AuthPublicShareController && $exception instanceof NeedAuthenticationException) { |
|
| 93 | + return $controller->getAuthenticationRedirect($this->getFunctionForRoute($this->request->getParam('_route'))); |
|
| 94 | + } |
|
| 95 | + |
|
| 96 | + throw $exception; |
|
| 97 | + } |
|
| 98 | + |
|
| 99 | + private function getFunctionForRoute(string $route): string { |
|
| 100 | + $tmp = explode('.', $route); |
|
| 101 | + return array_pop($tmp); |
|
| 102 | + } |
|
| 103 | + |
|
| 104 | + /** |
|
| 105 | + * Check if link sharing is allowed |
|
| 106 | + */ |
|
| 107 | + private function isLinkSharingEnabled(): bool { |
|
| 108 | + // Check if the shareAPI is enabled |
|
| 109 | + if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') { |
|
| 110 | + return false; |
|
| 111 | + } |
|
| 112 | + |
|
| 113 | + // Check whether public sharing is enabled |
|
| 114 | + if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') { |
|
| 115 | + return false; |
|
| 116 | + } |
|
| 117 | + |
|
| 118 | + return true; |
|
| 119 | + } |
|
| 120 | + |
|
| 121 | + private function throttle($bruteforceProtectionAction, $token): void { |
|
| 122 | + $ip = $this->request->getRemoteAddress(); |
|
| 123 | + $this->throttler->sleepDelayOrThrowOnMax($ip, $bruteforceProtectionAction); |
|
| 124 | + $this->throttler->registerAttempt($bruteforceProtectionAction, $ip, ['token' => $token]); |
|
| 125 | + } |
|
| 126 | 126 | } |
@@ -22,64 +22,64 @@ |
||
| 22 | 22 | |
| 23 | 23 | class DirectHome implements ICollection { |
| 24 | 24 | |
| 25 | - public function __construct( |
|
| 26 | - private IRootFolder $rootFolder, |
|
| 27 | - private DirectMapper $mapper, |
|
| 28 | - private ITimeFactory $timeFactory, |
|
| 29 | - private IThrottler $throttler, |
|
| 30 | - private IRequest $request, |
|
| 31 | - private IEventDispatcher $eventDispatcher, |
|
| 32 | - ) { |
|
| 33 | - } |
|
| 34 | - |
|
| 35 | - public function createFile($name, $data = null) { |
|
| 36 | - throw new Forbidden(); |
|
| 37 | - } |
|
| 38 | - |
|
| 39 | - public function createDirectory($name) { |
|
| 40 | - throw new Forbidden(); |
|
| 41 | - } |
|
| 42 | - |
|
| 43 | - public function getChild($name): DirectFile { |
|
| 44 | - try { |
|
| 45 | - $direct = $this->mapper->getByToken($name); |
|
| 46 | - |
|
| 47 | - // Expired |
|
| 48 | - if ($direct->getExpiration() < $this->timeFactory->getTime()) { |
|
| 49 | - throw new NotFound(); |
|
| 50 | - } |
|
| 51 | - |
|
| 52 | - return new DirectFile($direct, $this->rootFolder, $this->eventDispatcher); |
|
| 53 | - } catch (DoesNotExistException $e) { |
|
| 54 | - // Since the token space is so huge only throttle on non-existing token |
|
| 55 | - $this->throttler->registerAttempt('directlink', $this->request->getRemoteAddress()); |
|
| 56 | - $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), 'directlink'); |
|
| 57 | - |
|
| 58 | - throw new NotFound(); |
|
| 59 | - } |
|
| 60 | - } |
|
| 61 | - |
|
| 62 | - public function getChildren() { |
|
| 63 | - throw new MethodNotAllowed('Listing members of this collection is disabled'); |
|
| 64 | - } |
|
| 65 | - |
|
| 66 | - public function childExists($name): bool { |
|
| 67 | - return false; |
|
| 68 | - } |
|
| 69 | - |
|
| 70 | - public function delete() { |
|
| 71 | - throw new Forbidden(); |
|
| 72 | - } |
|
| 73 | - |
|
| 74 | - public function getName(): string { |
|
| 75 | - return 'direct'; |
|
| 76 | - } |
|
| 77 | - |
|
| 78 | - public function setName($name) { |
|
| 79 | - throw new Forbidden(); |
|
| 80 | - } |
|
| 81 | - |
|
| 82 | - public function getLastModified(): int { |
|
| 83 | - return 0; |
|
| 84 | - } |
|
| 25 | + public function __construct( |
|
| 26 | + private IRootFolder $rootFolder, |
|
| 27 | + private DirectMapper $mapper, |
|
| 28 | + private ITimeFactory $timeFactory, |
|
| 29 | + private IThrottler $throttler, |
|
| 30 | + private IRequest $request, |
|
| 31 | + private IEventDispatcher $eventDispatcher, |
|
| 32 | + ) { |
|
| 33 | + } |
|
| 34 | + |
|
| 35 | + public function createFile($name, $data = null) { |
|
| 36 | + throw new Forbidden(); |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + public function createDirectory($name) { |
|
| 40 | + throw new Forbidden(); |
|
| 41 | + } |
|
| 42 | + |
|
| 43 | + public function getChild($name): DirectFile { |
|
| 44 | + try { |
|
| 45 | + $direct = $this->mapper->getByToken($name); |
|
| 46 | + |
|
| 47 | + // Expired |
|
| 48 | + if ($direct->getExpiration() < $this->timeFactory->getTime()) { |
|
| 49 | + throw new NotFound(); |
|
| 50 | + } |
|
| 51 | + |
|
| 52 | + return new DirectFile($direct, $this->rootFolder, $this->eventDispatcher); |
|
| 53 | + } catch (DoesNotExistException $e) { |
|
| 54 | + // Since the token space is so huge only throttle on non-existing token |
|
| 55 | + $this->throttler->registerAttempt('directlink', $this->request->getRemoteAddress()); |
|
| 56 | + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), 'directlink'); |
|
| 57 | + |
|
| 58 | + throw new NotFound(); |
|
| 59 | + } |
|
| 60 | + } |
|
| 61 | + |
|
| 62 | + public function getChildren() { |
|
| 63 | + throw new MethodNotAllowed('Listing members of this collection is disabled'); |
|
| 64 | + } |
|
| 65 | + |
|
| 66 | + public function childExists($name): bool { |
|
| 67 | + return false; |
|
| 68 | + } |
|
| 69 | + |
|
| 70 | + public function delete() { |
|
| 71 | + throw new Forbidden(); |
|
| 72 | + } |
|
| 73 | + |
|
| 74 | + public function getName(): string { |
|
| 75 | + return 'direct'; |
|
| 76 | + } |
|
| 77 | + |
|
| 78 | + public function setName($name) { |
|
| 79 | + throw new Forbidden(); |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + public function getLastModified(): int { |
|
| 83 | + return 0; |
|
| 84 | + } |
|
| 85 | 85 | } |
@@ -34,179 +34,179 @@ |
||
| 34 | 34 | * @package OCA\DAV\Connector |
| 35 | 35 | */ |
| 36 | 36 | class PublicAuth extends AbstractBasic { |
| 37 | - private const BRUTEFORCE_ACTION = 'public_dav_auth'; |
|
| 38 | - public const DAV_AUTHENTICATED = 'public_link_authenticated'; |
|
| 39 | - |
|
| 40 | - private ?IShare $share = null; |
|
| 41 | - |
|
| 42 | - public function __construct( |
|
| 43 | - private IRequest $request, |
|
| 44 | - private IManager $shareManager, |
|
| 45 | - private ISession $session, |
|
| 46 | - private IThrottler $throttler, |
|
| 47 | - private LoggerInterface $logger, |
|
| 48 | - ) { |
|
| 49 | - // setup realm |
|
| 50 | - $defaults = new Defaults(); |
|
| 51 | - $this->realm = $defaults->getName(); |
|
| 52 | - } |
|
| 53 | - |
|
| 54 | - /** |
|
| 55 | - * @param RequestInterface $request |
|
| 56 | - * @param ResponseInterface $response |
|
| 57 | - * |
|
| 58 | - * @return array |
|
| 59 | - * @throws NotAuthenticated |
|
| 60 | - * @throws MaxDelayReached |
|
| 61 | - * @throws ServiceUnavailable |
|
| 62 | - */ |
|
| 63 | - public function check(RequestInterface $request, ResponseInterface $response): array { |
|
| 64 | - try { |
|
| 65 | - $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); |
|
| 66 | - |
|
| 67 | - $auth = new HTTP\Auth\Basic( |
|
| 68 | - $this->realm, |
|
| 69 | - $request, |
|
| 70 | - $response |
|
| 71 | - ); |
|
| 72 | - |
|
| 73 | - $userpass = $auth->getCredentials(); |
|
| 74 | - // If authentication provided, checking its validity |
|
| 75 | - if ($userpass && !$this->validateUserPass($userpass[0], $userpass[1])) { |
|
| 76 | - return [false, 'Username or password was incorrect']; |
|
| 77 | - } |
|
| 78 | - |
|
| 79 | - return $this->checkToken(); |
|
| 80 | - } catch (NotAuthenticated|MaxDelayReached $e) { |
|
| 81 | - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 82 | - throw $e; |
|
| 83 | - } catch (\Exception $e) { |
|
| 84 | - $class = get_class($e); |
|
| 85 | - $msg = $e->getMessage(); |
|
| 86 | - $this->logger->error($e->getMessage(), ['exception' => $e]); |
|
| 87 | - throw new ServiceUnavailable("$class: $msg"); |
|
| 88 | - } |
|
| 89 | - } |
|
| 90 | - |
|
| 91 | - /** |
|
| 92 | - * Extract token from request url |
|
| 93 | - * @return string |
|
| 94 | - * @throws NotFound |
|
| 95 | - */ |
|
| 96 | - private function getToken(): string { |
|
| 97 | - $path = $this->request->getPathInfo() ?: ''; |
|
| 98 | - // ['', 'dav', 'files', 'token'] |
|
| 99 | - $splittedPath = explode('/', $path); |
|
| 100 | - |
|
| 101 | - if (count($splittedPath) < 4 || $splittedPath[3] === '') { |
|
| 102 | - throw new NotFound(); |
|
| 103 | - } |
|
| 104 | - |
|
| 105 | - return $splittedPath[3]; |
|
| 106 | - } |
|
| 107 | - |
|
| 108 | - /** |
|
| 109 | - * Check token validity |
|
| 110 | - * @return array |
|
| 111 | - * @throws NotFound |
|
| 112 | - * @throws NotAuthenticated |
|
| 113 | - */ |
|
| 114 | - private function checkToken(): array { |
|
| 115 | - $token = $this->getToken(); |
|
| 116 | - |
|
| 117 | - try { |
|
| 118 | - /** @var IShare $share */ |
|
| 119 | - $share = $this->shareManager->getShareByToken($token); |
|
| 120 | - } catch (ShareNotFound $e) { |
|
| 121 | - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 122 | - throw new NotFound(); |
|
| 123 | - } |
|
| 124 | - |
|
| 125 | - $this->share = $share; |
|
| 126 | - \OC_User::setIncognitoMode(true); |
|
| 127 | - |
|
| 128 | - // If already authenticated |
|
| 129 | - if ($this->session->exists(self::DAV_AUTHENTICATED) |
|
| 130 | - && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) { |
|
| 131 | - return [true, $this->principalPrefix . $token]; |
|
| 132 | - } |
|
| 133 | - |
|
| 134 | - // If the share is protected but user is not authenticated |
|
| 135 | - if ($share->getPassword() !== null) { |
|
| 136 | - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 137 | - throw new NotAuthenticated(); |
|
| 138 | - } |
|
| 139 | - |
|
| 140 | - return [true, $this->principalPrefix . $token]; |
|
| 141 | - } |
|
| 142 | - |
|
| 143 | - /** |
|
| 144 | - * Validates a username and password |
|
| 145 | - * |
|
| 146 | - * This method should return true or false depending on if login |
|
| 147 | - * succeeded. |
|
| 148 | - * |
|
| 149 | - * @param string $username |
|
| 150 | - * @param string $password |
|
| 151 | - * |
|
| 152 | - * @return bool |
|
| 153 | - * @throws NotAuthenticated |
|
| 154 | - */ |
|
| 155 | - protected function validateUserPass($username, $password) { |
|
| 156 | - $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); |
|
| 157 | - |
|
| 158 | - $token = $this->getToken(); |
|
| 159 | - try { |
|
| 160 | - $share = $this->shareManager->getShareByToken($token); |
|
| 161 | - } catch (ShareNotFound $e) { |
|
| 162 | - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 163 | - return false; |
|
| 164 | - } |
|
| 165 | - |
|
| 166 | - $this->share = $share; |
|
| 167 | - \OC_User::setIncognitoMode(true); |
|
| 168 | - |
|
| 169 | - // check if the share is password protected |
|
| 170 | - if ($share->getPassword() !== null) { |
|
| 171 | - if ($share->getShareType() === IShare::TYPE_LINK |
|
| 172 | - || $share->getShareType() === IShare::TYPE_EMAIL |
|
| 173 | - || $share->getShareType() === IShare::TYPE_CIRCLE) { |
|
| 174 | - if ($this->shareManager->checkPassword($share, $password)) { |
|
| 175 | - // If not set, set authenticated session cookie |
|
| 176 | - if (!$this->session->exists(self::DAV_AUTHENTICATED) |
|
| 177 | - || $this->session->get(self::DAV_AUTHENTICATED) !== $share->getId()) { |
|
| 178 | - $this->session->set(self::DAV_AUTHENTICATED, $share->getId()); |
|
| 179 | - } |
|
| 180 | - return true; |
|
| 181 | - } |
|
| 182 | - |
|
| 183 | - if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED) |
|
| 184 | - && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) { |
|
| 185 | - return true; |
|
| 186 | - } |
|
| 187 | - |
|
| 188 | - if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { |
|
| 189 | - // do not re-authenticate over ajax, use dummy auth name to prevent browser popup |
|
| 190 | - http_response_code(401); |
|
| 191 | - header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"'); |
|
| 192 | - throw new NotAuthenticated('Cannot authenticate over ajax calls'); |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 196 | - return false; |
|
| 197 | - } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { |
|
| 198 | - return true; |
|
| 199 | - } |
|
| 200 | - |
|
| 201 | - $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 202 | - return false; |
|
| 203 | - } |
|
| 204 | - |
|
| 205 | - return true; |
|
| 206 | - } |
|
| 207 | - |
|
| 208 | - public function getShare(): IShare { |
|
| 209 | - assert($this->share !== null); |
|
| 210 | - return $this->share; |
|
| 211 | - } |
|
| 37 | + private const BRUTEFORCE_ACTION = 'public_dav_auth'; |
|
| 38 | + public const DAV_AUTHENTICATED = 'public_link_authenticated'; |
|
| 39 | + |
|
| 40 | + private ?IShare $share = null; |
|
| 41 | + |
|
| 42 | + public function __construct( |
|
| 43 | + private IRequest $request, |
|
| 44 | + private IManager $shareManager, |
|
| 45 | + private ISession $session, |
|
| 46 | + private IThrottler $throttler, |
|
| 47 | + private LoggerInterface $logger, |
|
| 48 | + ) { |
|
| 49 | + // setup realm |
|
| 50 | + $defaults = new Defaults(); |
|
| 51 | + $this->realm = $defaults->getName(); |
|
| 52 | + } |
|
| 53 | + |
|
| 54 | + /** |
|
| 55 | + * @param RequestInterface $request |
|
| 56 | + * @param ResponseInterface $response |
|
| 57 | + * |
|
| 58 | + * @return array |
|
| 59 | + * @throws NotAuthenticated |
|
| 60 | + * @throws MaxDelayReached |
|
| 61 | + * @throws ServiceUnavailable |
|
| 62 | + */ |
|
| 63 | + public function check(RequestInterface $request, ResponseInterface $response): array { |
|
| 64 | + try { |
|
| 65 | + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); |
|
| 66 | + |
|
| 67 | + $auth = new HTTP\Auth\Basic( |
|
| 68 | + $this->realm, |
|
| 69 | + $request, |
|
| 70 | + $response |
|
| 71 | + ); |
|
| 72 | + |
|
| 73 | + $userpass = $auth->getCredentials(); |
|
| 74 | + // If authentication provided, checking its validity |
|
| 75 | + if ($userpass && !$this->validateUserPass($userpass[0], $userpass[1])) { |
|
| 76 | + return [false, 'Username or password was incorrect']; |
|
| 77 | + } |
|
| 78 | + |
|
| 79 | + return $this->checkToken(); |
|
| 80 | + } catch (NotAuthenticated|MaxDelayReached $e) { |
|
| 81 | + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 82 | + throw $e; |
|
| 83 | + } catch (\Exception $e) { |
|
| 84 | + $class = get_class($e); |
|
| 85 | + $msg = $e->getMessage(); |
|
| 86 | + $this->logger->error($e->getMessage(), ['exception' => $e]); |
|
| 87 | + throw new ServiceUnavailable("$class: $msg"); |
|
| 88 | + } |
|
| 89 | + } |
|
| 90 | + |
|
| 91 | + /** |
|
| 92 | + * Extract token from request url |
|
| 93 | + * @return string |
|
| 94 | + * @throws NotFound |
|
| 95 | + */ |
|
| 96 | + private function getToken(): string { |
|
| 97 | + $path = $this->request->getPathInfo() ?: ''; |
|
| 98 | + // ['', 'dav', 'files', 'token'] |
|
| 99 | + $splittedPath = explode('/', $path); |
|
| 100 | + |
|
| 101 | + if (count($splittedPath) < 4 || $splittedPath[3] === '') { |
|
| 102 | + throw new NotFound(); |
|
| 103 | + } |
|
| 104 | + |
|
| 105 | + return $splittedPath[3]; |
|
| 106 | + } |
|
| 107 | + |
|
| 108 | + /** |
|
| 109 | + * Check token validity |
|
| 110 | + * @return array |
|
| 111 | + * @throws NotFound |
|
| 112 | + * @throws NotAuthenticated |
|
| 113 | + */ |
|
| 114 | + private function checkToken(): array { |
|
| 115 | + $token = $this->getToken(); |
|
| 116 | + |
|
| 117 | + try { |
|
| 118 | + /** @var IShare $share */ |
|
| 119 | + $share = $this->shareManager->getShareByToken($token); |
|
| 120 | + } catch (ShareNotFound $e) { |
|
| 121 | + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 122 | + throw new NotFound(); |
|
| 123 | + } |
|
| 124 | + |
|
| 125 | + $this->share = $share; |
|
| 126 | + \OC_User::setIncognitoMode(true); |
|
| 127 | + |
|
| 128 | + // If already authenticated |
|
| 129 | + if ($this->session->exists(self::DAV_AUTHENTICATED) |
|
| 130 | + && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) { |
|
| 131 | + return [true, $this->principalPrefix . $token]; |
|
| 132 | + } |
|
| 133 | + |
|
| 134 | + // If the share is protected but user is not authenticated |
|
| 135 | + if ($share->getPassword() !== null) { |
|
| 136 | + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 137 | + throw new NotAuthenticated(); |
|
| 138 | + } |
|
| 139 | + |
|
| 140 | + return [true, $this->principalPrefix . $token]; |
|
| 141 | + } |
|
| 142 | + |
|
| 143 | + /** |
|
| 144 | + * Validates a username and password |
|
| 145 | + * |
|
| 146 | + * This method should return true or false depending on if login |
|
| 147 | + * succeeded. |
|
| 148 | + * |
|
| 149 | + * @param string $username |
|
| 150 | + * @param string $password |
|
| 151 | + * |
|
| 152 | + * @return bool |
|
| 153 | + * @throws NotAuthenticated |
|
| 154 | + */ |
|
| 155 | + protected function validateUserPass($username, $password) { |
|
| 156 | + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); |
|
| 157 | + |
|
| 158 | + $token = $this->getToken(); |
|
| 159 | + try { |
|
| 160 | + $share = $this->shareManager->getShareByToken($token); |
|
| 161 | + } catch (ShareNotFound $e) { |
|
| 162 | + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 163 | + return false; |
|
| 164 | + } |
|
| 165 | + |
|
| 166 | + $this->share = $share; |
|
| 167 | + \OC_User::setIncognitoMode(true); |
|
| 168 | + |
|
| 169 | + // check if the share is password protected |
|
| 170 | + if ($share->getPassword() !== null) { |
|
| 171 | + if ($share->getShareType() === IShare::TYPE_LINK |
|
| 172 | + || $share->getShareType() === IShare::TYPE_EMAIL |
|
| 173 | + || $share->getShareType() === IShare::TYPE_CIRCLE) { |
|
| 174 | + if ($this->shareManager->checkPassword($share, $password)) { |
|
| 175 | + // If not set, set authenticated session cookie |
|
| 176 | + if (!$this->session->exists(self::DAV_AUTHENTICATED) |
|
| 177 | + || $this->session->get(self::DAV_AUTHENTICATED) !== $share->getId()) { |
|
| 178 | + $this->session->set(self::DAV_AUTHENTICATED, $share->getId()); |
|
| 179 | + } |
|
| 180 | + return true; |
|
| 181 | + } |
|
| 182 | + |
|
| 183 | + if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED) |
|
| 184 | + && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) { |
|
| 185 | + return true; |
|
| 186 | + } |
|
| 187 | + |
|
| 188 | + if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { |
|
| 189 | + // do not re-authenticate over ajax, use dummy auth name to prevent browser popup |
|
| 190 | + http_response_code(401); |
|
| 191 | + header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"'); |
|
| 192 | + throw new NotAuthenticated('Cannot authenticate over ajax calls'); |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 196 | + return false; |
|
| 197 | + } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { |
|
| 198 | + return true; |
|
| 199 | + } |
|
| 200 | + |
|
| 201 | + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
|
| 202 | + return false; |
|
| 203 | + } |
|
| 204 | + |
|
| 205 | + return true; |
|
| 206 | + } |
|
| 207 | + |
|
| 208 | + public function getShare(): IShare { |
|
| 209 | + assert($this->share !== null); |
|
| 210 | + return $this->share; |
|
| 211 | + } |
|
| 212 | 212 | } |
@@ -77,7 +77,7 @@ discard block |
||
| 77 | 77 | } |
| 78 | 78 | |
| 79 | 79 | return $this->checkToken(); |
| 80 | - } catch (NotAuthenticated|MaxDelayReached $e) { |
|
| 80 | + } catch (NotAuthenticated | MaxDelayReached $e) { |
|
| 81 | 81 | $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); |
| 82 | 82 | throw $e; |
| 83 | 83 | } catch (\Exception $e) { |
@@ -128,7 +128,7 @@ discard block |
||
| 128 | 128 | // If already authenticated |
| 129 | 129 | if ($this->session->exists(self::DAV_AUTHENTICATED) |
| 130 | 130 | && $this->session->get(self::DAV_AUTHENTICATED) === $share->getId()) { |
| 131 | - return [true, $this->principalPrefix . $token]; |
|
| 131 | + return [true, $this->principalPrefix.$token]; |
|
| 132 | 132 | } |
| 133 | 133 | |
| 134 | 134 | // If the share is protected but user is not authenticated |
@@ -137,7 +137,7 @@ discard block |
||
| 137 | 137 | throw new NotAuthenticated(); |
| 138 | 138 | } |
| 139 | 139 | |
| 140 | - return [true, $this->principalPrefix . $token]; |
|
| 140 | + return [true, $this->principalPrefix.$token]; |
|
| 141 | 141 | } |
| 142 | 142 | |
| 143 | 143 | /** |
@@ -188,7 +188,7 @@ discard block |
||
| 188 | 188 | if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { |
| 189 | 189 | // do not re-authenticate over ajax, use dummy auth name to prevent browser popup |
| 190 | 190 | http_response_code(401); |
| 191 | - header('WWW-Authenticate: DummyBasic realm="' . $this->realm . '"'); |
|
| 191 | + header('WWW-Authenticate: DummyBasic realm="'.$this->realm.'"'); |
|
| 192 | 192 | throw new NotAuthenticated('Cannot authenticate over ajax calls'); |
| 193 | 193 | } |
| 194 | 194 | |
@@ -18,89 +18,89 @@ |
||
| 18 | 18 | use Sabre\DAV\ServerPlugin; |
| 19 | 19 | |
| 20 | 20 | class BrowserErrorPagePlugin extends ServerPlugin { |
| 21 | - /** @var Server */ |
|
| 22 | - private $server; |
|
| 21 | + /** @var Server */ |
|
| 22 | + private $server; |
|
| 23 | 23 | |
| 24 | - /** |
|
| 25 | - * This initializes the plugin. |
|
| 26 | - * |
|
| 27 | - * This function is called by Sabre\DAV\Server, after |
|
| 28 | - * addPlugin is called. |
|
| 29 | - * |
|
| 30 | - * This method should set up the required event subscriptions. |
|
| 31 | - * |
|
| 32 | - * @param Server $server |
|
| 33 | - * @return void |
|
| 34 | - */ |
|
| 35 | - public function initialize(Server $server) { |
|
| 36 | - $this->server = $server; |
|
| 37 | - $server->on('exception', [$this, 'logException'], 1000); |
|
| 38 | - } |
|
| 24 | + /** |
|
| 25 | + * This initializes the plugin. |
|
| 26 | + * |
|
| 27 | + * This function is called by Sabre\DAV\Server, after |
|
| 28 | + * addPlugin is called. |
|
| 29 | + * |
|
| 30 | + * This method should set up the required event subscriptions. |
|
| 31 | + * |
|
| 32 | + * @param Server $server |
|
| 33 | + * @return void |
|
| 34 | + */ |
|
| 35 | + public function initialize(Server $server) { |
|
| 36 | + $this->server = $server; |
|
| 37 | + $server->on('exception', [$this, 'logException'], 1000); |
|
| 38 | + } |
|
| 39 | 39 | |
| 40 | - /** |
|
| 41 | - * @param IRequest $request |
|
| 42 | - * @return bool |
|
| 43 | - */ |
|
| 44 | - public static function isBrowserRequest(IRequest $request) { |
|
| 45 | - if ($request->getMethod() !== 'GET') { |
|
| 46 | - return false; |
|
| 47 | - } |
|
| 48 | - return $request->isUserAgent([ |
|
| 49 | - Request::USER_AGENT_IE, |
|
| 50 | - Request::USER_AGENT_MS_EDGE, |
|
| 51 | - Request::USER_AGENT_CHROME, |
|
| 52 | - Request::USER_AGENT_FIREFOX, |
|
| 53 | - Request::USER_AGENT_SAFARI, |
|
| 54 | - ]); |
|
| 55 | - } |
|
| 40 | + /** |
|
| 41 | + * @param IRequest $request |
|
| 42 | + * @return bool |
|
| 43 | + */ |
|
| 44 | + public static function isBrowserRequest(IRequest $request) { |
|
| 45 | + if ($request->getMethod() !== 'GET') { |
|
| 46 | + return false; |
|
| 47 | + } |
|
| 48 | + return $request->isUserAgent([ |
|
| 49 | + Request::USER_AGENT_IE, |
|
| 50 | + Request::USER_AGENT_MS_EDGE, |
|
| 51 | + Request::USER_AGENT_CHROME, |
|
| 52 | + Request::USER_AGENT_FIREFOX, |
|
| 53 | + Request::USER_AGENT_SAFARI, |
|
| 54 | + ]); |
|
| 55 | + } |
|
| 56 | 56 | |
| 57 | - /** |
|
| 58 | - * @param \Throwable $ex |
|
| 59 | - */ |
|
| 60 | - public function logException(\Throwable $ex): void { |
|
| 61 | - if ($ex instanceof Exception) { |
|
| 62 | - $httpCode = $ex->getHTTPCode(); |
|
| 63 | - $headers = $ex->getHTTPHeaders($this->server); |
|
| 64 | - } elseif ($ex instanceof MaxDelayReached) { |
|
| 65 | - $httpCode = 429; |
|
| 66 | - $headers = []; |
|
| 67 | - } else { |
|
| 68 | - $httpCode = 500; |
|
| 69 | - $headers = []; |
|
| 70 | - } |
|
| 71 | - $this->server->httpResponse->addHeaders($headers); |
|
| 72 | - $this->server->httpResponse->setStatus($httpCode); |
|
| 73 | - $body = $this->generateBody($httpCode); |
|
| 74 | - $this->server->httpResponse->setBody($body); |
|
| 75 | - $csp = new ContentSecurityPolicy(); |
|
| 76 | - $this->server->httpResponse->addHeader('Content-Security-Policy', $csp->buildPolicy()); |
|
| 77 | - $this->sendResponse(); |
|
| 78 | - } |
|
| 57 | + /** |
|
| 58 | + * @param \Throwable $ex |
|
| 59 | + */ |
|
| 60 | + public function logException(\Throwable $ex): void { |
|
| 61 | + if ($ex instanceof Exception) { |
|
| 62 | + $httpCode = $ex->getHTTPCode(); |
|
| 63 | + $headers = $ex->getHTTPHeaders($this->server); |
|
| 64 | + } elseif ($ex instanceof MaxDelayReached) { |
|
| 65 | + $httpCode = 429; |
|
| 66 | + $headers = []; |
|
| 67 | + } else { |
|
| 68 | + $httpCode = 500; |
|
| 69 | + $headers = []; |
|
| 70 | + } |
|
| 71 | + $this->server->httpResponse->addHeaders($headers); |
|
| 72 | + $this->server->httpResponse->setStatus($httpCode); |
|
| 73 | + $body = $this->generateBody($httpCode); |
|
| 74 | + $this->server->httpResponse->setBody($body); |
|
| 75 | + $csp = new ContentSecurityPolicy(); |
|
| 76 | + $this->server->httpResponse->addHeader('Content-Security-Policy', $csp->buildPolicy()); |
|
| 77 | + $this->sendResponse(); |
|
| 78 | + } |
|
| 79 | 79 | |
| 80 | - /** |
|
| 81 | - * @codeCoverageIgnore |
|
| 82 | - * @return bool|string |
|
| 83 | - */ |
|
| 84 | - public function generateBody(int $httpCode) { |
|
| 85 | - $request = \OCP\Server::get(IRequest::class); |
|
| 80 | + /** |
|
| 81 | + * @codeCoverageIgnore |
|
| 82 | + * @return bool|string |
|
| 83 | + */ |
|
| 84 | + public function generateBody(int $httpCode) { |
|
| 85 | + $request = \OCP\Server::get(IRequest::class); |
|
| 86 | 86 | |
| 87 | - $templateName = 'exception'; |
|
| 88 | - if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) { |
|
| 89 | - $templateName = (string)$httpCode; |
|
| 90 | - } |
|
| 87 | + $templateName = 'exception'; |
|
| 88 | + if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) { |
|
| 89 | + $templateName = (string)$httpCode; |
|
| 90 | + } |
|
| 91 | 91 | |
| 92 | - $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST); |
|
| 93 | - $content->assign('title', $this->server->httpResponse->getStatusText()); |
|
| 94 | - $content->assign('remoteAddr', $request->getRemoteAddress()); |
|
| 95 | - $content->assign('requestID', $request->getId()); |
|
| 96 | - return $content->fetchPage(); |
|
| 97 | - } |
|
| 92 | + $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST); |
|
| 93 | + $content->assign('title', $this->server->httpResponse->getStatusText()); |
|
| 94 | + $content->assign('remoteAddr', $request->getRemoteAddress()); |
|
| 95 | + $content->assign('requestID', $request->getId()); |
|
| 96 | + return $content->fetchPage(); |
|
| 97 | + } |
|
| 98 | 98 | |
| 99 | - /** |
|
| 100 | - * @codeCoverageIgnore |
|
| 101 | - */ |
|
| 102 | - public function sendResponse() { |
|
| 103 | - $this->server->sapi->sendResponse($this->server->httpResponse); |
|
| 104 | - exit(); |
|
| 105 | - } |
|
| 99 | + /** |
|
| 100 | + * @codeCoverageIgnore |
|
| 101 | + */ |
|
| 102 | + public function sendResponse() { |
|
| 103 | + $this->server->sapi->sendResponse($this->server->httpResponse); |
|
| 104 | + exit(); |
|
| 105 | + } |
|
| 106 | 106 | } |
@@ -86,7 +86,7 @@ |
||
| 86 | 86 | |
| 87 | 87 | $templateName = 'exception'; |
| 88 | 88 | if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) { |
| 89 | - $templateName = (string)$httpCode; |
|
| 89 | + $templateName = (string) $httpCode; |
|
| 90 | 90 | } |
| 91 | 91 | |
| 92 | 92 | $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST); |
@@ -25,149 +25,149 @@ |
||
| 25 | 25 | |
| 26 | 26 | class DirectHomeTest extends TestCase { |
| 27 | 27 | |
| 28 | - /** @var DirectMapper|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 29 | - private $directMapper; |
|
| 28 | + /** @var DirectMapper|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 29 | + private $directMapper; |
|
| 30 | 30 | |
| 31 | - /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 32 | - private $rootFolder; |
|
| 31 | + /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 32 | + private $rootFolder; |
|
| 33 | 33 | |
| 34 | - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 35 | - private $timeFactory; |
|
| 34 | + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 35 | + private $timeFactory; |
|
| 36 | 36 | |
| 37 | - /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 38 | - private $throttler; |
|
| 37 | + /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 38 | + private $throttler; |
|
| 39 | 39 | |
| 40 | - /** @var IRequest */ |
|
| 41 | - private $request; |
|
| 40 | + /** @var IRequest */ |
|
| 41 | + private $request; |
|
| 42 | 42 | |
| 43 | - /** @var DirectHome */ |
|
| 44 | - private $directHome; |
|
| 43 | + /** @var DirectHome */ |
|
| 44 | + private $directHome; |
|
| 45 | 45 | |
| 46 | - /** @var IEventDispatcher */ |
|
| 47 | - private $eventDispatcher; |
|
| 46 | + /** @var IEventDispatcher */ |
|
| 47 | + private $eventDispatcher; |
|
| 48 | 48 | |
| 49 | - protected function setUp(): void { |
|
| 50 | - parent::setUp(); |
|
| 49 | + protected function setUp(): void { |
|
| 50 | + parent::setUp(); |
|
| 51 | 51 | |
| 52 | - $this->directMapper = $this->createMock(DirectMapper::class); |
|
| 53 | - $this->rootFolder = $this->createMock(IRootFolder::class); |
|
| 54 | - $this->timeFactory = $this->createMock(ITimeFactory::class); |
|
| 55 | - $this->throttler = $this->createMock(IThrottler::class); |
|
| 56 | - $this->request = $this->createMock(IRequest::class); |
|
| 57 | - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); |
|
| 52 | + $this->directMapper = $this->createMock(DirectMapper::class); |
|
| 53 | + $this->rootFolder = $this->createMock(IRootFolder::class); |
|
| 54 | + $this->timeFactory = $this->createMock(ITimeFactory::class); |
|
| 55 | + $this->throttler = $this->createMock(IThrottler::class); |
|
| 56 | + $this->request = $this->createMock(IRequest::class); |
|
| 57 | + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); |
|
| 58 | 58 | |
| 59 | - $this->timeFactory->method('getTime') |
|
| 60 | - ->willReturn(42); |
|
| 59 | + $this->timeFactory->method('getTime') |
|
| 60 | + ->willReturn(42); |
|
| 61 | 61 | |
| 62 | - $this->request->method('getRemoteAddress') |
|
| 63 | - ->willReturn('1.2.3.4'); |
|
| 62 | + $this->request->method('getRemoteAddress') |
|
| 63 | + ->willReturn('1.2.3.4'); |
|
| 64 | 64 | |
| 65 | 65 | |
| 66 | - $this->directHome = new DirectHome( |
|
| 67 | - $this->rootFolder, |
|
| 68 | - $this->directMapper, |
|
| 69 | - $this->timeFactory, |
|
| 70 | - $this->throttler, |
|
| 71 | - $this->request, |
|
| 72 | - $this->eventDispatcher |
|
| 73 | - ); |
|
| 74 | - } |
|
| 66 | + $this->directHome = new DirectHome( |
|
| 67 | + $this->rootFolder, |
|
| 68 | + $this->directMapper, |
|
| 69 | + $this->timeFactory, |
|
| 70 | + $this->throttler, |
|
| 71 | + $this->request, |
|
| 72 | + $this->eventDispatcher |
|
| 73 | + ); |
|
| 74 | + } |
|
| 75 | 75 | |
| 76 | - public function testCreateFile(): void { |
|
| 77 | - $this->expectException(Forbidden::class); |
|
| 76 | + public function testCreateFile(): void { |
|
| 77 | + $this->expectException(Forbidden::class); |
|
| 78 | 78 | |
| 79 | - $this->directHome->createFile('foo', 'bar'); |
|
| 80 | - } |
|
| 79 | + $this->directHome->createFile('foo', 'bar'); |
|
| 80 | + } |
|
| 81 | 81 | |
| 82 | - public function testCreateDirectory(): void { |
|
| 83 | - $this->expectException(Forbidden::class); |
|
| 82 | + public function testCreateDirectory(): void { |
|
| 83 | + $this->expectException(Forbidden::class); |
|
| 84 | 84 | |
| 85 | - $this->directHome->createDirectory('foo'); |
|
| 86 | - } |
|
| 85 | + $this->directHome->createDirectory('foo'); |
|
| 86 | + } |
|
| 87 | 87 | |
| 88 | - public function testGetChildren(): void { |
|
| 89 | - $this->expectException(MethodNotAllowed::class); |
|
| 88 | + public function testGetChildren(): void { |
|
| 89 | + $this->expectException(MethodNotAllowed::class); |
|
| 90 | 90 | |
| 91 | - $this->directHome->getChildren(); |
|
| 92 | - } |
|
| 91 | + $this->directHome->getChildren(); |
|
| 92 | + } |
|
| 93 | 93 | |
| 94 | - public function testChildExists(): void { |
|
| 95 | - $this->assertFalse($this->directHome->childExists('foo')); |
|
| 96 | - } |
|
| 94 | + public function testChildExists(): void { |
|
| 95 | + $this->assertFalse($this->directHome->childExists('foo')); |
|
| 96 | + } |
|
| 97 | 97 | |
| 98 | - public function testDelete(): void { |
|
| 99 | - $this->expectException(Forbidden::class); |
|
| 98 | + public function testDelete(): void { |
|
| 99 | + $this->expectException(Forbidden::class); |
|
| 100 | 100 | |
| 101 | - $this->directHome->delete(); |
|
| 102 | - } |
|
| 101 | + $this->directHome->delete(); |
|
| 102 | + } |
|
| 103 | 103 | |
| 104 | - public function testGetName(): void { |
|
| 105 | - $this->assertSame('direct', $this->directHome->getName()); |
|
| 106 | - } |
|
| 104 | + public function testGetName(): void { |
|
| 105 | + $this->assertSame('direct', $this->directHome->getName()); |
|
| 106 | + } |
|
| 107 | 107 | |
| 108 | - public function testSetName(): void { |
|
| 109 | - $this->expectException(Forbidden::class); |
|
| 108 | + public function testSetName(): void { |
|
| 109 | + $this->expectException(Forbidden::class); |
|
| 110 | 110 | |
| 111 | - $this->directHome->setName('foo'); |
|
| 112 | - } |
|
| 111 | + $this->directHome->setName('foo'); |
|
| 112 | + } |
|
| 113 | 113 | |
| 114 | - public function testGetLastModified(): void { |
|
| 115 | - $this->assertSame(0, $this->directHome->getLastModified()); |
|
| 116 | - } |
|
| 114 | + public function testGetLastModified(): void { |
|
| 115 | + $this->assertSame(0, $this->directHome->getLastModified()); |
|
| 116 | + } |
|
| 117 | 117 | |
| 118 | - public function testGetChildValid(): void { |
|
| 119 | - $direct = Direct::fromParams([ |
|
| 120 | - 'expiration' => 100, |
|
| 121 | - ]); |
|
| 118 | + public function testGetChildValid(): void { |
|
| 119 | + $direct = Direct::fromParams([ |
|
| 120 | + 'expiration' => 100, |
|
| 121 | + ]); |
|
| 122 | 122 | |
| 123 | - $this->directMapper->method('getByToken') |
|
| 124 | - ->with('longtoken') |
|
| 125 | - ->willReturn($direct); |
|
| 123 | + $this->directMapper->method('getByToken') |
|
| 124 | + ->with('longtoken') |
|
| 125 | + ->willReturn($direct); |
|
| 126 | 126 | |
| 127 | - $this->throttler->expects($this->never()) |
|
| 128 | - ->method($this->anything()); |
|
| 127 | + $this->throttler->expects($this->never()) |
|
| 128 | + ->method($this->anything()); |
|
| 129 | 129 | |
| 130 | - $result = $this->directHome->getChild('longtoken'); |
|
| 131 | - $this->assertInstanceOf(DirectFile::class, $result); |
|
| 132 | - } |
|
| 130 | + $result = $this->directHome->getChild('longtoken'); |
|
| 131 | + $this->assertInstanceOf(DirectFile::class, $result); |
|
| 132 | + } |
|
| 133 | 133 | |
| 134 | - public function testGetChildExpired(): void { |
|
| 135 | - $direct = Direct::fromParams([ |
|
| 136 | - 'expiration' => 41, |
|
| 137 | - ]); |
|
| 134 | + public function testGetChildExpired(): void { |
|
| 135 | + $direct = Direct::fromParams([ |
|
| 136 | + 'expiration' => 41, |
|
| 137 | + ]); |
|
| 138 | 138 | |
| 139 | - $this->directMapper->method('getByToken') |
|
| 140 | - ->with('longtoken') |
|
| 141 | - ->willReturn($direct); |
|
| 139 | + $this->directMapper->method('getByToken') |
|
| 140 | + ->with('longtoken') |
|
| 141 | + ->willReturn($direct); |
|
| 142 | 142 | |
| 143 | - $this->throttler->expects($this->never()) |
|
| 144 | - ->method($this->anything()); |
|
| 143 | + $this->throttler->expects($this->never()) |
|
| 144 | + ->method($this->anything()); |
|
| 145 | 145 | |
| 146 | - $this->expectException(NotFound::class); |
|
| 146 | + $this->expectException(NotFound::class); |
|
| 147 | 147 | |
| 148 | - $this->directHome->getChild('longtoken'); |
|
| 149 | - } |
|
| 148 | + $this->directHome->getChild('longtoken'); |
|
| 149 | + } |
|
| 150 | 150 | |
| 151 | - public function testGetChildInvalid(): void { |
|
| 152 | - $this->directMapper->method('getByToken') |
|
| 153 | - ->with('longtoken') |
|
| 154 | - ->willThrowException(new DoesNotExistException('not found')); |
|
| 151 | + public function testGetChildInvalid(): void { |
|
| 152 | + $this->directMapper->method('getByToken') |
|
| 153 | + ->with('longtoken') |
|
| 154 | + ->willThrowException(new DoesNotExistException('not found')); |
|
| 155 | 155 | |
| 156 | - $this->throttler->expects($this->once()) |
|
| 157 | - ->method('registerAttempt') |
|
| 158 | - ->with( |
|
| 159 | - 'directlink', |
|
| 160 | - '1.2.3.4' |
|
| 161 | - ); |
|
| 162 | - $this->throttler->expects($this->once()) |
|
| 163 | - ->method('sleepDelayOrThrowOnMax') |
|
| 164 | - ->with( |
|
| 165 | - '1.2.3.4', |
|
| 166 | - 'directlink' |
|
| 167 | - ); |
|
| 156 | + $this->throttler->expects($this->once()) |
|
| 157 | + ->method('registerAttempt') |
|
| 158 | + ->with( |
|
| 159 | + 'directlink', |
|
| 160 | + '1.2.3.4' |
|
| 161 | + ); |
|
| 162 | + $this->throttler->expects($this->once()) |
|
| 163 | + ->method('sleepDelayOrThrowOnMax') |
|
| 164 | + ->with( |
|
| 165 | + '1.2.3.4', |
|
| 166 | + 'directlink' |
|
| 167 | + ); |
|
| 168 | 168 | |
| 169 | - $this->expectException(NotFound::class); |
|
| 169 | + $this->expectException(NotFound::class); |
|
| 170 | 170 | |
| 171 | - $this->directHome->getChild('longtoken'); |
|
| 172 | - } |
|
| 171 | + $this->directHome->getChild('longtoken'); |
|
| 172 | + } |
|
| 173 | 173 | } |