Passed
Pull Request — master (#236)
by
unknown
02:37
created

ForceSecureConnection::withHSTS()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\Middleware;
6
7
use Psr\Http\Message\ResponseFactoryInterface;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Psr\Http\Server\MiddlewareInterface;
11
use Psr\Http\Server\RequestHandlerInterface;
12
use Yiisoft\Http\Status;
13
14
/**
15
 * Redirects from HTTP to HTTPS and adds CSP and HSTS headers
16
 */
17
final class ForceSecureConnection implements MiddlewareInterface
18
{
19
    private bool $redirect = true;
20
    private int $statusCode = Status::MOVED_PERMANENTLY;
21
    private ?int $port = null;
22
23
    private bool $addCSP = true;
24
    private string $cspDirectives = self::DEFAULT_CSP_DIRECTIVES;
25
26
    private bool $addSTS = true;
27
    private int $hstsMaxAge = self::DEFAULT_HSTS_MAX_AGE;
28
    private bool $hstsSubDomains = false;
29
30
    private ResponseFactoryInterface $responseFactory;
31
    private const DEFAULT_CSP_DIRECTIVES = 'upgrade-insecure-requests; default-src https:';
32
    private const DEFAULT_HSTS_MAX_AGE = 31_536_000; // 12 months
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Yii\Web\Middleware\31_536_000 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
33
34
    public function __construct(ResponseFactoryInterface $responseFactory)
35
    {
36
        $this->responseFactory = $responseFactory;
37
    }
38
39
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
40
    {
41
        if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
42
            $url = (string)$request->getUri()->withScheme('https')->withPort($this->port);
43
            return $this->addCSP(
44
                $this->responseFactory
45
                    ->createResponse($this->statusCode)
46
                    ->withHeader('Location', $url)
47
            );
48
        }
49
        return $this->addHSTS($this->addCSP($handler->handle($request)));
50
    }
51
52
    public function withRedirection($statusCode = Status::MOVED_PERMANENTLY, int $port = null): self
53
    {
54
        $clone = clone $this;
55
        $clone->redirect = true;
56
        $clone->port = $port;
57
        $clone->statusCode = $statusCode;
58
        return $clone;
59
    }
60
    public function withoutRedirection(): self
61
    {
62
        $clone = clone $this;
63
        $clone->redirect = false;
64
        return $clone;
65
    }
66
67
    /**
68
     * Add Content-Security-Policy header to Response
69
     * @link https://developer.mozilla.org/docs/Web/HTTP/CSP
70
     * @link https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy
71
     * @param string $directives
72
     * @return ForceSecureConnection
73
     */
74
    public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
75
    {
76
        $clone = clone $this;
77
        $clone->addCSP = true;
78
        $clone->cspDirectives = $directives;
79
        return $clone;
80
    }
81
    public function withoutCSP(): self
82
    {
83
        $clone = clone $this;
84
        $clone->addCSP = false;
85
        return $clone;
86
    }
87
88
    /**
89
     * Add Strict-Transport-Security header to Response
90
     * @link https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security
91
     * @param int $maxAge
92
     * @param bool $subDomains
93
     * @return ForceSecureConnection
94
     */
95
    public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDomains = false): self
96
    {
97
        $clone = clone $this;
98
        $clone->addSTS = true;
99
        $clone->hstsMaxAge = $maxAge;
100
        $clone->hstsSubDomains = $subDomains;
101
        return $clone;
102
    }
103
    public function withoutHSTS(): self
104
    {
105
        $clone = clone $this;
106
        $clone->addSTS = false;
107
        return $clone;
108
    }
109
110
    /**
111
     * @param ResponseInterface $response
112
     * @return ResponseInterface
113
     */
114
    private function addCSP(ResponseInterface $response): ResponseInterface
115
    {
116
        return $this->addCSP
117
            ? $response->withHeader('Content-Security-Policy', $this->cspDirectives)
118
            : $response;
119
    }
120
121
    /**
122
     * @param ResponseInterface $response
123
     * @return ResponseInterface
124
     */
125
    private function addHSTS(ResponseInterface $response): ResponseInterface
126
    {
127
        $subDomains = $this->hstsSubDomains ? 'includeSubDomains' : '';
128
        return $this->addSTS
129
            ? $response->withHeader('Strict-Transport-Security', "max-age={$this->hstsMaxAge}; {$subDomains}")
130
            : $response;
131
    }
132
}
133