Skip to content

Commit 9da74bd

Browse files
dbuddeboer
authored andcommitted
Anonymous user context lookup (#330)
* make anonymous hash header same between varnish and symfony * always do a hash lookup, even for anonymous requests
1 parent 61cf5dc commit 9da74bd

File tree

9 files changed

+121
-36
lines changed

9 files changed

+121
-36
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC
3636
* Moved Varnish 4 and 5 configuration files from `resources/config/varnish-4/`
3737
to `resources/config/varnish/`.
3838
* Changed default Varnish version to 5.
39+
* Removed special case for anonymous users in user context behaviour. Varnish
40+
now does a hash lookup for anonymous users as well.
3941

4042
### NGINX
4143

@@ -49,6 +51,8 @@ See also the [GitHub releases page](https://github.com/FriendsOfSymfony/FOSHttpC
4951
options array for customization.
5052
* Provide a trait for the event dispatching kernel, instead of a base class.
5153
The trait offers both the addSubscriber and the addListener methods.
54+
* The user context by default does not use a hardcoded hash for anonymous users
55+
but does a hash lookup. You can still configure a hardcoded hash.
5256

5357
### Testing
5458

doc/symfony-cache-configuration.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,10 @@ based on session cookies or authorization headers. If the default settings are
180180
right for you, you don't need to do anything more. You can customize a number of
181181
options through the constructor:
182182

183-
* **anonymous_hash**: Hash used for anonymous user. This is a performance
184-
optimization to not do a backend request for users that are not logged in.
183+
* **anonymous_hash**: Hard-coded hash to use for anonymous users. This is a
184+
performance optimization to not do a backend request for users that are not
185+
logged in. If you specify a non-empty value for this field, that is used as
186+
context hash header instead of doing a hash lookup for anonymous users.
185187

186188
* **user_hash_accept_header**: Accept header value to be used to request the
187189
user hash to the backend application. Must match the setup of the backend

doc/user-context.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ client, moving step 2-4 into the cache. After the page is in cache, subsequent
6262
requests from clients that got the same hash can be served from the cache as
6363
well.
6464

65+
.. note::
66+
67+
If your application starts sessions for anonymous users, you will get one
68+
hash lookup request for each of those users. Your application can return
69+
the same hash for authenticated users with no special privileges as for
70+
anonymous users with a session cookie.
71+
72+
If there is no cookie and no authentication information, the hash lookup is
73+
skipped and no hash header added to the request. However, we can not avoid
74+
the initial hash lookup request per different cookie, as the caching proxy
75+
can not know which session cookies indicate a logged in user and which an
76+
anonymous session.
77+
6578
Proxy Client Configuration
6679
--------------------------
6780

doc/varnish-configuration.rst

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Purge removes a specific URL (including query strings) in all its variants (as
5555
specified by the ``Vary`` header).
5656

5757
Subroutines are provided in ``resources/config/varnish-[version]/fos_purge.vcl``.
58-
To enable support add the following to ``your_varnish.vcl``:
58+
To enable this feature, add the following to ``your_varnish.vcl``:
5959

6060
.. configuration-block::
6161

@@ -96,7 +96,7 @@ Refreshing applies only to a specific URL including the query string, but *not*
9696
its variants.
9797

9898
Subroutines are provided in ``resources/config/varnish-[version]/fos_refresh.vcl``.
99-
To enable support, add the following to ``your_varnish.vcl``:
99+
To enable this feature, add the following to ``your_varnish.vcl``:
100100

101101
.. configuration-block::
102102

@@ -125,7 +125,7 @@ Ban
125125
Banning invalidates whole groups of cached entries with regular expressions.
126126

127127
Subroutines are provided in ``resources/config/varnish-[version]/fos_ban.vcl``
128-
To enable support add the following to ``your_varnish.vcl``:
128+
To enable this feature, add the following to ``your_varnish.vcl``:
129129

130130
.. configuration-block::
131131

@@ -202,11 +202,26 @@ User Context
202202

203203
Feature: :doc:`user context hashing <user-context>`
204204

205-
The ``fos_user_context.vcl`` needs the ``user_context_hash_url`` subroutine that sets a URL to the request lookup URL. The default URL is ``/_fos_user_context_hash`` and you can simply include ``resources/config/varnish-[version]/fos_user_context_url.vcl`` in your configuration to provide this. If you need a different URL, include a custom file implementing the ``user_context_hash_url`` subroutine.
205+
The ``fos_user_context.vcl`` needs the ``user_context_hash_url`` subroutine
206+
that sets the URL to do the hash lookup. The default URL is
207+
``/_fos_user_context_hash`` and you can simply include
208+
``resources/config/varnish-[version]/fos_user_context_url.vcl`` in your
209+
configuration to provide this. If you need a different URL, write your own
210+
``user_context_hash_url`` subroutine instead.
206211

212+
.. tip::
213+
214+
The provided VCL to fetch the user hash restarts GET/HEAD requests. It
215+
would be more efficient to do the hash lookup request with curl, using the
216+
`curl Varnish plugin`_. If you can enable curl support, the recommended way
217+
is to implement your own VCL to do a curl request for the hash lookup
218+
instead of using the VCL provided here.
207219

208-
To enable support add the following to ``your_varnish.vcl``:
220+
Also note that restarting a GET request leads to Varnish discarding the
221+
body of the request. If you have some special case where you have GET
222+
requests with a body, use curl.
209223

224+
To enable this feature, add the following to ``your_varnish.vcl``:
210225

211226
.. configuration-block::
212227

@@ -262,13 +277,6 @@ To enable support add the following to ``your_varnish.vcl``:
262277
Your backend application needs to respond to the ``application/vnd.fos.user-context-hash``
263278
request with :ref:`a proper user hash <return context hash>`.
264279

265-
.. note::
266-
267-
We do not use ``X-Original-Url`` here, as the header will be sent to the
268-
backend and the header has semantical meaning for some applications, which
269-
would lead to problems. For example, the Microsoft IIS rewriting module
270-
uses it, and consequently Symfony also looks into it to support IIS.
271-
272280
.. tip::
273281

274282
The provided VCL assumes that you want the context hash to be cached, so we
@@ -358,7 +366,7 @@ sends an ``X-Cache-Debug`` header:
358366

359367
Subroutines are provided in ``fos_debug.vcl``.
360368

361-
To enable support add the following to ``your_varnish.vcl``:
369+
To enable this feature, add the following to ``your_varnish.vcl``:
362370

363371
.. configuration-block::
364372

@@ -388,5 +396,6 @@ To enable support add the following to ``your_varnish.vcl``:
388396
.. _banning for Varnish 3: https://www.varnish-software.com/book/3/Cache_invalidation.html#banning
389397
.. _ban lurker: https://www.varnish-software.com/blog/ban-lurker
390398
.. _explained in the Varnish documentation: https://www.varnish-cache.org/trac/wiki/VCLExampleRemovingSomeCookies#RemovingallBUTsomecookies
391-
.. _`builtin VCL`: https://www.varnish-cache.org/trac/browser/bin/varnishd/builtin.vcl?rev=4.0
392-
.. _`default VCL`: https://www.varnish-cache.org/trac/browser/bin/varnishd/default.vcl?rev=3.0
399+
.. _curl Varnish plugin: https://github.com/varnish/libvmod-curl
400+
.. _`builtin VCL`: https://github.com/varnishcache/varnish-cache/blob/5.0/bin/varnishd/builtin.vcl
401+
.. _`default VCL`: https://github.com/varnishcache/varnish-cache/blob/3.0/bin/varnishd/default.vcl

resources/config/varnish-3/fos_user_context.vcl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ sub fos_user_context_recv {
1919
}
2020

2121
# Lookup the context hash if there are credentials on the request
22-
# Only do this for cacheable requests. Returning a hash lookup discards the request body.
22+
# Note that the hash lookup discards the request body.
2323
# https://www.varnish-cache.org/trac/ticket/652
2424
if (req.restarts == 0
25-
&& (req.http.cookie || req.http.authorization)
2625
&& (req.request == "GET" || req.request == "HEAD")
2726
) {
2827
# Backup accept header, if set
@@ -31,9 +30,15 @@ sub fos_user_context_recv {
3130
}
3231
set req.http.accept = "application/vnd.fos.user-context-hash";
3332

34-
# Backup original URL
33+
# Backup original URL.
34+
#
35+
# We do not use X-Original-Url here, as the header will be sent to the
36+
# backend and X-Original-Url has semantical meaning for some applications.
37+
# For example, the Microsoft IIS rewriting module uses it, and thus
38+
# frameworks like Symfony also have to handle that header to integrate with IIS.
39+
3540
set req.http.X-Fos-Original-Url = req.url;
36-
41+
3742
call user_context_hash_url;
3843

3944
# Force the lookup, the backend must tell not to cache or vary on all

resources/config/varnish/fos_user_context.vcl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ sub fos_user_context_recv {
1919
}
2020

2121
# Lookup the context hash if there are credentials on the request
22-
# Only do this for cacheable requests. Returning a hash lookup discards the request body.
22+
# Note that the hash lookup discards the request body.
2323
# https://www.varnish-cache.org/trac/ticket/652
2424
if (req.restarts == 0
25-
&& (req.http.cookie || req.http.authorization)
2625
&& (req.method == "GET" || req.method == "HEAD")
2726
) {
2827
# Backup accept header, if set
@@ -31,9 +30,15 @@ sub fos_user_context_recv {
3130
}
3231
set req.http.accept = "application/vnd.fos.user-context-hash";
3332

34-
# Backup original URL
33+
# Backup original URL.
34+
#
35+
# We do not use X-Original-Url here, as the header will be sent to the
36+
# backend and X-Original-Url has semantical meaning for some applications.
37+
# For example, the Microsoft IIS rewriting module uses it, and thus
38+
# frameworks like Symfony also have to handle that header to integrate with IIS.
39+
3540
set req.http.X-Fos-Original-Url = req.url;
36-
41+
3742
call user_context_hash_url;
3843

3944
# Force the lookup, the backend must tell not to cache or vary on all

src/SymfonyCache/UserContextListener.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class UserContextListener implements EventSubscriberInterface
4343
/**
4444
* When creating this listener, you can configure a number of options.
4545
*
46-
* - anonymous_hash: Hash used for anonymous user.
46+
* - anonymous_hash: Hash used for anonymous user. Hash lookup skipped for anonymous if this is set.
4747
* - user_hash_accept_header: Accept header value to be used to request the user hash to the
4848
* backend application. Must match the setup of the backend application.
4949
* - user_hash_header: Name of the header the user context hash will be stored into. Must
@@ -60,7 +60,7 @@ public function __construct(array $options = [])
6060
{
6161
$resolver = new OptionsResolver();
6262
$resolver->setDefaults([
63-
'anonymous_hash' => '38015b703d82206ebc01d17a39c727e5',
63+
'anonymous_hash' => null,
6464
'user_hash_accept_header' => 'application/vnd.fos.user-context-hash',
6565
'user_hash_header' => 'X-User-Context-Hash',
6666
'user_hash_uri' => '/_fos_user_context_hash',
@@ -104,8 +104,8 @@ public function preHandle(CacheEvent $event)
104104
return;
105105
}
106106

107-
if ($request->isMethodSafe()) {
108-
$request->headers->set($this->options['user_hash_header'], $this->getUserHash($event->getKernel(), $request));
107+
if ($request->isMethodSafe() && $hash = $this->getUserHash($event->getKernel(), $request)) {
108+
$request->headers->set($this->options['user_hash_header'], $hash);
109109
}
110110
}
111111

@@ -162,7 +162,7 @@ private function getUserHash(HttpKernelInterface $kernel, Request $request)
162162
return $this->userHash;
163163
}
164164

165-
if ($this->isAnonymous($request)) {
165+
if ($this->options['anonymous_hash'] && $this->isAnonymous($request)) {
166166
return $this->userHash = $this->options['anonymous_hash'];
167167
}
168168

tests/Functional/Symfony/EventDispatchingHttpCacheTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ public function testEventListeners()
5050
$kernel->addSubscriber(new DebugListener());
5151
$kernel->addSubscriber(new PurgeListener());
5252
$kernel->addSubscriber(new RefreshListener());
53-
$kernel->addSubscriber(new UserContextListener());
53+
$kernel->addSubscriber(new UserContextListener([
54+
// avoid having to provide mocking for the hash lookup
55+
// we already test anonymous hash lookup in the UserContextListener unit test
56+
'anonymous_hash' => 'abcdef',
57+
]));
5458

5559
$response = $kernel->handle($request);
5660
$this->assertSame($expectedResponse, $response);

tests/Unit/SymfonyCache/UserContextListenerTest.php

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,60 @@ public function testPassingUserHashNotAllowed($arg, $options)
9898
public function testUserHashAnonymous($arg, $options)
9999
{
100100
$userContextListener = new UserContextListener($arg);
101-
102101
$request = new Request();
103102

104-
$event = new CacheEvent($this->kernel, $request);
103+
if ($options['anonymous_hash']) {
104+
$event = new CacheEvent($this->kernel, $request);
105+
$userContextListener->preHandle($event);
106+
107+
$this->assertTrue($request->headers->has($options['user_hash_header']));
108+
$this->assertSame($options['anonymous_hash'], $request->headers->get($options['user_hash_header']));
109+
} else {
110+
$hashRequest = Request::create($options['user_hash_uri'], $options['user_hash_method'], [], [], [], $request->server->all());
111+
$hashRequest->attributes->set('internalRequest', true);
112+
$hashRequest->headers->set('Accept', $options['user_hash_accept_header']);
113+
// Ensure request properties have been filled up.
114+
$hashRequest->getPathInfo();
115+
$hashRequest->getMethod();
116+
117+
$expectedContextHash = 'my_generated_hash';
118+
// Just avoid the response to modify the request object, otherwise it's impossible to test objects equality.
119+
/** @var \Symfony\Component\HttpFoundation\Response|\PHPUnit_Framework_MockObject_MockObject $hashResponse */
120+
$hashResponse = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Response')
121+
->setMethods(['prepare'])
122+
->getMock();
123+
$hashResponse->headers->set($options['user_hash_header'], $expectedContextHash);
124+
125+
$that = $this;
126+
$kernel = $this->kernel
127+
->shouldReceive('handle')
128+
->once()
129+
->with(
130+
\Mockery::on(
131+
function (Request $request) use ($that, $hashRequest) {
132+
// we need to call some methods to get the internal fields initialized
133+
$request->getMethod();
134+
$request->getPathInfo();
135+
$that->assertEquals($hashRequest, $request);
136+
$that->assertCount(0, $request->cookies->all());
137+
138+
return true;
139+
}
140+
)
141+
)
142+
->andReturn($hashResponse)
143+
->getMock();
144+
145+
$event = new CacheEvent($kernel, $request);
146+
$userContextListener->preHandle($event);
147+
148+
$this->assertTrue($request->headers->has($options['user_hash_header']));
149+
$this->assertSame($expectedContextHash, $request->headers->get($options['user_hash_header']));
150+
}
105151

106-
$userContextListener->preHandle($event);
107152
$response = $event->getResponse();
108153

109154
$this->assertNull($response);
110-
$this->assertTrue($request->headers->has($options['user_hash_header']));
111-
$this->assertSame($options['anonymous_hash'], $request->headers->get($options['user_hash_header']));
112155
}
113156

114157
/**

0 commit comments

Comments
 (0)