Skip to content

Commit

Permalink
Merge pull request #152 from lolautruche/contextHashSymfonyReverseProxy
Browse files Browse the repository at this point in the history
Implemented Symfony reverse proxy support for user context hash.
  • Loading branch information
dbu committed Oct 30, 2014
2 parents 851a001 + 106c4ed commit d495ec8
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 2 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

1.1.0
-----

* **2014-10-14** Allow cache headers overwrite.
* **2014-10-29** Added support for the user context lookup with Symfony built-in
reverse proxy, aka `HttpCache`.

1.0.0
-----

Expand Down
1 change: 1 addition & 0 deletions EventListener/UserContextSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public function onKernelRequest(GetResponseEvent $event)
if ($this->ttl > 0) {
$response->setClientTtl($this->ttl);
$response->setVary($this->userIdentifierHeaders);
$response->setPublic();
} else {
$response->setClientTtl(0);
$response->headers->addCacheControlDirective('no-cache');
Expand Down
188 changes: 188 additions & 0 deletions HttpCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

/*
* This file is part of the FOSHttpCacheBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\HttpCacheBundle;

use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache as BaseHttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
* Base class for enhanced Symfony reverse proxy.
*
* @author Jérôme Vieilledent <lolautruche@gmail.com> (courtesy of eZ Systems AS)
*
* {@inheritdoc}
*/
abstract class HttpCache extends BaseHttpCache
{
/**
* Hash for anonymous user.
*/
const ANONYMOUS_HASH = '38015b703d82206ebc01d17a39c727e5';

/**
* Accept header value to be used to request the user hash to the backend application.
* It must match the one defined in FOSHttpCacheBundle's configuration.
*/
const USER_HASH_ACCEPT_HEADER = 'application/vnd.fos.user-context-hash';

/**
* Name of the header the user context hash will be stored into.
* It must match the one defined in FOSHttpCacheBundle's configuration.
*/
const USER_HASH_HEADER = 'X-User-Context-Hash';

/**
* URI used with the forwarded request for user context hash generation.
*/
const USER_HASH_URI = '/_fos_user_context_hash';

/**
* HTTP Method used with the forwarded request for user context hash generation.
*/
const USER_HASH_METHOD = 'GET';

/**
* Prefix for session names.
* Must match your session configuration.
*/
const SESSION_NAME_PREFIX = 'PHPSESSID';

/**
* Generated user hash.
*
* @var string
*/
private $userHash;

public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if (!$this->isInternalRequest($request)) {
// Prevent tampering attacks on the hash mechanism
if ($request->headers->get('accept') === static::USER_HASH_ACCEPT_HEADER
|| $request->headers->get(static::USER_HASH_HEADER) !== null) {
return new Response('', 400);
}

if ($request->isMethodSafe()) {
$request->headers->set(static::USER_HASH_HEADER, $this->getUserHash($request));
}
}

return parent::handle($request, $type, $catch);
}

/**
* Checks if passed request object is to be considered internal (e.g. for user hash lookup).
*
* @param Request $request
*
* @return bool
*/
private function isInternalRequest(Request $request)
{
return $request->attributes->get('internalRequest', false) === true;
}

/**
* Returns the user context hash for $request.
*
* @param Request $request
*
* @return string
*/
private function getUserHash(Request $request)
{
if (isset($this->userHash)) {
return $this->userHash;
}

if ($this->isAnonymous($request)) {
return $this->userHash = static::ANONYMOUS_HASH;
}

// Forward the request to generate the user hash
$forwardReq = $this->generateForwardRequest($request);
$resp = $this->handle($forwardReq);
// Store the user hash in memory for sub-requests (processed in the same thread).
$this->userHash = $resp->headers->get(static::USER_HASH_HEADER);

return $this->userHash;
}

/**
* Checks if current request is considered anonymous.
*
* @param Request $request
*
* @return bool
*/
private function isAnonymous(Request $request)
{
foreach ($request->cookies as $name => $value) {
if ($this->isSessionName($name)) {
return false;
}
}

return true;
}

/**
* Checks if passed string can be considered as a session name, such as would be used in cookies.
*
* @param string $name
*
* @return bool
*/
private function isSessionName($name)
{
return strpos($name, static::SESSION_NAME_PREFIX) === 0;
}

/**
* Generates the request object that will be forwarded to get the user context hash.
*
* @param Request $request
*
* @return Request
*/
private function generateForwardRequest(Request $request)
{
$forwardReq = Request::create(static::USER_HASH_URI, static::USER_HASH_METHOD, array(), array(), array(), $request->server->all());
$forwardReq->attributes->set('internalRequest', true);
$forwardReq->headers->set('Accept', static::USER_HASH_ACCEPT_HEADER);
$this->cleanupForwardRequest($forwardReq, $request);

return $forwardReq;
}

/**
* Cleans up request to forward for user hash generation.
* Cleans cookie header to only get proper sessionIds in it. This is to make the hash request cacheable.
*
* @param Request $forwardReq
* @param Request $originalRequest
*/
protected function cleanupForwardRequest(Request $forwardReq, Request $originalRequest)
{
$sessionIds = array();
foreach ($originalRequest->cookies as $name => $value) {
if ( $this->isSessionName($name)) {
$sessionIds[$name] = $value;
$forwardReq->cookies->set($name, $value);
}
}
$forwardReq->headers->set('Cookie', http_build_query($sessionIds, '', '; '));
}
}
1 change: 1 addition & 0 deletions Resources/doc/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ corresponding reference section.
features/user-context
features/helpers
features/testing
features/symfony-http-cache
82 changes: 82 additions & 0 deletions Resources/doc/features/symfony-http-cache.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
Symfony HttpCache
=================

Symfony comes with a built-in reverse proxy written in PHP, known as
``HttpCache``. It can be useful when one hosts a Symfony application on shared
hosting for instance
(see [HttpCache documentation](http://symfony.com/doc/current/book/http_cache.html#symfony-reverse-proxy).

If you use Symfony ``HttpCache``, you'll need to make your ``AppCache`` class
extend ``FOS\HttpCacheBundle\HttpCache`` instead of
``Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache``.

.. warning::

Symfony HttpCache support is currently limited to following features:

* User context

Class constants
---------------

``FOS\HttpCacheBundle\HttpCache`` defines constants that can easily be overriden
in your ``AppCache`` class:

.. code-block:: php
use FOS\HttpCacheBundle\HttpCache;
class AppCache extends HttpCache
{
/**
* Overriding default value for SESSION_NAME_PREFIX
* to use eZSESSID instead.
*/
const SESSION_NAME_PREFIX = 'eZSESSID';
}
User context
~~~~~~~~~~~~

.. note::

For detailed information on user context, please read the
`user context documentation page </features/user-context>`

* ``SESSION_NAME_PREFIX``: Prefix for session names. Must match your session
configuration.
Needed for caching correctly generated user context hash for each user session.

**default**: ``PHPSESSID``

.. warning::

If you have a customized session name, it is **very important** that this
constant matches it.
Session IDs are indeed used as keys to cache the generated use context hash.

Wrong session name will lead to unexpected results such as having the same
user context hash for every users,
or not having it cached at all (painful for performance.

* ``USER_HASH_ACCEPT_HEADER``: Accept header value to be used to request the
user hash to the backend application.
It must match the one defined in FOSHttpCacheBundle's configuration (see below).

**default**: ``application/vnd.fos.user-context-hash``

* ``USER_HASH_HEADER``: Name of the header the user context hash will be stored
into.
It must match the one defined in FOSHttpCacheBundle's configuration (see below).

**default**: ``X-User-Context-Hash``

* ``USER_HASH_URI``: URI used with the forwarded request for user context hash
generation.

**default**: ``/_fos_user_context_hash``

* ``USER_HASH_METHOD``: HTTP Method used with the forwarded request for user
context hash generation.

**default**: ``GET``
10 changes: 9 additions & 1 deletion Resources/doc/reference/configuration/user-context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ Configuration
Caching Proxy Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~

First you need to set up your caching proxy as explained in the
Varnish
"""""""

Set up Varnish caching proxy as explained in the
:ref:`user context documentation <foshttpcache:user-context>`.

Symfony reverse proxy
"""""""""""""""""""""

Set up Symfony reverse proxy as explained in the :doc:`Symfony HttpCache dedicated documentation page </features/symfony-http-cache>`.

Context Hash Route
~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion Tests/Unit/EventListener/UserContextSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function testOnKernelRequestCached()
$this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response);
$this->assertEquals('hash', $response->headers->get('X-Hash'));
$this->assertEquals('X-SessionId', $response->headers->get('Vary'));
$this->assertEquals('max-age=30, private', $response->headers->get('Cache-Control'));
$this->assertEquals('max-age=30, public', $response->headers->get('Cache-Control'));
}

public function testOnKernelRequestNotMatched()
Expand Down

0 comments on commit d495ec8

Please sign in to comment.