HstsMiddleware::checkHstsSuperdomains()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
crap 3
1
<?php
2
namespace CheatCodes\GuzzleHsts;
3
4
use InvalidArgumentException;
5
use Psr\Http\Message\RequestInterface as Request;
6
use Psr\Http\Message\ResponseInterface as Response;
7
8
class HstsMiddleware
9
{
10
    /**
11
     * Next handler for the Guzzle middleware
12
     *
13
     * @var callable
14
     */
15
    private $nextHandler;
16
17
    /**
18
     * Store instances cache
19
     *
20
     * @var StoreInterface[]
21
     */
22
    private $storeInstances = [];
23
24
    /**
25
     * HstsMiddleware constructor
26
     *
27
     * @param callable $nextHandler Next handler to invoke.
28
     */
29 18
    public function __construct(callable $nextHandler)
30
    {
31 18
        $this->nextHandler = $nextHandler;
32 18
    }
33
34
    /**
35
     * Invoke the guzzle middleware
36
     *
37
     * @param Request $request
38
     * @param array   $options
39
     * @return \GuzzleHttp\Promise\PromiseInterface
40
     */
41 18
    public function __invoke(Request $request, array $options)
42
    {
43 18
        $fn = $this->nextHandler;
44
45 18
        $store = $this->getStoreInstance($options);
46
47 17
        $request = $this->handleHstsRewrite($request, $store);
48
49 17
        return $fn($request, $options)
50
            ->then(function (Response $response) use ($request, $store) {
51 17
                return $this->handleHstsRegistering($response, $request, $store);
52 17
            });
53
    }
54
55
    /**
56
     * Rewrite the requested uri if the requested host is a known HSTS host
57
     *
58
     * @param Request        $request
59
     * @param StoreInterface $store
60
     * @return Request
61
     */
62 17
    private function handleHstsRewrite(Request $request, StoreInterface $store)
63
    {
64 17
        $uri = $request->getUri();
65 17
        $domainName = $uri->getHost();
66
67 17
        if ($uri->getScheme() === 'http'
68 17
            && !$this->isIpAddress($domainName)
69 17
            && $this->isKnownHstsHosts($store, $uri->getHost())
70
        ) {
71 10
            $uri = $uri->withScheme('https');
72
73 10
            return $request->withUri($uri);
74
        }
75
76 17
        return $request;
77
    }
78
79
    /**
80
     * Register the host as a known HSTS host if the header is set properly
81
     *
82
     * @param Response       $response
83
     * @param Request        $request
84
     * @param StoreInterface $store
85
     * @return Response
86
     */
87 17
    private function handleHstsRegistering(Response $response, Request $request, StoreInterface $store)
88
    {
89 17
        $domainName = $request->getUri()->getHost();
90
91 17
        if ($request->getUri()->getScheme() === 'https'
92 17
            && $response->hasHeader('Strict-Transport-Security')
93 17
            && !$this->isIpAddress($domainName)
94
        ) {
95 13
            $header = $response->getHeader('Strict-Transport-Security');
96
97
            // Only process the first header, https://tools.ietf.org/html/rfc6797#section-8.1
98 13
            $policy = $this->parseHeader(array_shift($header));
99
100 13
            if (isset($policy['max-age'])) {
101 11
                if ($policy['max-age'] < 1) {
102 1
                    $store->delete($domainName);
103
                } else {
104
                    // Remove all unneeded data from the policy
105 11
                    $policy = array_intersect_key($policy, array_flip([
106 11
                        'max-age', 'includesubdomains',
107
                    ]));
108
109 11
                    $store->set($domainName, $policy['max-age'], $policy);
110
                }
111
            }
112
        }
113
114 17
        return $response;
115
    }
116
117
    /**
118
     * Check if the given domain is a known HSTS host
119
     *
120
     * @param StoreInterface $store
121
     * @param string         $domainName
122
     * @param bool           $includeSubDomains
123
     * @return bool
124
     */
125 16
    private function checkHstsDomain(StoreInterface $store, $domainName, $includeSubDomains)
126
    {
127 16
        $policy = $store->get($domainName);
128
129 16
        return $policy !== false && (!$includeSubDomains || isset($policy['includesubdomains']));
130
    }
131
132
    /**
133
     * Check if the given domain's superdomains are known HSTS hosts
134
     *
135
     * @param StoreInterface $store
136
     * @param string         $domainName
137
     * @return bool
138
     */
139 11
    private function checkHstsSuperdomains(StoreInterface $store, $domainName)
140
    {
141 11
        $labels = explode('.', $domainName);
142 11
        $labelCount = count($labels);
143
144 11
        for ($i = 1; $i < $labelCount; ++$i) {
145 11
            $domainName = implode('.', array_slice($labels, $labelCount - $i));
146
147 11
            if ($this->checkHstsDomain($store, $domainName, true)) {
148 2
                return true;
149
            }
150
        }
151
152 10
        return false;
153
    }
154
155
    /**
156
     * Check if the given domain or a superdomain is a known HSTS host
157
     *
158
     * @param StoreInterface $store
159
     * @param string         $domainName
160
     * @return bool
161
     */
162 16
    private function isKnownHstsHosts(StoreInterface $store, $domainName)
163
    {
164 16
        return $this->checkHstsDomain($store, $domainName, false)
165 16
            || $this->checkHstsSuperdomains($store, $domainName);
166
    }
167
168
    /**
169
     * Get the store instance, possibly cached
170
     *
171
     * @param array $options
172
     * @return StoreInterface
173
     * @throws InvalidArgumentException
174
     */
175 18
    private function getStoreInstance(array $options)
176
    {
177
        // Get option or use the default store
178 18
        $store = isset($options['hsts_store']) ? $options['hsts_store'] : ArrayStore::class;
179
180
        // Just return the store if it is already an instance
181 18
        if ($store instanceof StoreInterface) {
182 1
            return $store;
183
        }
184
185
        // Instanciate new store or return already instanciated store
186 17
        if (is_string($store) && class_exists($store) && class_implements($store, StoreInterface::class)) {
187 16
            if (!isset($this->storeInstances[$store])) {
188 16
                $this->storeInstances[$store] = new $store();
189
            }
190
191 16
            return $this->storeInstances[$store];
192
        }
193
194 1
        throw new InvalidArgumentException('hsts_store must be an ' . StoreInterface::class .
195 1
            ' instance or the name of a class extending ' . StoreInterface::class);
196
    }
197
198
    /**
199
     * Parse the HSTS header
200
     *
201
     * @param string $header
202
     * @return array
203
     */
204 13
    private function parseHeader($header)
205
    {
206 13
        $directives = explode(';', $header);
207 13
        $parsed = [];
208
209 13
        foreach ($directives as $directive) {
210 13
            $directive = trim($directive);
211
212 13
            if (preg_match('/(?<name>.+?)=[\'"]?(?<value>.+?)[\'"]?$/', $directive, $matches)) {
213 11
                $name = strtolower($matches['name']);
214 11
                $value = $matches['value'];
215
            } else {
216 5
                $name = strtolower($directive);
217 5
                $value = true;
218
            }
219
220 13
            $parsed[$name] = $value;
221
        }
222
223 13
        return $parsed;
224
    }
225
226
    /**
227
     * Check if a host is an ip address
228
     *
229
     * @param string $host
230
     * @return bool
231
     */
232 17
    private function isIpAddress($host)
233
    {
234 17
        return filter_var($host, FILTER_VALIDATE_IP) !== false;
235
    }
236
237
    /**
238
     * Handler for registering the middleware
239
     *
240
     * @return \Closure
241
     */
242
    public static function handler()
243
    {
244 18
        return function (callable $handler) {
245 18
            return new self($handler);
246 18
        };
247
    }
248
}
249