Skip to content

Commit c58a92c

Browse files
committed
add allow_404 option to allow 404 pages to be redirected from http to https
1 parent f5f7f11 commit c58a92c

12 files changed

+275
-12
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Features
2626
- [x] Enable/disable HTTP Strict Transport Security Header and set its value.
2727
- [x] Allow add `www.` prefix during redirection from http or already https.
2828
- [x] Allow remove `www.` prefix during redirection from http or already https.
29+
- [x] Force Https for 404 pages
2930

3031
Installation
3132
------------
@@ -87,6 +88,8 @@ return [
8788
// remove existing "www." prefix during redirection from http or already https
8889
// only works if previous's config 'add_www_prefix' => false
8990
'remove_www_prefix' => false,
91+
// Force Https for 404 pages
92+
'allow_404' => true,
9093
],
9194
// ...
9295
];

config/expressive-force-https-module.local.php.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ return [
1717
],
1818
'add_www_prefix' => false,
1919
'remove_www_prefix' => false,
20+
'allow_404' => true,
2021
],
2122

2223
'dependencies' => [

config/force-https-module.local.php.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ return [
1515
],
1616
'add_www_prefix' => false,
1717
'remove_www_prefix' => false,
18+
'allow_404' => true,
1819
],
1920
];

config/module.config.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
return [
66
'service_manager' => [
77
'factories' => [
8-
Listener\ForceHttps::class => Listener\ForceHttpsFactory::class,
8+
Listener\ForceHttps::class => Listener\ForceHttpsFactory::class,
9+
Listener\NotFoundLoggingListenerOnSharedEventManager::class => Listener\NotFoundLoggingListenerOnSharedEventManagerFactory::class,
910
],
1011
],
1112
'listeners' => [

spec/Listener/ForceHttpsSpec.php

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Zend\EventManager\EventManagerInterface;
1111
use Zend\Http\PhpEnvironment\Request;
1212
use Zend\Http\PhpEnvironment\Response;
13-
use Zend\Mvc\Application;
1413
use Zend\Mvc\MvcEvent;
1514
use Zend\Router\RouteMatch;
1615
use Zend\Uri\Uri;
@@ -50,6 +49,7 @@
5049
$listener->attach($this->eventManager);
5150

5251
expect($this->eventManager)->not->toReceive('attach')->with(MvcEvent::EVENT_ROUTE, [$listener, 'forceHttpsScheme']);
52+
expect($this->eventManager)->not->toReceive('attach')->with(MvcEvent::EVENT_DISPATCH_ERROR, [$listener, 'forceHttpsScheme'], 1000);
5353

5454
});
5555

@@ -106,6 +106,27 @@
106106

107107
});
108108

109+
it('not redirect if router not match', function () {
110+
111+
$listener = new ForceHttps([
112+
'enable' => true,
113+
'force_all_routes' => true,
114+
'force_specific_routes' => [],
115+
]);
116+
117+
allow($this->mvcEvent)->toReceive('getRouteMatch')->andReturn(null);
118+
allow($this->routeMatch)->toReceive('getMatchedRouteName')->andReturn('about');
119+
120+
allow($this->mvcEvent)->toReceive('getRequest', 'getUri', 'getScheme')->andReturn('https');
121+
allow($this->mvcEvent)->toReceive('getRequest', 'getUri', 'toString')->andReturn('https://www.example.com/about');
122+
allow($this->mvcEvent)->toReceive('getResponse')->andReturn($this->response);
123+
expect($this->mvcEvent)->toReceive('getResponse');
124+
125+
$listener->forceHttpsScheme($this->mvcEvent);
126+
expect($this->response)->not->toReceive('getHeaders');
127+
128+
});
129+
109130
});
110131

111132
context('on current scheme is http', function () {
@@ -129,6 +150,24 @@
129150
$listener->forceHttpsScheme($this->mvcEvent);
130151

131152
expect($this->mvcEvent)->toReceive('getResponse');
153+
expect($this->response)->not->toReceive('send');
154+
155+
});
156+
157+
it('not redirect on router not match', function () {
158+
159+
$listener = new ForceHttps([
160+
'enable' => true,
161+
]);
162+
163+
allow($this->mvcEvent)->toReceive('getRequest', 'getUri', 'getScheme')->andReturn('http');
164+
allow($this->mvcEvent)->toReceive('getRouteMatch')->andReturn(null);
165+
allow($this->mvcEvent)->toReceive('getResponse')->andReturn($this->response);
166+
167+
$listener->forceHttpsScheme($this->mvcEvent);
168+
169+
expect($this->mvcEvent)->toReceive('getResponse');
170+
expect($this->response)->not->toReceive('send');
132171

133172
});
134173

@@ -232,6 +271,35 @@
232271

233272
});
234273

274+
275+
it('redirect no router not match, but allow_404 is true', function () {
276+
277+
$listener = new ForceHttps([
278+
'enable' => true,
279+
'allow_404' => true,
280+
]);
281+
282+
allow($this->mvcEvent)->toReceive('getRequest')->andReturn($this->request);
283+
allow($this->request)->toReceive('getUri')->andReturn($this->uri);
284+
allow($this->uri)->toReceive('getScheme')->andReturn('http');
285+
allow($this->mvcEvent)->toReceive('getRouteMatch')->andReturn(null);
286+
allow($this->uri)->toReceive('setScheme')->with('https')->andReturn($this->uri);
287+
allow($this->uri)->toReceive('toString')->andReturn('https://example.com/404');
288+
allow($this->mvcEvent)->toReceive('getResponse')->andReturn($this->response);
289+
allow($this->response)->toReceive('setStatusCode')->with(308)->andReturn($this->response);
290+
allow($this->response)->toReceive('getHeaders', 'addHeaderLine')->with('Location', 'https://example.com/404');
291+
allow($this->response)->toReceive('send');
292+
293+
$closure = function () use ($listener) {
294+
$listener->forceHttpsScheme($this->mvcEvent);
295+
};
296+
expect($closure)->toThrow(new QuitException('Exit statement occurred', 0));
297+
298+
expect($this->mvcEvent)->toReceive('getResponse');
299+
expect($this->response)->toReceive('getHeaders', 'addHeaderLine')->with('Location', 'https://example.com/404');
300+
301+
});
302+
235303
it('redirect with www prefix with configurable "add_www_prefix" on force_all_routes', function () {
236304

237305
$listener = new ForceHttps([

spec/Middleware/ForceHttpsSpec.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
$match = RouteResult::fromRouteFailure(null);
5151
allow($this->router)->toReceive('match')->andReturn($match);
52+
allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http');
5253

5354
$listener = new ForceHttps(['enable' => true], $this->router);
5455

@@ -61,6 +62,29 @@
6162

6263
});
6364

65+
it('not redirect on router not match and config allow_404 is false', function () {
66+
67+
$match = RouteResult::fromRouteFailure(null);
68+
allow($this->router)->toReceive('match')->andReturn($match);
69+
allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http');
70+
71+
$listener = new ForceHttps(
72+
[
73+
'enable' => true,
74+
'allow_404' => false,
75+
],
76+
$this->router
77+
);
78+
79+
$handler = Double::instance(['implements' => RequestHandlerInterface::class]);
80+
allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response);
81+
82+
$listener->process($this->request, $handler);
83+
84+
expect($this->response)->not->toReceive('withStatus');
85+
86+
});
87+
6488
it('not redirect on https and match but no strict_transport_security config', function () {
6589

6690
$match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class])));
@@ -199,6 +223,33 @@
199223

200224
});
201225

226+
it('return Response with 308 status on http and not match, but allow_404 is true', function () {
227+
228+
$match = RouteResult::fromRouteFailure(null);
229+
230+
allow($this->router)->toReceive('match')->andReturn($match);
231+
allow($this->request)->toReceive('getUri', 'getScheme')->andReturn('http');
232+
allow($this->request)->toReceive('getUri', 'withScheme', '__toString')->andReturn('https://example.com/404');
233+
234+
$handler = Double::instance(['implements' => RequestHandlerInterface::class]);
235+
allow($handler)->toReceive('handle')->with($this->request)->andReturn($this->response);
236+
allow($this->response)->toReceive('withStatus')->with(308)->andReturn($this->response);
237+
allow($this->response)->toReceive('withHeader')->with('Location', 'https://example.com/404')->andReturn($this->response);
238+
239+
$listener = new ForceHttps(
240+
[
241+
'enable' => true,
242+
'allow_404' => true,
243+
],
244+
$this->router
245+
);
246+
$listener->process($this->request, $handler);
247+
248+
expect($this->response)->toReceive('withStatus')->with(308);
249+
expect($this->response)->toReceive('withHeader')->with('Location', 'https://example.com/404');
250+
251+
});
252+
202253
it('return Response with 308 status with include www prefix on http and match with configurable "add_www_prefix"', function () {
203254

204255
$match = RouteResult::fromRoute(new Route('/about', Double::instance(['implements' => MiddlewareInterface::class])));

src/HttpsTrait.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,22 @@ private function isSchemeHttps(string $uriScheme) : bool
2525
/**
2626
* Check Config if is going to be forced to https.
2727
*
28-
* @param RouteMatch|RouteResult $match
28+
* @param RouteMatch|RouteResult|null $match
2929
*/
30-
private function isGoingToBeForcedToHttps($match) : bool
30+
private function isGoingToBeForcedToHttps($match = null) : bool
3131
{
32+
$is404 = $match === null || ($match instanceof RouteResult && $match->isFailure());
33+
if (isset($this->config['allow_404']) &&
34+
$this->config['allow_404'] === true &&
35+
$is404
36+
) {
37+
return true;
38+
}
39+
40+
if ($is404) {
41+
return false;
42+
}
43+
3244
if (! $this->config['force_all_routes'] &&
3345
! \in_array(
3446
$match->getMatchedRouteName(),
@@ -44,11 +56,11 @@ private function isGoingToBeForcedToHttps($match) : bool
4456
/**
4557
* Check if Setup Strict-Transport-Security need to be skipped.
4658
*
47-
* @param RouteMatch|RouteResult $match
48-
* @param Response|ResponseInterface $response
59+
* @param RouteMatch|RouteResult|null $match
60+
* @param Response|ResponseInterface $response
4961
*
5062
*/
51-
private function isSkippedHttpStrictTransportSecurity(string $uriScheme, $match, $response) : bool
63+
private function isSkippedHttpStrictTransportSecurity(string $uriScheme, $match = null, $response) : bool
5264
{
5365
return ! $this->isSchemeHttps($uriScheme) ||
5466
! $this->isGoingToBeForcedToHttps($match) ||

src/Listener/ForceHttps.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ public function attach(EventManagerInterface $events, $priority = 1) : void
3333
}
3434

3535
$this->listeners[] = $events->attach(MvcEvent::EVENT_ROUTE, [$this, 'forceHttpsScheme']);
36+
$this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'forceHttpsScheme'], 1000);
3637
}
3738

38-
private function setHttpStrictTransportSecurity(string $uriScheme, RouteMatch $match, Response $response) : Response
39+
private function setHttpStrictTransportSecurity(string $uriScheme, RouteMatch $match = null, Response $response) : Response
3940
{
4041
if ($this->isSkippedHttpStrictTransportSecurity($uriScheme, $match, $response)) {
4142
return $response;
@@ -67,7 +68,7 @@ public function forceHttpsScheme(MvcEvent $e) : void
6768
/** @var string $uriScheme*/
6869
$uriScheme = $uri->getScheme();
6970

70-
/** @var RouteMatch $routeMatch */
71+
/** @var RouteMatch|null $routeMatch */
7172
$routeMatch = $e->getRouteMatch();
7273
$response = $this->setHttpStrictTransportSecurity($uriScheme, $routeMatch, $response);
7374
if (! $this->isGoingToBeForcedToHttps($routeMatch)) {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ForceHttpsModule\Listener;
6+
7+
use ForceHttpsModule\HttpsTrait;
8+
use Zend\Http\PhpEnvironment\Response;
9+
use Zend\Mvc\MvcEvent;
10+
use Zend\Router\RouteMatch;
11+
12+
class ForceHttpsOnSharedEventManager
13+
{
14+
use HttpsTrait;
15+
16+
/**
17+
* @var array
18+
*/
19+
private $config;
20+
21+
public function __construct(array $config)
22+
{
23+
$this->config = $config;
24+
}
25+
26+
private function setHttpStrictTransportSecurity(string $uriScheme, Response $response) : Response
27+
{
28+
if ($this->isSkippedHttpStrictTransportSecurity($uriScheme, null, $response)) {
29+
return $response;
30+
}
31+
32+
if ($this->config['strict_transport_security']['enable'] === true) {
33+
$response->getHeaders()
34+
->addHeaderLine('Strict-Transport-Security: ' . $this->config['strict_transport_security']['value']);
35+
return $response;
36+
}
37+
38+
// set max-age = 0 to strictly expire it,
39+
$response->getHeaders()
40+
->addHeaderLine('Strict-Transport-Security: max-age=0');
41+
return $response;
42+
}
43+
44+
public function __invoke(MvcEvent $e)
45+
{
46+
/** @var \Zend\Router\RouteMatch $routeMatch */
47+
$routeMatch = $e->getRouteMatch();
48+
49+
$controller = $e->getTarget();
50+
$action = \str_replace('-', '', $routeMatch->getParam('action')) . 'Action';
51+
52+
if (\method_exists($controller, $action)) {
53+
return;
54+
}
55+
56+
/** @var \Zend\Http\PhpEnvironment\Request $request */
57+
$request = $e->getRequest();
58+
/** @var Response $response */
59+
$response = $e->getResponse();
60+
61+
$uri = $request->getUri();
62+
/** @var string $uriScheme*/
63+
$uriScheme = $uri->getScheme();
64+
65+
$response = $this->setHttpStrictTransportSecurity($uriScheme, $response);
66+
if (! $this->isGoingToBeForcedToHttps(null)) {
67+
return;
68+
}
69+
70+
if ($this->isSchemeHttps($uriScheme)) {
71+
$uriString = $uri->toString();
72+
$httpsRequestUri = $this->getFinalhttpsRequestUri($uriString);
73+
74+
if ($uriString === $httpsRequestUri) {
75+
return;
76+
}
77+
}
78+
79+
if (! isset($httpsRequestUri)) {
80+
$uriString = $uri->setScheme('https')->toString();
81+
$httpsRequestUri = $this->getFinalhttpsRequestUri($uriString);
82+
}
83+
84+
// 308 keeps headers, request method, and request body
85+
$response->setStatusCode(308);
86+
$response->getHeaders()
87+
->addHeaderLine('Location', $httpsRequestUri);
88+
$response->send();
89+
90+
exit(0);
91+
}
92+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ForceHttpsModule\Listener;
6+
7+
use Psr\Container\ContainerInterface;
8+
9+
class ForceHttpsOnSharedEventManagerFactory
10+
{
11+
public function __invoke(ContainerInterface $container) : ForceHttps
12+
{
13+
$config = $container->get('config');
14+
$forceHttpsConfig = $config['force-https-module'] ?? ['enable' => false];
15+
16+
return new ForceHttpsOnSharedEventManager($forceHttpsConfig);
17+
}
18+
}

0 commit comments

Comments
 (0)