Skip to content

Commit d495ec8

Browse files
committed
Merge pull request #152 from lolautruche/contextHashSymfonyReverseProxy
Implemented Symfony reverse proxy support for user context hash.
2 parents 851a001 + 106c4ed commit d495ec8

File tree

8 files changed

+438
-2
lines changed

8 files changed

+438
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
1.1.0
5+
-----
6+
7+
* **2014-10-14** Allow cache headers overwrite.
8+
* **2014-10-29** Added support for the user context lookup with Symfony built-in
9+
reverse proxy, aka `HttpCache`.
10+
411
1.0.0
512
-----
613

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: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
private 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+
private 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+
private 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+
private 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+
private 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+
return $forwardReq;
168+
}
169+
170+
/**
171+
* Cleans up request to forward for user hash generation.
172+
* Cleans cookie header to only get proper sessionIds in it. This is to make the hash request cacheable.
173+
*
174+
* @param Request $forwardReq
175+
* @param Request $originalRequest
176+
*/
177+
protected function cleanupForwardRequest(Request $forwardReq, Request $originalRequest)
178+
{
179+
$sessionIds = array();
180+
foreach ($originalRequest->cookies as $name => $value) {
181+
if ( $this->isSessionName($name)) {
182+
$sessionIds[$name] = $value;
183+
$forwardReq->cookies->set($name, $value);
184+
}
185+
}
186+
$forwardReq->headers->set('Cookie', http_build_query($sessionIds, '', '; '));
187+
}
188+
}

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