Passed
Pull Request — master (#236)
by Aleksei
13:59
created

ForceSecureConnection::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 2
b 1
f 0
nc 1
nop 1
dl 0
loc 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 from HTTP to HTTPS and adds CSP and HSTS headers.
17
 *
18
 * Note: Prefer forcing HTTPS via web server in case you are not creating installable product such as CMS
19
 * and not hosting the project on a server where you do not have access to web server configuration.
20
 */
21
final class ForceSecureConnection implements MiddlewareInterface
22
{
23
    private const DEFAULT_CSP_DIRECTIVES = 'upgrade-insecure-requests; default-src https:';
24
    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...
25
26
    private bool $redirect = true;
27
    private int $statusCode = Status::MOVED_PERMANENTLY;
28
    private ?int $port = null;
29
30
    private bool $addCSP = true;
31
    private string $cspDirectives = self::DEFAULT_CSP_DIRECTIVES;
32
33
    private bool $addSTS = true;
34
    private int $hstsMaxAge = self::DEFAULT_HSTS_MAX_AGE;
35
    private bool $hstsSubDomains = false;
36
37
    private ResponseFactoryInterface $responseFactory;
38
39
    public function __construct(ResponseFactoryInterface $responseFactory)
40
    {
41
        $this->responseFactory = $responseFactory;
42
    }
43
44
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
45
    {
46
        if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
47
            $url = (string)$request->getUri()->withScheme('https')->withPort($this->port);
48
            return $this->addCSP(
49
                $this->responseFactory
50
                    ->createResponse($this->statusCode)
51
                    ->withHeader(Header::LOCATION, $url)
52
            );
53
        }
54
        return $this->addHSTS($this->addCSP($handler->handle($request)));
55
    }
56
57
    public function withRedirection($statusCode = Status::MOVED_PERMANENTLY, int $port = null): self
58
    {
59
        $clone = clone $this;
60
        $clone->redirect = true;
61
        $clone->port = $port;
62
        $clone->statusCode = $statusCode;
63
        return $clone;
64
    }
65
    public function withoutRedirection(): self
66
    {
67
        $clone = clone $this;
68
        $clone->redirect = false;
69
        return $clone;
70
    }
71
72
    /**
73
     * Add Content-Security-Policy header to Response
74
     * @link https://developer.mozilla.org/docs/Web/HTTP/CSP
75
     * @link https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy
76
     * @param string $directives
77
     * @return ForceSecureConnection
78
     */
79
    public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
80
    {
81
        $clone = clone $this;
82
        $clone->addCSP = true;
83
        $clone->cspDirectives = $directives;
84
        return $clone;
85
    }
86
    public function withoutCSP(): self
87
    {
88
        $clone = clone $this;
89
        $clone->addCSP = false;
90
        return $clone;
91
    }
92
93
    /**
94
     * Add Strict-Transport-Security header to Response
95
     * @link https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security
96
     * @param int $maxAge
97
     * @param bool $subDomains
98
     * @return ForceSecureConnection
99
     */
100
    public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDomains = false): self
101
    {
102
        $clone = clone $this;
103
        $clone->addSTS = true;
104
        $clone->hstsMaxAge = $maxAge;
105
        $clone->hstsSubDomains = $subDomains;
106
        return $clone;
107
    }
108
    public function withoutHSTS(): self
109
    {
110
        $clone = clone $this;
111
        $clone->addSTS = false;
112
        return $clone;
113
    }
114
115
    private function addCSP(ResponseInterface $response): ResponseInterface
116
    {
117
        return $this->addCSP
118
            ? $response->withHeader(Header::CONTENT_SECURITY_POLICY, $this->cspDirectives)
119
            : $response;
120
    }
121
122
    private function addHSTS(ResponseInterface $response): ResponseInterface
123
    {
124
        $subDomains = $this->hstsSubDomains ? 'includeSubDomains' : '';
125
        return $this->addSTS
126
            ? $response->withHeader(Header::STRICT_TRANSPORT_SECURITY, "max-age={$this->hstsMaxAge}; {$subDomains}")
127
            : $response;
128
    }
129
}
130