Completed
Pull Request — master (#322)
by David de
10:31 queued 07:56
created

UserContextSubscriber::isSessionName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
ccs 2
cts 2
cp 1
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
/*
4
 * This file is part of the FOSHttpCache 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\HttpCache\SymfonyCache;
13
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\HttpFoundation\Response;
17
use Symfony\Component\HttpKernel\HttpKernelInterface;
18
use Symfony\Component\OptionsResolver\OptionsResolver;
19
20
/**
21
 * User context handler for the symfony built-in HttpCache.
22
 *
23
 * @author Jérôme Vieilledent <[email protected]> (courtesy of eZ Systems AS)
24
 *
25
 * {@inheritdoc}
26
 */
27
class UserContextSubscriber implements EventSubscriberInterface
28
{
29
    /**
30
     * The options configured in the constructor argument or default values.
31
     *
32
     * @var array
33
     */
34
    private $options;
35
36
    /**
37
     * Generated user hash.
38
     *
39
     * @var string
40
     */
41
    private $userHash;
42
43
    /**
44
     * When creating this subscriber, you can configure a number of options.
45
     *
46
     * - anonymous_hash:          Hash used for anonymous user.
47
     * - user_hash_accept_header: Accept header value to be used to request the user hash to the
48
     *                            backend application. Must match the setup of the backend application.
49
     * - user_hash_header:        Name of the header the user context hash will be stored into. Must
50
     *                            match the setup for the Vary header in the backend application.
51
     * - user_hash_uri:           Target URI used in the request for user context hash generation.
52
     * - user_hash_method:        HTTP Method used with the hash lookup request for user context hash generation.
53
     * - session_name_prefix:     Prefix for session cookies. Must match your PHP session configuration.
54
     *
55
     * @param array $options Options to overwrite the default options
56
     *
57
     * @throws \InvalidArgumentException if unknown keys are found in $options
58
     */
59 12
    public function __construct(array $options = [])
60
    {
61 12
        $resolver = new OptionsResolver();
62 12
        $resolver->setDefaults([
63 12
            'anonymous_hash' => '38015b703d82206ebc01d17a39c727e5',
64
            'user_hash_accept_header' => 'application/vnd.fos.user-context-hash',
65
            'user_hash_header' => 'X-User-Context-Hash',
66
            'user_hash_uri' => '/_fos_user_context_hash',
67
            'user_hash_method' => 'GET',
68
            'session_name_prefix' => 'PHPSESSID',
69
        ]);
70
71 12
        $this->options = $resolver->resolve($options);
72 11
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 1
    public static function getSubscribedEvents()
78
    {
79
        return [
80 1
            Events::PRE_HANDLE => 'preHandle',
81
        ];
82
    }
83
84
    /**
85
     * Look at the request before it is handled by the kernel.
86
     *
87
     * Adds the user hash header to the request.
88
     *
89
     * Checks if an external request tries tampering with the use context hash mechanism
90
     * to prevent attacks.
91
     *
92
     * @param CacheEvent $event
93
     */
94 11
    public function preHandle(CacheEvent $event)
95
    {
96 11
        $request = $event->getRequest();
97 11
        if (!$this->isInternalRequest($request)) {
98
            // Prevent tampering attacks on the hash mechanism
99 11
            if ($request->headers->get('accept') === $this->options['user_hash_accept_header']
100 11
                || $request->headers->get($this->options['user_hash_header']) !== null
101
            ) {
102 4
                $event->setResponse(new Response('', 400));
103
104 4
                return;
105
            }
106
107 7
            if ($request->isMethodSafe()) {
108 7
                $request->headers->set($this->options['user_hash_header'], $this->getUserHash($event->getKernel(), $request));
109
            }
110
        }
111
112
        // let the kernel handle this request.
113 7
    }
114
115
    /**
116
     * Remove unneeded things from the request for user hash generation.
117
     *
118
     * Cleans cookies header to only keep the session identifier cookie, so the hash lookup request
119
     * can be cached per session.
120
     *
121
     * @param Request $hashLookupRequest
122
     * @param Request $originalRequest
123
     */
124 4
    protected function cleanupHashLookupRequest(Request $hashLookupRequest, Request $originalRequest)
125
    {
126 4
        $sessionIds = [];
127 4
        foreach ($originalRequest->cookies as $name => $value) {
128 4
            if ($this->isSessionName($name)) {
129 2
                $sessionIds[$name] = $value;
130 4
                $hashLookupRequest->cookies->set($name, $value);
131
            }
132
        }
133
134 4
        if (count($sessionIds) > 0) {
135 2
            $hashLookupRequest->headers->set('Cookie', http_build_query($sessionIds, '', '; '));
136
        }
137 4
    }
138
139
    /**
140
     * Checks if passed request object is to be considered internal (e.g. for user hash lookup).
141
     *
142
     * @param Request $request
143
     *
144
     * @return bool
145
     */
146 11
    private function isInternalRequest(Request $request)
147
    {
148 11
        return $request->attributes->get('internalRequest', false) === true;
149
    }
150
151
    /**
152
     * Returns the user context hash for $request.
153
     *
154
     * @param HttpKernelInterface $kernel
155
     * @param Request             $request
156
     *
157
     * @return string
158
     */
159 7
    private function getUserHash(HttpKernelInterface $kernel, Request $request)
160
    {
161 7
        if (isset($this->userHash)) {
162
            return $this->userHash;
163
        }
164
165 7
        if ($this->isAnonymous($request)) {
166 3
            return $this->userHash = $this->options['anonymous_hash'];
167
        }
168
169
        // Hash lookup request to let the backend generate the user hash
170 4
        $hashLookupRequest = $this->generateHashLookupRequest($request);
171 4
        $resp = $kernel->handle($hashLookupRequest);
172
        // Store the user hash in memory for sub-requests (processed in the same thread).
173 4
        $this->userHash = $resp->headers->get($this->options['user_hash_header']);
0 ignored issues
show
Documentation Bug introduced by
It seems like $resp->headers->get($thi...ns['user_hash_header']) can also be of type array. However, the property $userHash is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
174
175 4
        return $this->userHash;
176
    }
177
178
    /**
179
     * Checks if current request is considered anonymous.
180
     *
181
     * @param Request $request
182
     *
183
     * @return bool
184
     */
185 7
    private function isAnonymous(Request $request)
186
    {
187
        // You might have to enable rewriting of the Authorization header in your server config or .htaccess:
188
        // RewriteEngine On
189
        // RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
190 7
        if ($request->server->has('AUTHORIZATION') ||
191 7
            $request->server->has('HTTP_AUTHORIZATION') ||
192 7
            $request->server->has('PHP_AUTH_USER')
193
        ) {
194 2
            return false;
195
        }
196
197 5
        foreach ($request->cookies as $name => $value) {
198 2
            if ($this->isSessionName($name)) {
199 2
                return false;
200
            }
201
        }
202
203 3
        return true;
204
    }
205
206
    /**
207
     * Checks if passed string can be considered as a session name, such as would be used in cookies.
208
     *
209
     * @param string $name
210
     *
211
     * @return bool
212
     */
213 4
    private function isSessionName($name)
214
    {
215 4
        return strpos($name, $this->options['session_name_prefix']) === 0;
216
    }
217
218
    /**
219
     * Generates the request object that will be forwarded to get the user context hash.
220
     *
221
     * @param Request $request
222
     *
223
     * @return Request The request that will return the user context hash value
224
     */
225 4
    private function generateHashLookupRequest(Request $request)
226
    {
227 4
        $hashLookupRequest = Request::create($this->options['user_hash_uri'], $this->options['user_hash_method'], [], [], [], $request->server->all());
228 4
        $hashLookupRequest->attributes->set('internalRequest', true);
229 4
        $hashLookupRequest->headers->set('Accept', $this->options['user_hash_accept_header']);
230 4
        $this->cleanupHashLookupRequest($hashLookupRequest, $request);
231
232 4
        return $hashLookupRequest;
233
    }
234
}
235