Issues (3)

src/ForceSecureConnection.php (1 issue)

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