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:
87:
88: public function afterCompile(Nette\PhpGenerator\ClassType $class)
89: {
90: if ($this->cliMode) {
91: return;
92: }
93:
94: $initialize = $class->getMethod('initialize');
95: $config = $this->getConfig();
96: $headers = $config['headers'];
97:
98: if (isset($config['frames']) && $config['frames'] !== true) {
99: $frames = $config['frames'];
100: if ($frames === false) {
101: $frames = 'DENY';
102: } elseif (preg_match('#^https?:#', $frames)) {
103: $frames = "ALLOW-FROM $frames";
104: }
105: $headers['X-Frame-Options'] = $frames;
106: }
107:
108: if (isset($config['csp-report'])) {
109: trigger_error('Rename csp-repost to cspReportOnly in config.', E_USER_DEPRECATED);
110: $config['cspReportOnly'] = $config['csp-report'];
111: }
112:
113: foreach (['csp', 'cspReportOnly'] as $key) {
114: if (empty($config[$key])) {
115: continue;
116: }
117: $value = self::buildPolicy($config[$key]);
118: if (strpos($value, "'nonce'")) {
119: $value = Nette\DI\ContainerBuilder::literal(
120: 'str_replace(?, ? . (isset($cspNonce) \? $cspNonce : $cspNonce = base64_encode(Nette\Utils\Random::generate(16, "\x00-\xFF"))), ?)',
121: ["'nonce", "'nonce-", $value]
122: );
123: }
124: $headers['Content-Security-Policy' . ($key === 'csp' ? '' : '-Report-Only')] = $value;
125: }
126:
127: if (!empty($config['featurePolicy'])) {
128: $headers['Feature-Policy'] = self::buildPolicy($config['featurePolicy']);
129: }
130:
131: foreach ($headers as $key => $value) {
132: if ($value != null) {
133: $initialize->addBody('$this->getService(?)->setHeader(?, ?);', [$this->prefix('response'), $key, $value]);
134: }
135: }
136:
137: if (!empty($config['sameSiteProtection'])) {
138: $initialize->addBody('$this->getService(?)->setCookie(...?);', [$this->prefix('response'), ['nette-samesite', '1', 0, '/', null, null, true, 'Strict']]);
139: }
140: }
141:
142:
143: private static function buildPolicy(array $config)
144: {
145: static $nonQuoted = ['require-sri-for' => 1, 'sandbox' => 1];
146: $value = '';
147: foreach ($config as $type => $policy) {
148: if ($policy === false) {
149: continue;
150: }
151: $policy = $policy === true ? [] : (array) $policy;
152: $value .= $type;
153: foreach ($policy as $item) {
154: $value .= !isset($nonQuoted[$type]) && preg_match('#^[a-z-]+\z#', $item) ? " '$item'" : " $item";
155: }
156: $value .= '; ';
157: }
158: return $value;
159: }
160: }
161: