‪TYPO3CMS  ‪main
HashProxy.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use GuzzleHttp\Promise;
21 use Psr\Http\Message\ResponseInterface;
26 
31 final class ‪HashProxy implements \JsonSerializable, ‪SourceValueInterface
32 {
33  // one week
34  private const ‪CACHE_LIFETIME = 604800;
35  private ‪HashType ‪$type = HashType::sha256;
36  private ?string ‪$glob = null;
40  private ?array ‪$urls = null;
41 
42  public static function ‪glob(string ‪$glob): self
43  {
44  $pattern = GeneralUtility::getFileAbsFileName(‪$glob);
45  $files = array_filter(‪glob($pattern), 'is_file');
46  if ($files === []) {
47  throw new \LogicException('Glob pattern did not resolve any files', 1678615628);
48  }
49  $target = new self();
50  $target->glob = ‪$glob;
51  return $target;
52  }
53 
54  public static function ‪urls(string ...‪$urls): self
55  {
56  if (‪$urls === []) {
57  throw new \LogicException('No URL provided', 1678617132);
58  }
59  foreach (‪$urls as ‪$url) {
60  if (!self::isValidUrl(‪$url)) {
61  throw new \LogicException(
62  sprintf('Value "%s" is not a valid file-like URL', ‪$url),
63  1678616641
64  );
65  }
66  }
67  $target = new self();
68  $target->urls = ‪$urls;
69  return $target;
70  }
71 
72  public static function ‪knows(string $value): bool
73  {
74  return str_starts_with($value, "'hash-proxy-") && $value[-1] === "'";
75  }
76 
77  public static function ‪parse(string $value): self
78  {
79  if (!self::knows($value)) {
80  throw new \LogicException(sprintf('Parsing "%s" is not known', $value), 1678619052);
81  }
82  // extract from `'hash-proxy-[...]'`
83  $value = substr($value, 12, -1);
84  $properties = json_decode($value, true, 4, JSON_THROW_ON_ERROR);
85  if (!empty($properties['glob'])) {
86  $target = ‪self::glob($properties['glob']);
87  } elseif (!empty($properties['urls'])) {
88  $target = ‪self::urls(...$properties['urls']);
89  } else {
90  throw new \LogicException('Cannot parse payload', 1678619395);
91  }
92  return $target->withType(HashType::from($properties['type'] ?? ''));
93  }
94 
95  public function ‪withType(‪HashType ‪$type): self
96  {
97  if ($this->type === ‪$type) {
98  return $this;
99  }
100  $target = clone $this;
101  $target->type = ‪$type;
102  return $target;
103  }
104 
105  public function ‪isEmpty(): bool
106  {
107  return $this->‪glob === null && $this->‪urls === null;
108  }
109 
110  public function ‪compile(?‪FrontendInterface $cache = null): ?string
111  {
112  if ($this->‪isEmpty()) {
113  return null;
114  }
115  $hashes = array_map(
116  fn(string $hash): string => sprintf("'%s-%s'", $this->type->value, $hash),
117  $this->compileHashValues($cache)
118  );
119  return implode(' ', $hashes);
120  }
121 
122  public function ‪serialize(): ?string
123  {
124  if ($this->‪isEmpty()) {
125  return null;
126  }
127  return sprintf("'hash-proxy-%s'", json_encode($this, JSON_UNESCAPED_SLASHES));
128  }
129 
130  public function ‪jsonSerialize(): mixed
131  {
132  return [
133  'type' => ‪$this->type,
134  'glob' => ‪$this->glob,
135  'urls' => ‪$this->urls,
136  ];
137  }
138 
142  private static function ‪isValidUrl(string $value): bool
143  {
144  try {
145  $uri = new ‪Uri($value);
146  } catch (\InvalidArgumentException) {
147  return false;
148  }
149  return basename($uri->getPath()) !== '';
150  }
151 
152  private function ‪compileHashValues(?‪FrontendInterface $cache): array
153  {
154  if ($this->‪glob !== null) {
155  $pattern = GeneralUtility::getFileAbsFileName($this->‪glob);
156  $files = array_filter(‪glob($pattern), 'is_file');
157  return array_map(
158  fn(string $file): string => base64_encode(
159  hash_file($this->type->value, $file, true)
160  ),
161  $files
162  );
163  }
164  if ($this->‪urls !== null) {
165  $hashes = [];
167  // try to resolve hashes from cache
168  if ($cache !== null) {
169  ‪$urls = [];
170  $identifiers = [];
171  foreach ($this->‪urls as ‪$url) {
172  $identifiers[‪$url] = 'CspHashProxyUrl_' . sha1(json_encode([$this->type, ‪$url]));
173  $cachedHash = $cache->‪get($identifiers[‪$url]);
174  if ($cachedHash === false) {
175  // fetch content of URL & generate hash
176  ‪$urls[] = ‪$url;
177  } elseif ($cachedHash !== null) {
178  // only use cached hash of URL that did not fail previously
179  $hashes[] = $cachedHash;
180  }
181  }
182  }
183  // process content of remaining URLs
184  $contents = $this->fetchUrlContents(‪$urls);
185  foreach ($contents as ‪$url => $content) {
186  $contentHash = $content !== null ? base64_encode(hash($this->type->value, $content, true)) : null;
187  if ($contentHash !== null) {
188  $hashes[] = $contentHash;
189  }
190  if ($cache !== null && isset($identifiers[‪$url])) {
191  $cache->‪set($identifiers[‪$url], $contentHash, ['CspHashProxyUrl'], self::CACHE_LIFETIME);
192  }
193  }
194  return $hashes;
195  }
196  return [];
197  }
198 
203  private function fetchUrlContents(array ‪$urls): array
204  {
205  $client = GeneralUtility::makeInstance(GuzzleClientFactory::class)->getClient();
207 
208  foreach (‪$urls as ‪$url) {
209  ‪$promises[‪$url] = $client->requestAsync('GET', ‪$url);
210  }
211 
212  ‪$resolvedPromises = Promise\Utils::settle(‪$promises)->wait();
213  return array_map(
214  static function (array $response): ?string {
215  if ($response['state'] === 'fulfilled' && $response['value'] instanceof ResponseInterface) {
216  return (string)$response['value']->getBody();
217  }
218  return null;
219  },
221  );
222  }
223 }
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\knows
‪static knows(string $value)
Definition: HashProxy.php:72
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceValueInterface
Definition: SourceValueInterface.php:31
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\withType
‪withType(HashType $type)
Definition: HashProxy.php:95
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\parse
‪static parse(string $value)
Definition: HashProxy.php:77
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\compile
‪compile(?FrontendInterface $cache=null)
Definition: HashProxy.php:110
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\$promises
‪$promises
Definition: HashProxy.php:206
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\isEmpty
‪isEmpty()
Definition: HashProxy.php:105
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\$glob
‪string $glob
Definition: HashProxy.php:36
‪TYPO3\CMS\Core\Http\Client\GuzzleClientFactory
Definition: GuzzleClientFactory.php:28
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\$type
‪HashType $type
Definition: HashProxy.php:35
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\$urls
‪array $urls
Definition: HashProxy.php:40
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\CACHE_LIFETIME
‪const CACHE_LIFETIME
Definition: HashProxy.php:34
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:30
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\$resolvedPromises
‪foreach($urls as $url) $resolvedPromises
Definition: HashProxy.php:212
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface\get
‪mixed get($entryIdentifier)
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy
Definition: HashProxy.php:32
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\isValidUrl
‪static isValidUrl(string $value)
Definition: HashProxy.php:142
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\glob
‪static glob(string $glob)
Definition: HashProxy.php:42
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\urls
‪static urls(string ... $urls)
Definition: HashProxy.php:54
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy
Definition: ConsumableNonce.php:18
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\compileHashValues
‪compileHashValues(?FrontendInterface $cache)
Definition: HashProxy.php:152
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\serialize
‪serialize()
Definition: HashProxy.php:122
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashProxy\jsonSerialize
‪jsonSerialize()
Definition: HashProxy.php:130
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface\set
‪set($entryIdentifier, $data, array $tags=[], $lifetime=null)
‪TYPO3\CMS\Core\Security\ContentSecurityPolicy\HashType
‪HashType
Definition: HashType.php:25
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52