2017-04-13 16:50:44 -04:00
< ? php
2020-07-09 06:25:57 -04:00
2020-03-19 07:09:57 -04:00
declare ( strict_types = 1 );
2020-08-24 08:54:25 -04:00
2017-04-13 16:50:44 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2017-04-13 16:50:44 -04:00
*/
namespace OC\AppFramework\Middleware\Security ;
use OC\AppFramework\Utility\ControllerMethodReflector ;
2020-03-19 07:09:57 -04:00
use OCP\AppFramework\Controller ;
use OCP\AppFramework\Http ;
2023-02-28 16:26:22 -05:00
use OCP\AppFramework\Http\Attribute\BruteForceProtection ;
2017-04-13 16:50:44 -04:00
use OCP\AppFramework\Http\Response ;
2020-03-19 07:09:57 -04:00
use OCP\AppFramework\Http\TooManyRequestsResponse ;
2017-04-13 16:50:44 -04:00
use OCP\AppFramework\Middleware ;
2020-03-19 07:09:57 -04:00
use OCP\AppFramework\OCS\OCSException ;
use OCP\AppFramework\OCSController ;
2017-04-13 16:50:44 -04:00
use OCP\IRequest ;
2023-08-28 09:50:45 -04:00
use OCP\Security\Bruteforce\IThrottler ;
2020-03-19 07:09:57 -04:00
use OCP\Security\Bruteforce\MaxDelayReached ;
2023-03-08 06:08:53 -05:00
use Psr\Log\LoggerInterface ;
2023-02-28 16:26:22 -05:00
use ReflectionMethod ;
2017-04-13 16:50:44 -04:00
/**
* Class BruteForceMiddleware performs the bruteforce protection for controllers
* that are annotated with @ BruteForceProtection ( action = $action ) whereas $action
* is the action that should be logged within the database .
*
* @ package OC\AppFramework\Middleware\Security
*/
class BruteForceMiddleware extends Middleware {
2023-08-14 13:16:31 -04:00
private int $delaySlept = 0 ;
2023-03-08 06:08:53 -05:00
public function __construct (
protected ControllerMethodReflector $reflector ,
2023-08-28 09:50:45 -04:00
protected IThrottler $throttler ,
2023-03-08 06:08:53 -05:00
protected IRequest $request ,
protected LoggerInterface $logger ,
) {
2017-04-13 16:50:44 -04:00
}
/**
* { @ inheritDoc }
*/
2017-08-01 11:32:03 -04:00
public function beforeController ( $controller , $methodName ) {
2017-04-13 16:50:44 -04:00
parent :: beforeController ( $controller , $methodName );
if ( $this -> reflector -> hasAnnotation ( 'BruteForceProtection' )) {
$action = $this -> reflector -> getAnnotationParameter ( 'BruteForceProtection' , 'action' );
2023-08-14 13:16:31 -04:00
$this -> delaySlept += $this -> throttler -> sleepDelayOrThrowOnMax ( $this -> request -> getRemoteAddress (), $action );
2023-02-28 16:26:22 -05:00
} else {
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
$attributes = $reflectionMethod -> getAttributes ( BruteForceProtection :: class );
if ( ! empty ( $attributes )) {
$remoteAddress = $this -> request -> getRemoteAddress ();
foreach ( $attributes as $attribute ) {
/** @var BruteForceProtection $protection */
$protection = $attribute -> newInstance ();
$action = $protection -> getAction ();
2023-08-14 13:16:31 -04:00
$this -> delaySlept += $this -> throttler -> sleepDelayOrThrowOnMax ( $remoteAddress , $action );
2023-02-28 16:26:22 -05:00
}
}
2017-04-13 16:50:44 -04:00
}
}
/**
* { @ inheritDoc }
*/
2017-08-01 11:32:03 -04:00
public function afterController ( $controller , $methodName , Response $response ) {
2023-02-28 16:26:22 -05:00
if ( $response -> isThrottled ()) {
2023-05-11 03:17:30 -04:00
try {
if ( $this -> reflector -> hasAnnotation ( 'BruteForceProtection' )) {
$action = $this -> reflector -> getAnnotationParameter ( 'BruteForceProtection' , 'action' );
2023-02-28 16:26:22 -05:00
$ip = $this -> request -> getRemoteAddress ();
2023-05-11 03:17:30 -04:00
$this -> throttler -> registerAttempt ( $action , $ip , $response -> getThrottleMetadata ());
2023-08-14 13:16:31 -04:00
$this -> delaySlept += $this -> throttler -> sleepDelayOrThrowOnMax ( $ip , $action );
2023-05-11 03:17:30 -04:00
} else {
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
$attributes = $reflectionMethod -> getAttributes ( BruteForceProtection :: class );
if ( ! empty ( $attributes )) {
$ip = $this -> request -> getRemoteAddress ();
$metaData = $response -> getThrottleMetadata ();
foreach ( $attributes as $attribute ) {
/** @var BruteForceProtection $protection */
$protection = $attribute -> newInstance ();
$action = $protection -> getAction ();
if ( ! isset ( $metaData [ 'action' ]) || $metaData [ 'action' ] === $action ) {
$this -> throttler -> registerAttempt ( $action , $ip , $metaData );
2023-08-14 13:16:31 -04:00
$this -> delaySlept += $this -> throttler -> sleepDelayOrThrowOnMax ( $ip , $action );
2023-05-11 03:17:30 -04:00
}
2023-02-28 16:26:22 -05:00
}
2023-05-11 03:17:30 -04:00
} else {
$this -> logger -> debug ( 'Response for ' . get_class ( $controller ) . '::' . $methodName . ' got bruteforce throttled but has no annotation nor attribute defined.' );
2023-02-28 16:26:22 -05:00
}
}
2023-05-11 03:17:30 -04:00
} catch ( MaxDelayReached $e ) {
if ( $controller instanceof OCSController ) {
throw new OCSException ( $e -> getMessage (), Http :: STATUS_TOO_MANY_REQUESTS );
}
return new TooManyRequestsResponse ();
2023-02-28 16:26:22 -05:00
}
2017-04-13 16:50:44 -04:00
}
2023-08-14 13:16:31 -04:00
if ( $this -> delaySlept ) {
2023-08-22 10:00:39 -04:00
$response -> addHeader ( 'X-Nextcloud-Bruteforce-Throttled' , $this -> delaySlept . 'ms' );
2023-08-14 13:16:31 -04:00
}
2017-04-13 16:50:44 -04:00
return parent :: afterController ( $controller , $methodName , $response );
}
2020-03-19 07:09:57 -04:00
/**
* @ param Controller $controller
* @ param string $methodName
* @ param \Exception $exception
* @ throws \Exception
* @ return Response
*/
public function afterException ( $controller , $methodName , \Exception $exception ) : Response {
if ( $exception instanceof MaxDelayReached ) {
if ( $controller instanceof OCSController ) {
throw new OCSException ( $exception -> getMessage (), Http :: STATUS_TOO_MANY_REQUESTS );
}
return new TooManyRequestsResponse ();
}
throw $exception ;
}
2017-04-13 16:50:44 -04:00
}