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