| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* This file is part of the Nette Framework (https://nette.org) |
| 5 |
* Copyright (c) 2004 David Grudl (https://davidgrudl.com) |
| 6 |
*/ |
| 7 |
declare (strict_types=1); |
| 8 |
namespace Packetery\Nette\Bridges\HttpDI; |
| 9 |
|
| 10 |
use Packetery\Nette; |
| 11 |
use Packetery\Nette\Schema\Expect; |
| 12 |
/** |
| 13 |
* HTTP extension for Nette DI. |
| 14 |
*/ |
| 15 |
class HttpExtension extends \Packetery\Nette\DI\CompilerExtension |
| 16 |
{ |
| 17 |
/** @var bool */ |
| 18 |
private $cliMode; |
| 19 |
public function __construct(bool $cliMode = \false) |
| 20 |
{ |
| 21 |
$this->cliMode = $cliMode; |
| 22 |
} |
| 23 |
public function getConfigSchema() : \Packetery\Nette\Schema\Schema |
| 24 |
{ |
| 25 |
return Expect::structure([ |
| 26 |
'proxy' => Expect::anyOf(Expect::arrayOf('string'), Expect::string()->castTo('array'))->firstIsDefault()->dynamic(), |
| 27 |
'headers' => Expect::arrayOf('scalar|null')->default(['X-Powered-By' => 'Nette Framework 3', 'Content-Type' => 'text/html; charset=utf-8'])->mergeDefaults(), |
| 28 |
'frames' => Expect::anyOf(Expect::string(), Expect::bool(), null)->default('SAMEORIGIN'), |
| 29 |
// X-Frame-Options |
| 30 |
'csp' => Expect::arrayOf('array|scalar|null'), |
| 31 |
// Content-Security-Policy |
| 32 |
'cspReportOnly' => Expect::arrayOf('array|scalar|null'), |
| 33 |
// Content-Security-Policy-Report-Only |
| 34 |
'featurePolicy' => Expect::arrayOf('array|scalar|null'), |
| 35 |
// Feature-Policy |
| 36 |
'cookiePath' => Expect::string()->dynamic(), |
| 37 |
'cookieDomain' => Expect::string()->dynamic(), |
| 38 |
'cookieSecure' => Expect::anyOf('auto', null, \true, \false)->firstIsDefault()->dynamic(), |
| 39 |
// Whether the cookie is available only through HTTPS |
| 40 |
'disableNetteCookie' => Expect::bool(\false), |
| 41 |
]); |
| 42 |
} |
| 43 |
public function loadConfiguration() |
| 44 |
{ |
| 45 |
$builder = $this->getContainerBuilder(); |
| 46 |
$config = $this->config; |
| 47 |
$builder->addDefinition($this->prefix('requestFactory'))->setFactory(\Packetery\Nette\Http\RequestFactory::class)->addSetup('setProxy', [$config->proxy]); |
| 48 |
$request = $builder->addDefinition($this->prefix('request'))->setFactory('@\\Packetery\\Nette\\Http\\RequestFactory::fromGlobals'); |
| 49 |
$response = $builder->addDefinition($this->prefix('response'))->setFactory(\Packetery\Nette\Http\Response::class); |
| 50 |
if ($config->cookiePath !== null) { |
| 51 |
$response->addSetup('$cookiePath', [$config->cookiePath]); |
| 52 |
} |
| 53 |
if ($config->cookieDomain !== null) { |
| 54 |
$value = $config->cookieDomain === 'domain' ? $builder::literal('$this->getService(?)->getUrl()->getDomain(2)', [$request->getName()]) : $config->cookieDomain; |
| 55 |
$response->addSetup('$cookieDomain', [$value]); |
| 56 |
} |
| 57 |
if ($config->cookieSecure !== null) { |
| 58 |
$value = $config->cookieSecure === 'auto' ? $builder::literal('$this->getService(?)->isSecured()', [$request->getName()]) : $config->cookieSecure; |
| 59 |
$response->addSetup('$cookieSecure', [$value]); |
| 60 |
} |
| 61 |
if ($this->name === 'http') { |
| 62 |
$builder->addAlias('nette.httpRequestFactory', $this->prefix('requestFactory')); |
| 63 |
$builder->addAlias('httpRequest', $this->prefix('request')); |
| 64 |
$builder->addAlias('httpResponse', $this->prefix('response')); |
| 65 |
} |
| 66 |
if (!$this->cliMode) { |
| 67 |
$this->sendHeaders(); |
| 68 |
} |
| 69 |
} |
| 70 |
private function sendHeaders() |
| 71 |
{ |
| 72 |
$config = $this->config; |
| 73 |
$headers = \array_map('strval', $config->headers); |
| 74 |
if (isset($config->frames) && $config->frames !== \true && !isset($headers['X-Frame-Options'])) { |
| 75 |
$frames = $config->frames; |
| 76 |
if ($frames === \false) { |
| 77 |
$frames = 'DENY'; |
| 78 |
} elseif (\preg_match('#^https?:#', $frames)) { |
| 79 |
$frames = "ALLOW-FROM {$frames}"; |
| 80 |
} |
| 81 |
$headers['X-Frame-Options'] = $frames; |
| 82 |
} |
| 83 |
foreach (['csp', 'cspReportOnly'] as $key) { |
| 84 |
if (empty($config->{$key})) { |
| 85 |
continue; |
| 86 |
} |
| 87 |
$value = self::buildPolicy($config->{$key}); |
| 88 |
if (\strpos($value, "'nonce'")) { |
| 89 |
$this->initialization->addBody('$cspNonce = base64_encode(random_bytes(16));'); |
| 90 |
$value = \Packetery\Nette\DI\ContainerBuilder::literal('str_replace(?, ? . $cspNonce, ?)', ["'nonce", "'nonce-", $value]); |
| 91 |
} |
| 92 |
$headers['Content-Security-Policy' . ($key === 'csp' ? '' : '-Report-Only')] = $value; |
| 93 |
} |
| 94 |
if (!empty($config->featurePolicy)) { |
| 95 |
$headers['Feature-Policy'] = self::buildPolicy($config->featurePolicy); |
| 96 |
} |
| 97 |
$this->initialization->addBody('$response = $this->getService(?);', [$this->prefix('response')]); |
| 98 |
foreach ($headers as $key => $value) { |
| 99 |
if ($value !== '') { |
| 100 |
$this->initialization->addBody('$response->setHeader(?, ?);', [$key, $value]); |
| 101 |
} |
| 102 |
} |
| 103 |
if (!$config->disableNetteCookie) { |
| 104 |
$this->initialization->addBody('\\Packetery\\Nette\\Http\\Helpers::initCookie($this->getService(?), $response);', [$this->prefix('request')]); |
| 105 |
} |
| 106 |
} |
| 107 |
private static function buildPolicy(array $config) : string |
| 108 |
{ |
| 109 |
$nonQuoted = ['require-sri-for' => 1, 'sandbox' => 1]; |
| 110 |
$value = ''; |
| 111 |
foreach ($config as $type => $policy) { |
| 112 |
if ($policy === \false) { |
| 113 |
continue; |
| 114 |
} |
| 115 |
$policy = $policy === \true ? [] : (array) $policy; |
| 116 |
$value .= $type; |
| 117 |
foreach ($policy as $item) { |
| 118 |
if (\is_array($item)) { |
| 119 |
$item = \key($item) . ':'; |
| 120 |
} |
| 121 |
$value .= !isset($nonQuoted[$type]) && \preg_match('#^[a-z-]+$#D', $item) ? " '{$item}'" : " {$item}"; |
| 122 |
} |
| 123 |
$value .= '; '; |
| 124 |
} |
| 125 |
return $value; |
| 126 |
} |
| 127 |
} |
| 128 |
|