Skip to content

Commit 5ffcbf5

Browse files
authored
Do not sanity check hash of anonymous requests (#289)
2 parents fa6b5a4 + 0990981 commit 5ffcbf5

File tree

7 files changed

+186
-3
lines changed

7 files changed

+186
-3
lines changed

DependencyInjection/FOSHttpCacheExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ private function loadUserContext(ContainerBuilder $container, XmlFileLoader $loa
192192
->replaceArgument(3, $config['user_hash_header'])
193193
->replaceArgument(4, $config['hash_cache_ttl']);
194194

195+
$container->getDefinition($this->getAlias().'.user_context.anonymous_request_matcher')
196+
->replaceArgument(0, $config['user_identifier_headers']);
197+
195198
if ($config['logout_handler']['enabled']) {
196199
$container->getDefinition($this->getAlias().'.user_context.logout_handler')
197200
->replaceArgument(1, $config['user_identifier_headers'])

EventListener/UserContextSubscriber.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use FOS\HttpCache\UserContext\HashGenerator;
1515
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\HttpFoundation\Request;
1617
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
1718
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
1819
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@@ -61,12 +62,22 @@ class UserContextSubscriber implements EventSubscriberInterface
6162
*/
6263
private $ttl;
6364

65+
/**
66+
* Used to exclude anonymous requests (no authentication nor session) from user hash sanity check.
67+
* It prevents issues when the hash generator that is used returns a customized value for anonymous users,
68+
* that differs from the documented, hardcoded one.
69+
*
70+
* @var RequestMatcherInterface
71+
*/
72+
private $anonymousRequestMatcher;
73+
6474
public function __construct(
6575
RequestMatcherInterface $requestMatcher,
6676
HashGenerator $hashGenerator,
6777
array $userIdentifierHeaders = array('Cookie', 'Authorization'),
6878
$hashHeader = 'X-User-Context-Hash',
69-
$ttl = 0
79+
$ttl = 0,
80+
RequestMatcherInterface $anonymousRequestMatcher = null
7081
) {
7182
if (!count($userIdentifierHeaders)) {
7283
throw new \InvalidArgumentException('The user context must vary on some request headers');
@@ -76,6 +87,7 @@ public function __construct(
7687
$this->userIdentifierHeaders = $userIdentifierHeaders;
7788
$this->hashHeader = $hashHeader;
7889
$this->ttl = $ttl;
90+
$this->anonymousRequestMatcher = $anonymousRequestMatcher;
7991
}
8092

8193
/**
@@ -93,7 +105,7 @@ public function onKernelRequest(GetResponseEvent $event)
93105
}
94106

95107
if (!$this->requestMatcher->matches($event->getRequest())) {
96-
if ($event->getRequest()->headers->has($this->hashHeader)) {
108+
if ($event->getRequest()->headers->has($this->hashHeader) && !$this->isAnonymous($event->getRequest())) {
97109
$this->hash = $this->hashGenerator->generateHash();
98110
}
99111

@@ -120,6 +132,20 @@ public function onKernelRequest(GetResponseEvent $event)
120132
$event->setResponse($response);
121133
}
122134

135+
/**
136+
* Tests if $request is an anonymous request or not.
137+
*
138+
* For backward compatibility reasons, true will be returned if no anonymous request matcher was provided.
139+
*
140+
* @param Request $request
141+
*
142+
* @return bool
143+
*/
144+
private function isAnonymous(Request $request)
145+
{
146+
return $this->anonymousRequestMatcher ? $this->anonymousRequestMatcher->matches($request) : false;
147+
}
148+
123149
/**
124150
* Add the context hash header to the headers to vary on if the header was
125151
* present in the request.

Resources/config/user_context.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<argument />
2828
<argument />
2929
<argument />
30+
<argument type="service" id="fos_http_cache.user_context.anonymous_request_matcher" />
3031
<tag name="kernel.event_subscriber" />
3132
</service>
3233

@@ -39,5 +40,9 @@
3940
<argument />
4041
<argument />
4142
</service>
43+
44+
<service id="fos_http_cache.user_context.anonymous_request_matcher" class="FOS\HttpCacheBundle\UserContext\AnonymousRequestMatcher">
45+
<argument type="collection" />
46+
</service>
4247
</services>
4348
</container>

Tests/Unit/EventListener/UserContextSubscriberTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,39 @@ public function testFullRequestHashOk()
198198
$this->assertContains('X-Hash', $event->getResponse()->headers->get('Vary'));
199199
}
200200

201+
/**
202+
* If the request is an anonymous one, no hash should be generated/validated.
203+
*/
204+
public function testFullAnonymousRequestHashNotGenerated()
205+
{
206+
$request = new Request();
207+
$request->setMethod('GET');
208+
$request->headers->set('X-Hash', 'anonymous-hash');
209+
210+
$requestMatcher = $this->getRequestMatcher($request, false);
211+
$hashGenerator = \Mockery::mock('\FOS\HttpCache\UserContext\HashGenerator');
212+
$hashGenerator->shouldReceive('generateHash')->never();
213+
214+
$anonymousRequestMatcher = \Mockery::mock('\Symfony\Component\HttpFoundation\RequestMatcherInterface');
215+
$anonymousRequestMatcher->shouldReceive('matches')->andReturn(true);
216+
217+
// onKernelRequest
218+
$userContextSubscriber = new UserContextSubscriber($requestMatcher, $hashGenerator, array('X-SessionId'), 'X-Hash', 0, $anonymousRequestMatcher);
219+
$event = $this->getKernelRequestEvent($request);
220+
221+
$userContextSubscriber->onKernelRequest($event);
222+
223+
$response = $event->getResponse();
224+
225+
$this->assertNull($response);
226+
227+
// onKernelResponse
228+
$event = $this->getKernelResponseEvent($request);
229+
$userContextSubscriber->onKernelResponse($event);
230+
231+
$this->assertContains('X-Hash', $event->getResponse()->headers->get('Vary'));
232+
}
233+
201234
/**
202235
* If there is no hash in the requests but session changed, prevent setting bad cache.
203236
*/
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCacheBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCacheBundle\Tests\Unit\UserContext;
13+
14+
use FOS\HttpCacheBundle\UserContext\AnonymousRequestMatcher;
15+
use PHPUnit_Framework_TestCase;
16+
use Symfony\Component\HttpFoundation\Request;
17+
18+
class AnonymousRequestMatcherTest extends PHPUnit_Framework_TestCase
19+
{
20+
public function testMatchAnonymousRequest()
21+
{
22+
$request = new Request();
23+
24+
$requestMatcher = new AnonymousRequestMatcher(['Cookie', 'Authorization']);
25+
26+
$this->assertTrue($requestMatcher->matches($request));
27+
}
28+
29+
public function testNoMatchIfCookie()
30+
{
31+
$request = new Request();
32+
$request->headers->set('Cookie', 'PHPSESSID7e476fc9f29f69d2ad6f11dbcd663b42=25f6d9c5a843e3c948cd26902385a527');
33+
$request->cookies->set('PHPSESSID7e476fc9f29f69d2ad6f11dbcd663b42', '25f6d9c5a843e3c948cd26902385a527');
34+
35+
$requestMatcher = new AnonymousRequestMatcher(['Cookie', 'Authorization']);
36+
37+
$this->assertFalse($requestMatcher->matches($request));
38+
}
39+
40+
public function testNoMatchIfEmptyCookieHeader()
41+
{
42+
$request = new Request();
43+
$request->headers->set('Cookie', '');
44+
45+
$requestMatcher = new AnonymousRequestMatcher(['Cookie', 'Authorization']);
46+
47+
$this->assertTrue($requestMatcher->matches($request));
48+
}
49+
50+
public function testNoMatchIfAuthenticationHeader()
51+
{
52+
$request = new Request();
53+
$request->headers->set('Authorization', 'foo: bar');
54+
55+
$requestMatcher = new AnonymousRequestMatcher(['Cookie', 'Authorization']);
56+
57+
$this->assertFalse($requestMatcher->matches($request));
58+
}
59+
60+
public function testMatchEmptyCookieHeaderHeader()
61+
{
62+
$request = new Request();
63+
$request->headers->set('Cookie', '');
64+
65+
$requestMatcher = new AnonymousRequestMatcher(['Cookie', 'Authorization']);
66+
67+
$this->assertTrue($requestMatcher->matches($request));
68+
}
69+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCacheBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCacheBundle\UserContext;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
16+
17+
/**
18+
* Matches anonymous requests using a list of identification headers.
19+
*/
20+
class AnonymousRequestMatcher implements RequestMatcherInterface
21+
{
22+
private $userIdentifierHeaders;
23+
24+
/**
25+
* @param array $userIdentifierHeaders List of request headers that authenticate a non-anonymous request
26+
*/
27+
public function __construct(array $userIdentifierHeaders)
28+
{
29+
$this->userIdentifierHeaders = $userIdentifierHeaders;
30+
}
31+
32+
public function matches(Request $request)
33+
{
34+
foreach ($this->userIdentifierHeaders as $header) {
35+
if ($request->headers->has($header)) {
36+
if (strtolower($header) === 'cookie' && 0 === $request->cookies->count()) {
37+
// ignore empty cookie header
38+
continue;
39+
}
40+
41+
return false;
42+
}
43+
}
44+
45+
return true;
46+
}
47+
}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"mockery/mockery": "0.9.*",
3030
"monolog/monolog": "*",
3131
"sensio/framework-extra-bundle": "^2.3||^3.0",
32-
"symfony/symfony": "^2.3||^3.0",
32+
"symfony/symfony": "^2.3.4||^3.0",
3333
"symfony/phpunit-bridge": "^2.7||^3.0",
3434
"symfony/expression-language": "^2.4||^3.0",
3535
"symfony/monolog-bundle": "^2.3||^3.0",

0 commit comments

Comments
 (0)