Skip to content

Commit 6e9c9b0

Browse files
committed
Added internal IP resolver and Cloudflare example
1 parent c52740e commit 6e9c9b0

File tree

8 files changed

+135
-11
lines changed

8 files changed

+135
-11
lines changed

Form/Recaptcha3Type.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Symfony\Component\Form\FormView;
99
use Symfony\Component\OptionsResolver\OptionsResolver;
1010

11-
class Recaptcha3Type extends AbstractType
11+
final class Recaptcha3Type extends AbstractType
1212
{
1313
/** @var string */
1414
private $siteKey;

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,65 @@ karser_recaptcha3:
163163
RECAPTCHA3_ENABLED=0
164164
```
165165

166+
### How to add Cloudflare IP resolver:
167+
168+
From the [Cloudflare docs](https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-):
169+
To provide the client (visitor) IP address for every request to the origin, Cloudflare adds the CF-Connecting-IP header.
170+
```
171+
"CF-Connecting-IP: A.B.C.D"
172+
```
173+
174+
So you can implement custom IP resolver which attempts to read the `CF-Connecting-IP` header or fallbacks with the internal IP resolver:
175+
176+
```php
177+
<?php declare(strict_types=1);
178+
179+
namespace App\Service;
180+
181+
use Karser\Recaptcha3Bundle\Services\IpResolverInterface;
182+
use Symfony\Component\HttpFoundation\RequestStack;
183+
184+
class CloudflareIpResolver implements IpResolverInterface
185+
{
186+
/** @var IpResolverInterface */
187+
private $decorated;
188+
189+
/** @var RequestStack */
190+
private $requestStack;
191+
192+
public function __construct(IpResolverInterface $decorated, RequestStack $requestStack)
193+
{
194+
$this->decorated = $decorated;
195+
$this->requestStack = $requestStack;
196+
}
197+
198+
public function resolveIp(): ?string
199+
{
200+
return $this->doResolveIp() ?? $this->decorated->resolveIp();
201+
}
202+
203+
private function doResolveIp(): ?string
204+
{
205+
$request = $this->requestStack->getCurrentRequest();
206+
if ($request === null) {
207+
return null;
208+
}
209+
return $request->server->get('HTTP_CF_CONNECTING_IP');
210+
}
211+
}
212+
```
213+
214+
Here is the service declaration. It decorates the internal resolver:
215+
```yaml
216+
#services.yaml
217+
services:
218+
App\Service\CloudflareIpResolver:
219+
decorates: 'karser_recaptcha3.ip_resolver'
220+
arguments:
221+
$decorated: '@App\Service\CloudflareIpResolver.inner'
222+
$requestStack: '@request_stack'
223+
```
224+
166225
Testing
167226
-------
168227

Resources/config/services.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ services:
1414
arguments:
1515
- '@karser_recaptcha3.google.recaptcha'
1616
- '%karser_recaptcha3.enabled%'
17-
- '@request_stack'
17+
- '@karser_recaptcha3.ip_resolver'
1818
tags:
1919
- { name: validator.constraint_validator, alias: karser_recaptcha3_validator }
2020

21+
karser_recaptcha3.ip_resolver:
22+
class: Karser\Recaptcha3Bundle\Services\IpResolver
23+
public: false
24+
arguments:
25+
- '@request_stack'
26+
2127
karser_recaptcha3.google.recaptcha:
2228
class: 'ReCaptcha\ReCaptcha'
2329
arguments:

Services/IpResolver.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Karser\Recaptcha3Bundle\Services;
4+
5+
use Symfony\Component\HttpFoundation\RequestStack;
6+
7+
final class IpResolver implements IpResolverInterface
8+
{
9+
/** @var RequestStack */
10+
private $requestStack;
11+
12+
public function __construct(RequestStack $requestStack)
13+
{
14+
$this->requestStack = $requestStack;
15+
}
16+
17+
public function resolveIp(): ?string
18+
{
19+
$request = $this->requestStack->getCurrentRequest();
20+
if ($request === null) {
21+
return null;
22+
}
23+
return $request->getClientIp();
24+
}
25+
}

Services/IpResolverInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Karser\Recaptcha3Bundle\Services;
4+
5+
interface IpResolverInterface
6+
{
7+
public function resolveIp(): ?string;
8+
}

Tests/Services/IpResolverTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Karser\Recaptcha3Bundle\Tests\Services;
4+
5+
use Karser\Recaptcha3Bundle\Services\IpResolver;
6+
use PHPUnit\Framework\TestCase;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpFoundation\RequestStack;
9+
10+
class IpResolverTest extends TestCase
11+
{
12+
public function testEmptyRequest()
13+
{
14+
$stack = new RequestStack();
15+
$stack->push(new Request());
16+
$resolver = new IpResolver($stack);
17+
self::assertNull($resolver->resolveIp());
18+
}
19+
20+
public function testRequest()
21+
{
22+
$stack = new RequestStack();
23+
$stack->push(new Request([], [], [], [], [], ['REMOTE_ADDR' => '0.0.0.0']));
24+
$resolver = new IpResolver($stack);
25+
self::assertSame('0.0.0.0', $resolver->resolveIp());
26+
}
27+
}

Validator/Constraints/Recaptcha3.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* @Annotation
99
*/
10-
class Recaptcha3 extends Constraint
10+
final class Recaptcha3 extends Constraint
1111
{
1212
public $message = 'Your computer or network may be sending automated queries';
1313
}

Validator/Constraints/Recaptcha3Validator.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,28 @@
22

33
namespace Karser\Recaptcha3Bundle\Validator\Constraints;
44

5+
use Karser\Recaptcha3Bundle\Services\IpResolverInterface;
56
use ReCaptcha\ReCaptcha;
6-
use Symfony\Component\HttpFoundation\RequestStack;
77
use Symfony\Component\Validator\Constraint;
88
use Symfony\Component\Validator\ConstraintValidator;
99
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
1010

11-
class Recaptcha3Validator extends ConstraintValidator
11+
final class Recaptcha3Validator extends ConstraintValidator
1212
{
1313
/** @var ReCaptcha */
1414
private $recaptcha;
1515

1616
/** @var bool */
1717
private $enabled;
1818

19-
/** @var RequestStack */
20-
private $requestStack;
19+
/** @var IpResolverInterface */
20+
private $ipResolver;
2121

22-
public function __construct($recaptcha, bool $enabled, RequestStack $requestStack)
22+
public function __construct($recaptcha, bool $enabled, IpResolverInterface $ipResolver)
2323
{
2424
$this->recaptcha = $recaptcha;
2525
$this->enabled = $enabled;
26-
$this->requestStack = $requestStack;
26+
$this->ipResolver = $ipResolver;
2727
}
2828

2929
public function validate($value, Constraint $constraint): void
@@ -36,8 +36,7 @@ public function validate($value, Constraint $constraint): void
3636
return;
3737
}
3838

39-
$request = $this->requestStack->getCurrentRequest();
40-
$ip = $request ? $request->server->get('HTTP_CF_CONNECTING_IP') ?? $request->getClientIp() : null;
39+
$ip = $this->ipResolver->resolveIp();
4140

4241
$response = $this->recaptcha->verify($value, $ip);
4342
if (!$response->isSuccess()) {

0 commit comments

Comments
 (0)