1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Bridges\HttpDI;
9:
10: use Nette;
11:
12:
13: 14: 15:
16: class HttpExtension extends Nette\DI\CompilerExtension
17: {
18: public $defaults = [
19: 'proxy' => [],
20: 'headers' => [
21: 'X-Powered-By' => 'Nette Framework',
22: 'Content-Type' => 'text/html; charset=utf-8',
23: ],
24: 'frames' => 'SAMEORIGIN',
25: 'csp' => [],
26: 'cspReportOnly' => [],
27: 'csp-report' => null,
28: 'featurePolicy' => [],
29: 'cookieSecure' => null,
30: 'sameSiteProtection' => null,
31: ];
32:
33:
34: private $cliMode;
35:
36:
37: public function __construct($cliMode = false)
38: {
39: $this->cliMode = $cliMode;
40: }
41:
42:
43: public function loadConfiguration()
44: {
45: $builder = $this->getContainerBuilder();
46: $config = $this->validateConfig($this->defaults);
47:
48: $builder->addDefinition($this->prefix('requestFactory'))
49: ->setClass(Nette\Http\RequestFactory::class)
50: ->addSetup('setProxy', [$config['proxy']]);
51:
52: $builder->addDefinition($this->prefix('request'))
53: ->setClass(Nette\Http\Request::class)
54: ->setFactory('@Nette\Http\RequestFactory::createHttpRequest');
55:
56: $builder->addDefinition($this->prefix('response'))
57: ->setClass(Nette\Http\Response::class);
58:
59: $builder->addDefinition($this->prefix('context'))
60: ->setClass(Nette\Http\Context::class)
61: ->addSetup('::trigger_error', ['Service http.context is deprecated.', E_USER_DEPRECATED]);
62:
63: if ($this->name === 'http') {
64: $builder->addAlias('nette.httpRequestFactory', $this->prefix('requestFactory'));
65: $builder->addAlias('nette.httpContext', $this->prefix('context'));
66: $builder->addAlias('httpRequest', $this->prefix('request'));
67: $builder->addAlias('httpResponse', $this->prefix('response'));
68: }
69: }
70:
71:
72: public function beforeCompile()
73: {
74: $builder = $this->getContainerBuilder();
75: if (isset($this->config['cookieSecure'])) {
76: $value = $this->config['cookieSecure'] === 'auto'
77: ? $builder::literal('$this->getService(?)->isSecured()', [$this->prefix('request')])
78: : (bool) $this->config['cookieSecure'];
79:
80: $builder->getDefinition($this->prefix('response'))
81: ->addSetup('$cookieSecure', [$value]);
82: $builder->getDefinitionByType(Nette\Http\Session::class)
83: ->addSetup('setOptions', [['cookie_secure' => $value]]);
84: }
85:
86: if (!empty($this->config['sameSiteProtection'])) {
87: $builder->getDefinitionByType(Nette\Http\Session::class)
88: ->addSetup('setOptions', [['cookie_samesite' => 'Lax']]);
89: }
90: }
91:
92:
93: public function afterCompile(Nette\PhpGenerator\ClassType $class)
94: {
95: if ($this->cliMode) {
96: return;
97: }
98:
99: $initialize = $class->getMethod('initialize');
100: $config = $this->getConfig();
101: $headers = $config['headers'];
102:
103: if (isset($config['frames']) && $config['frames'] !== true) {
104: $frames = $config['frames'];
105: if ($frames === false) {
106: $frames = 'DENY';
107: } elseif (preg_match('#^https?:#', $frames)) {
108: $frames = "ALLOW-FROM $frames";
109: }
110: $headers['X-Frame-Options'] = $frames;
111: }
112:
113: if (isset($config['csp-report'])) {
114: trigger_error('Rename csp-repost to cspReportOnly in config.', E_USER_DEPRECATED);
115: $config['cspReportOnly'] = $config['csp-report'];
116: }
117:
118: foreach (['csp', 'cspReportOnly'] as $key) {
119: if (empty($config[$key])) {
120: continue;
121: }
122: $value = self::buildPolicy($config[$key]);
123: if (strpos($value, "'nonce'")) {
124: $value = Nette\DI\ContainerBuilder::literal(
125: 'str_replace(?, ? . (isset($cspNonce) \? $cspNonce : $cspNonce = base64_encode(Nette\Utils\Random::generate(16, "\x00-\xFF"))), ?)',
126: ["'nonce", "'nonce-", $value]
127: );
128: }
129: $headers['Content-Security-Policy' . ($key === 'csp' ? '' : '-Report-Only')] = $value;
130: }
131:
132: if (!empty($config['featurePolicy'])) {
133: $headers['Feature-Policy'] = self::buildPolicy($config['featurePolicy']);
134: }
135:
136: foreach ($headers as $key => $value) {
137: if ($value != null) {
138: $initialize->addBody('$this->getService(?)->setHeader(?, ?);', [$this->prefix('response'), $key, $value]);
139: }
140: }
141:
142: if (!empty($config['sameSiteProtection'])) {
143: $initialize->addBody('$this->getService(?)->setCookie(...?);', [$this->prefix('response'), ['nette-samesite', '1', 0, '/', null, null, true, 'Strict']]);
144: }
145: }
146:
147:
148: private static function buildPolicy(array $config)
149: {
150: static $nonQuoted = ['require-sri-for' => 1, 'sandbox' => 1];
151: $value = '';
152: foreach ($config as $type => $policy) {
153: if ($policy === false) {
154: continue;
155: }
156: $policy = $policy === true ? [] : (array) $policy;
157: $value .= $type;
158: foreach ($policy as $item) {
159: $value .= !isset($nonQuoted[$type]) && preg_match('#^[a-z-]+\z#', $item) ? " '$item'" : " $item";
160: }
161: $value .= '; ';
162: }
163: return $value;
164: }
165: }
166: