Passed
Push — master ( 89a60a...7d44c8 )
by Alexander
02:13
created

ForceSecureConnection::addHSTS()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 3
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\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 9
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
50
    {
51 9
        if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
52 2
            $url = (string)$request->getUri()->withScheme('https')->withPort($this->port);
53 2
            return $this->addHSTS(
54 2
                $this->responseFactory
55 2
                    ->createResponse($this->statusCode)
56 2
                    ->withHeader(Header::LOCATION, $url)
57
            );
58
        }
59 7
        return $this->addHSTS($this->addCSP($handler->handle($request)));
60
    }
61
62
    /**
63
     * Redirects from HTTP to HTTPS
64
     * @param int $statusCode
65
     * @param null|int $port
66
     * @return self
67
     */
68 3
    public function withRedirection($statusCode = Status::MOVED_PERMANENTLY, int $port = null): self
69
    {
70 3
        $clone = clone $this;
71 3
        $clone->redirect = true;
72 3
        $clone->port = $port;
73 3
        $clone->statusCode = $statusCode;
74 3
        return $clone;
75
    }
76 6
    public function withoutRedirection(): self
77
    {
78 6
        $clone = clone $this;
79 6
        $clone->redirect = false;
80 6
        return $clone;
81
    }
82
83
    /**
84
     * Add Content-Security-Policy header to Response
85
     * @see Header::CONTENT_SECURITY_POLICY
86
     * @param string $directives
87
     * @return self
88
     */
89 4
    public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
90
    {
91 4
        $clone = clone $this;
92 4
        $clone->addCSP = true;
93 4
        $clone->cspDirectives = $directives;
94 4
        return $clone;
95
    }
96 5
    public function withoutCSP(): self
97
    {
98 5
        $clone = clone $this;
99 5
        $clone->addCSP = false;
100 5
        return $clone;
101
    }
102
103
    /**
104
     * Add Strict-Transport-Security header to each Response
105
     * @see Header::STRICT_TRANSPORT_SECURITY
106
     * @param int $maxAge
107
     * @param bool $subDomains
108
     * @return self
109
     */
110 4
    public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDomains = false): self
111
    {
112 4
        $clone = clone $this;
113 4
        $clone->addHSTS = true;
114 4
        $clone->hstsMaxAge = $maxAge;
115 4
        $clone->hstsSubDomains = $subDomains;
116 4
        return $clone;
117
    }
118 5
    public function withoutHSTS(): self
119
    {
120 5
        $clone = clone $this;
121 5
        $clone->addHSTS = false;
122 5
        return $clone;
123
    }
124
125 7
    private function addCSP(ResponseInterface $response): ResponseInterface
126
    {
127 7
        return $this->addCSP
128 4
            ? $response->withHeader(Header::CONTENT_SECURITY_POLICY, $this->cspDirectives)
129 7
            : $response;
130
    }
131 9
    private function addHSTS(ResponseInterface $response): ResponseInterface
132
    {
133 9
        $subDomains = $this->hstsSubDomains ? '; includeSubDomains' : '';
134 9
        return $this->addHSTS
135 5
            ? $response->withHeader(Header::STRICT_TRANSPORT_SECURITY, "max-age={$this->hstsMaxAge}{$subDomains}")
136 9
            : $response;
137
    }
138
}
139