Passed
Push — master ( 974680...fc5316 )
by Kirill
03:59
created

CookiesMiddleware::decodeCookie()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 27
rs 8.8333
cc 7
nc 8
nop 1
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Cookies\Middleware;
13
14
use Psr\Http\Message\ResponseInterface as Response;
15
use Psr\Http\Message\ServerRequestInterface as Request;
16
use Psr\Http\Server\MiddlewareInterface;
17
use Psr\Http\Server\RequestHandlerInterface;
18
use Spiral\Cookies\Config\CookiesConfig;
19
use Spiral\Cookies\Cookie;
20
use Spiral\Cookies\CookieQueue;
21
use Spiral\Encrypter\EncryptionInterface;
22
use Spiral\Encrypter\Exception\DecryptException;
23
use Spiral\Encrypter\Exception\EncryptException;
24
25
/**
26
 * Middleware used to encrypt and decrypt cookies. Creates container scope for a cookie bucket.
27
 *
28
 * Attention, EncrypterInterface is requested from container on demand.
29
 */
30
final class CookiesMiddleware implements MiddlewareInterface
31
{
32
    /** @var CookiesConfig */
33
    private $config;
34
35
    /** @var EncryptionInterface */
36
    private $encryption;
37
38
    /**
39
     * @param CookiesConfig       $config
40
     * @param EncryptionInterface $encryption
41
     */
42
    public function __construct(CookiesConfig $config, EncryptionInterface $encryption)
43
    {
44
        $this->config = $config;
45
        $this->encryption = $encryption;
46
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51
    public function process(Request $request, RequestHandlerInterface $handler): Response
52
    {
53
        //Aggregates all user cookies
54
        $queue = new CookieQueue(
55
            $this->config->resolveDomain($request->getUri()),
56
            $request->getUri()->getScheme() === 'https'
57
        );
58
59
        $response = $handler->handle(
60
            $this->unpackCookies($request)->withAttribute(CookieQueue::ATTRIBUTE, $queue)
61
        );
62
63
        return $this->packCookies($response, $queue);
64
    }
65
66
    /**
67
     * Unpack incoming cookies and decrypt their content.
68
     *
69
     * @param Request $request
70
     * @return Request
71
     */
72
    protected function unpackCookies(Request $request): Request
73
    {
74
        $cookies = $request->getCookieParams();
75
76
        foreach ($cookies as $name => $cookie) {
77
            if (!$this->isProtected($name)) {
78
                continue;
79
            }
80
81
            $cookies[$name] = $this->decodeCookie($cookie);
82
        }
83
84
        return $request->withCookieParams($cookies);
85
    }
86
87
    /**
88
     * Check if cookie has to be protected.
89
     *
90
     * @param string $cookie
91
     * @return bool
92
     */
93
    protected function isProtected(string $cookie): bool
94
    {
95
        if (in_array($cookie, $this->config->getExcludedCookies(), true)) {
96
            //Excluded
97
            return false;
98
        }
99
100
        return $this->config->getProtectionMethod() !== CookiesConfig::COOKIE_UNPROTECTED;
101
    }
102
103
    /**
104
     * Pack outcoming cookies with encrypted value.
105
     *
106
     * @param Response    $response
107
     * @param CookieQueue $queue
108
     * @return Response
109
     *
110
     * @throws EncryptException
111
     */
112
    protected function packCookies(Response $response, CookieQueue $queue): Response
113
    {
114
        if (empty($queue->getScheduled())) {
115
            return $response;
116
        }
117
118
        $cookies = $response->getHeader('Set-Cookie');
119
120
        foreach ($queue->getScheduled() as $cookie) {
121
            if (empty($cookie->getValue()) || !$this->isProtected($cookie->getName())) {
122
                $cookies[] = $cookie->createHeader();
123
                continue;
124
            }
125
126
            $cookies[] = $this->encodeCookie($cookie)->createHeader();
127
        }
128
129
        return $response->withHeader('Set-Cookie', $cookies);
130
    }
131
132
    /**
133
     * @param string|array $cookie
134
     * @return array|mixed|null
135
     */
136
    private function decodeCookie($cookie)
137
    {
138
        try {
139
            if (is_array($cookie)) {
140
                return array_map([$this, 'decodeCookie'], $cookie);
141
            }
142
        } catch (DecryptException $exception) {
143
            return null;
144
        }
145
146
        switch ($this->config->getProtectionMethod()) {
147
            case CookiesConfig::COOKIE_ENCRYPT:
148
                try {
149
                    return $this->encryption->getEncrypter()->decrypt($cookie);
150
                } catch (DecryptException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
151
                }
152
                return null;
153
            case CookiesConfig::COOKIE_HMAC:
154
                $hmac = substr($cookie, -1 * CookiesConfig::MAC_LENGTH);
155
                $value = substr($cookie, 0, strlen($cookie) - strlen($hmac));
156
157
                if (hash_equals($this->hmacSign($value), $hmac)) {
158
                    return $value;
159
                }
160
        }
161
162
        return null;
163
    }
164
165
    /**
166
     * Sign string.
167
     *
168
     * @param string|null $value
169
     * @return string
170
     */
171
    private function hmacSign($value): string
172
    {
173
        return hash_hmac(
174
            CookiesConfig::HMAC_ALGORITHM,
175
            $value,
176
            $this->encryption->getKey()
177
        );
178
    }
179
180
    /**
181
     * @param Cookie $cookie
182
     * @return Cookie
183
     */
184
    private function encodeCookie(Cookie $cookie): Cookie
185
    {
186
        if ($this->config->getProtectionMethod() === CookiesConfig::COOKIE_ENCRYPT) {
187
            $encryptor = $this->encryption->getEncrypter();
188
189
            return $cookie->withValue($encryptor->encrypt($cookie->getValue()));
190
        }
191
192
        //VALUE.HMAC
193
        return $cookie->withValue($cookie->getValue() . $this->hmacSign($cookie->getValue()));
194
    }
195
}
196