Skip to content

Commit 7f31ece

Browse files
committed
Implemented Symfony reverse proxy support for user context hash.
Ref #26 Added tests for Symfony RP
1 parent 851a001 commit 7f31ece

File tree

7 files changed

+430
-2
lines changed

7 files changed

+430
-2
lines changed

EventListener/UserContextSubscriber.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public function onKernelRequest(GetResponseEvent $event)
103103
if ($this->ttl > 0) {
104104
$response->setClientTtl($this->ttl);
105105
$response->setVary($this->userIdentifierHeaders);
106+
$response->setPublic();
106107
} else {
107108
$response->setClientTtl(0);
108109
$response->headers->addCacheControlDirective('no-cache');

HttpCache.php

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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;
13+
14+
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache as BaseHttpCache;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
19+
/**
20+
* Base class for enhanced Symfony reverse proxy.
21+
*
22+
* @author Jérôme Vieilledent <[email protected]> (courtesy of eZ Systems AS)
23+
*
24+
* {@inheritdoc}
25+
*/
26+
abstract class HttpCache extends BaseHttpCache
27+
{
28+
/**
29+
* Hash for anonymous user.
30+
*/
31+
const ANONYMOUS_HASH = '38015b703d82206ebc01d17a39c727e5';
32+
33+
/**
34+
* Accept header value to be used to request the user hash to the backend application.
35+
* It must match the one defined in FOSHttpCacheBundle's configuration.
36+
*/
37+
const USER_HASH_ACCEPT_HEADER = 'application/vnd.fos.user-context-hash';
38+
39+
/**
40+
* Name of the header the user context hash will be stored into.
41+
* It must match the one defined in FOSHttpCacheBundle's configuration.
42+
*/
43+
const USER_HASH_HEADER = 'X-User-Context-Hash';
44+
45+
/**
46+
* URI used with the forwarded request for user context hash generation.
47+
*/
48+
const USER_HASH_URI = '/_fos_user_context_hash';
49+
50+
/**
51+
* HTTP Method used with the forwarded request for user context hash generation.
52+
*/
53+
const USER_HASH_METHOD = 'GET';
54+
55+
/**
56+
* Prefix for session names.
57+
* Must match your session configuration.
58+
*/
59+
const SESSION_NAME_PREFIX = 'PHPSESSID';
60+
61+
/**
62+
* Generated user hash.
63+
*
64+
* @var string
65+
*/
66+
private $userHash;
67+
68+
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
69+
{
70+
if (!$this->isInternalRequest($request)) {
71+
// Prevent tampering attacks on the hash mechanism
72+
if ($request->headers->get('accept') === static::USER_HASH_ACCEPT_HEADER
73+
|| $request->headers->get(static::USER_HASH_HEADER) !== null) {
74+
return new Response('', 400);
75+
}
76+
77+
if ($request->isMethodSafe()) {
78+
$request->headers->set(static::USER_HASH_HEADER, $this->getUserHash($request));
79+
}
80+
}
81+
82+
return parent::handle($request, $type, $catch);
83+
}
84+
85+
/**
86+
* Checks if passed request object is to be considered internal (e.g. for user hash lookup).
87+
*
88+
* @param Request $request
89+
*
90+
* @return bool
91+
*/
92+
protected function isInternalRequest(Request $request)
93+
{
94+
return $request->attributes->get('internalRequest', false) === true;
95+
}
96+
97+
/**
98+
* Returns the user context hash for $request.
99+
*
100+
* @param Request $request
101+
*
102+
* @return string
103+
*/
104+
protected function getUserHash(Request $request)
105+
{
106+
if (isset($this->userHash)) {
107+
return $this->userHash;
108+
}
109+
110+
if ($this->isAnonymous($request)) {
111+
return $this->userHash = static::ANONYMOUS_HASH;
112+
}
113+
114+
// Forward the request to generate the user hash
115+
$forwardReq = $this->generateForwardRequest($request);
116+
$resp = $this->handle($forwardReq);
117+
// Store the user hash in memory for sub-requests (processed in the same thread).
118+
$this->userHash = $resp->headers->get(static::USER_HASH_HEADER);
119+
120+
return $this->userHash;
121+
}
122+
123+
/**
124+
* Checks if current request is considered anonymous.
125+
*
126+
* @param Request $request
127+
*
128+
* @return bool
129+
*/
130+
protected function isAnonymous(Request $request)
131+
{
132+
foreach ($request->cookies as $name => $value) {
133+
if ($this->isSessionName($name)) {
134+
return false;
135+
}
136+
}
137+
138+
return true;
139+
}
140+
141+
/**
142+
* Checks if passed string can be considered as a session name, such as would be used in cookies.
143+
*
144+
* @param string $name
145+
*
146+
* @return bool
147+
*/
148+
protected function isSessionName($name)
149+
{
150+
return strpos($name, static::SESSION_NAME_PREFIX) === 0;
151+
}
152+
153+
/**
154+
* Generates the request object that will be forwarded to get the user context hash.
155+
*
156+
* @param Request $request
157+
*
158+
* @return Request
159+
*/
160+
protected function generateForwardRequest(Request $request)
161+
{
162+
$forwardReq = Request::create(static::USER_HASH_URI, static::USER_HASH_METHOD, array(), array(), array(), $request->server->all());
163+
$forwardReq->attributes->set('internalRequest', true);
164+
$forwardReq->headers->set('Accept', static::USER_HASH_ACCEPT_HEADER);
165+
$this->cleanupForwardRequest($forwardReq, $request);
166+
167+
168+
return $forwardReq;
169+
}
170+
171+
/**
172+
* Cleans up request to forward for user hash generation.
173+
* Cleans cookie header to only get proper sessionIds in it. This is to make the hash request cacheable.
174+
*
175+
* @param Request $forwardReq
176+
* @param Request $originalRequest
177+
*/
178+
protected function cleanupForwardRequest(Request $forwardReq, Request $originalRequest)
179+
{
180+
$sessionIds = array();
181+
foreach ($originalRequest->cookies as $name => $value)
182+
{
183+
if ( $this->isSessionName($name))
184+
{
185+
$sessionIds[] = $value;
186+
$forwardReq->cookies->set($name, $value);
187+
}
188+
}
189+
$forwardReq->headers->set('Cookie', implode('|', $sessionIds));
190+
}
191+
}

Resources/doc/features.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ corresponding reference section.
1212
features/user-context
1313
features/helpers
1414
features/testing
15+
features/symfony-http-cache
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
Symfony HttpCache
2+
=================
3+
4+
Symfony comes with a built-in reverse proxy written in PHP, known as
5+
``HttpCache``. It can be useful when one hosts a Symfony application on shared
6+
hosting for instance.
7+
8+
If you use Symfony ``HttpCache``, you'll need to make your ``AppCache`` class
9+
extend ``FOS\HttpCacheBundle\HttpCache`` instead of
10+
``Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache``.
11+
12+
.. warning::
13+
14+
Symfony HttpCache support is currently limited to following features:
15+
16+
* User context
17+
18+
Class constants
19+
---------------
20+
21+
``FOS\HttpCacheBundle\HttpCache`` defines constants that can easily be overriden
22+
in your ``AppCache`` class:
23+
24+
.. code-block:: php
25+
26+
use FOS\HttpCacheBundle\HttpCache;
27+
28+
class AppCache extends HttpCache
29+
{
30+
/**
31+
* Overriding default value for SESSION_NAME_PREFIX
32+
* to use eZSESSID instead.
33+
*/
34+
const SESSION_NAME_PREFIX = 'eZSESSID';
35+
}
36+
37+
User context
38+
~~~~~~~~~~~~
39+
40+
.. note::
41+
42+
For detailed information on user context, please read the
43+
`user context documentation page </features/user-context>`
44+
45+
* ``SESSION_NAME_PREFIX``: Prefix for session names. Must match your session
46+
configuration.
47+
Needed for caching correctly generated user context hash for each user session.
48+
49+
**default**: ``PHPSESSID``
50+
51+
.. warning::
52+
53+
If you have a customized session name, it is **very important** that this
54+
constant matches it.
55+
Session IDs are indeed used as keys to cache the generated use context hash.
56+
57+
Wrong session name will lead to unexpected results such as having the same
58+
user context hash for every users,
59+
or not having it cached at all (painful for performance.
60+
61+
* ``USER_HASH_ACCEPT_HEADER``: Accept header value to be used to request the
62+
user hash to the backend application.
63+
It must match the one defined in FOSHttpCacheBundle's configuration (see below).
64+
65+
**default**: ``application/vnd.fos.user-context-hash``
66+
67+
* ``USER_HASH_HEADER``: Name of the header the user context hash will be stored
68+
into.
69+
It must match the one defined in FOSHttpCacheBundle's configuration (see below).
70+
71+
**default**: ``X-User-Context-Hash``
72+
73+
* ``USER_HASH_URI``: URI used with the forwarded request for user context hash
74+
generation.
75+
76+
**default**: ``/_fos_user_context_hash``
77+
78+
* ``USER_HASH_METHOD``: HTTP Method used with the forwarded request for user
79+
context hash generation.
80+
81+
**default**: ``GET``

Resources/doc/reference/configuration/user-context.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ Configuration
1111
Caching Proxy Configuration
1212
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1313

14-
First you need to set up your caching proxy as explained in the
14+
Varnish
15+
"""""""
16+
17+
Set up Varnish caching proxy as explained in the
1518
:ref:`user context documentation <foshttpcache:user-context>`.
1619

20+
Symfony reverse proxy
21+
"""""""""""""""""""""
22+
23+
Set up Symfony reverse proxy as explained in the :doc:`Symfony HttpCache dedicated documentation page </features/symfony-http-cache>`.
24+
1725
Context Hash Route
1826
~~~~~~~~~~~~~~~~~~
1927

Tests/Unit/EventListener/UserContextSubscriberTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public function testOnKernelRequestCached()
9494
$this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response);
9595
$this->assertEquals('hash', $response->headers->get('X-Hash'));
9696
$this->assertEquals('X-SessionId', $response->headers->get('Vary'));
97-
$this->assertEquals('max-age=30, private', $response->headers->get('Cache-Control'));
97+
$this->assertEquals('max-age=30, public', $response->headers->get('Cache-Control'));
9898
}
9999

100100
public function testOnKernelRequestNotMatched()

0 commit comments

Comments
 (0)