ForceSecureConnection   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 15
eloc 50
c 1
b 0
f 0
dl 0
loc 123
ccs 49
cts 49
cp 1
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A process() 0 11 3
A addHSTS() 0 6 3
A withoutHSTS() 0 5 1
A withoutCSP() 0 5 1
A withHSTS() 0 7 1
A addCSP() 0 5 2
A withCSP() 0 6 1
A withoutRedirection() 0 5 1
A withRedirection() 0 7 1
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\Header;
13
use Yiisoft\Http\Status;
14
15
/**
16
 * Redirects insecure requests from HTTP to HTTPS, and adds headers necessary to enhance security policy.
17
 *
18
 * HTTP Strict-Transport-Security (HSTS) header is added to each response and tells the browser that your site works on
19
 * HTTPS only.
20
 *
21
 * The Content-Security-Policy (CSP) header can force the browser to load page resources only through a secure
22
 * connection, even if links in the page layout are specified with an unprotected protocol.
23
 *
24
 * Note: Prefer forcing HTTPS via web server in case you are not creating installable product such as CMS and not
25
 * hosting the project on a server where you do not have access to web server configuration.
26
 */
27
final class ForceSecureConnection implements MiddlewareInterface
28
{
29
    private const DEFAULT_CSP_DIRECTIVES = 'upgrade-insecure-requests; default-src https:';
30
    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...
31
32
    private bool $redirect = true;
33
    private int $statusCode = Status::MOVED_PERMANENTLY;
34
    private ?int $port = null;
35
36
    private bool $addCSP = true;
37
    private string $cspDirectives = self::DEFAULT_CSP_DIRECTIVES;
38
39
    private bool $addHSTS = true;
40
    private int $hstsMaxAge = self::DEFAULT_HSTS_MAX_AGE;
41
    private bool $hstsSubDomains = false;
42
43
    private ResponseFactoryInterface $responseFactory;
44
45 15
    public function __construct(ResponseFactoryInterface $responseFactory)
46
    {
47 15
        $this->responseFactory = $responseFactory;
48 15
    }
49
50 9
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
51
    {
52 9
        if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
53 2
            $url = (string)$request->getUri()->withScheme('https')->withPort($this->port);
54 2
            return $this->addHSTS(
55 2
                $this->responseFactory
56 2
                    ->createResponse($this->statusCode)
57 2
                    ->withHeader(Header::LOCATION, $url)
58
            );
59
        }
60 7
        return $this->addHSTS($this->addCSP($handler->handle($request)));
61
    }
62
63
    /**
64
     * Redirects from HTTP to HTTPS
65
     *
66
     * @param int $statusCode
67
     * @param int|null $port
68
     *
69
     * @return self
70
     */
71 3
    public function withRedirection($statusCode = Status::MOVED_PERMANENTLY, int $port = null): self
72
    {
73 3
        $new = clone $this;
74 3
        $new->redirect = true;
75 3
        $new->port = $port;
76 3
        $new->statusCode = $statusCode;
77 3
        return $new;
78
    }
79
80 6
    public function withoutRedirection(): self
81
    {
82 6
        $new = clone $this;
83 6
        $new->redirect = false;
84 6
        return $new;
85
    }
86
87
    /**
88
     * Add Content-Security-Policy header to Response
89
     *
90
     * @see Header::CONTENT_SECURITY_POLICY
91
     *
92
     * @param string $directives
93
     *
94
     * @return self
95
     */
96 4
    public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
97
    {
98 4
        $new = clone $this;
99 4
        $new->addCSP = true;
100 4
        $new->cspDirectives = $directives;
101 4
        return $new;
102
    }
103
104 5
    public function withoutCSP(): self
105
    {
106 5
        $new = clone $this;
107 5
        $new->addCSP = false;
108 5
        return $new;
109
    }
110
111
    /**
112
     * Add Strict-Transport-Security header to each Response
113
     *
114
     * @see Header::STRICT_TRANSPORT_SECURITY
115
     *
116
     * @param int $maxAge
117
     * @param bool $subDomains
118
     *
119
     * @return self
120
     */
121 4
    public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDomains = false): self
122
    {
123 4
        $new = clone $this;
124 4
        $new->addHSTS = true;
125 4
        $new->hstsMaxAge = $maxAge;
126 4
        $new->hstsSubDomains = $subDomains;
127 4
        return $new;
128
    }
129
130 5
    public function withoutHSTS(): self
131
    {
132 5
        $new = clone $this;
133 5
        $new->addHSTS = false;
134 5
        return $new;
135
    }
136
137 7
    private function addCSP(ResponseInterface $response): ResponseInterface
138
    {
139 7
        return $this->addCSP
140 4
            ? $response->withHeader(Header::CONTENT_SECURITY_POLICY, $this->cspDirectives)
141 7
            : $response;
142
    }
143
144 9
    private function addHSTS(ResponseInterface $response): ResponseInterface
145
    {
146 9
        $subDomains = $this->hstsSubDomains ? '; includeSubDomains' : '';
147 9
        return $this->addHSTS
148 5
            ? $response->withHeader(Header::STRICT_TRANSPORT_SECURITY, "max-age={$this->hstsMaxAge}{$subDomains}")
149 9
            : $response;
150
    }
151
}
152