Passed
Pull Request — master (#236)
by Aleksei
25:00 queued 09:58
created

ForceSecureConnection::withRedirection()   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\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
    public function __construct(ResponseFactoryInterface $responseFactory)
46
    {
47
        $this->responseFactory = $responseFactory;
48
    }
49
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
50
    {
51
        if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
52
            $url = (string)$request->getUri()->withScheme('https')->withPort($this->port);
53
            return $this->addHSTS(
54
                $this->responseFactory
55
                    ->createResponse($this->statusCode)
56
                    ->withHeader(Header::LOCATION, $url)
57
            );
58
        }
59
        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
    public function withRedirection($statusCode = Status::MOVED_PERMANENTLY, int $port = null): self
69
    {
70
        $clone = clone $this;
71
        $clone->redirect = true;
72
        $clone->port = $port;
73
        $clone->statusCode = $statusCode;
74
        return $clone;
75
    }
76
    public function withoutRedirection(): self
77
    {
78
        $clone = clone $this;
79
        $clone->redirect = false;
80
        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
    public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
90
    {
91
        $clone = clone $this;
92
        $clone->addCSP = true;
93
        $clone->cspDirectives = $directives;
94
        return $clone;
95
    }
96
    public function withoutCSP(): self
97
    {
98
        $clone = clone $this;
99
        $clone->addCSP = false;
100
        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
    public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDomains = false): self
111
    {
112
        $clone = clone $this;
113
        $clone->addHSTS = true;
114
        $clone->hstsMaxAge = $maxAge;
115
        $clone->hstsSubDomains = $subDomains;
116
        return $clone;
117
    }
118
    public function withoutHSTS(): self
119
    {
120
        $clone = clone $this;
121
        $clone->addHSTS = false;
122
        return $clone;
123
    }
124
125
    private function addCSP(ResponseInterface $response): ResponseInterface
126
    {
127
        return $this->addCSP
128
            ? $response->withHeader(Header::CONTENT_SECURITY_POLICY, $this->cspDirectives)
129
            : $response;
130
    }
131
    private function addHSTS(ResponseInterface $response): ResponseInterface
132
    {
133
        $subDomains = $this->hstsSubDomains ? '; includeSubDomains' : '';
134
        return $this->addHSTS
135
            ? $response->withHeader(Header::STRICT_TRANSPORT_SECURITY, "max-age={$this->hstsMaxAge}{$subDomains}")
136
            : $response;
137
    }
138
}
139