From 96561fcec33c6a0f2f69a73939134a0c8f462006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Luke=C5=A1?= Date: Wed, 21 Dec 2016 14:11:11 +0100 Subject: [PATCH 1/5] WebLoader: added support for SRI generating --- WebLoader/Nette/CssLoader.php | 18 ++++++---- WebLoader/Nette/JavaScriptLoader.php | 8 ++++- WebLoader/Nette/WebLoader.php | 51 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/WebLoader/Nette/CssLoader.php b/WebLoader/Nette/CssLoader.php index 0fd9935..69a6033 100644 --- a/WebLoader/Nette/CssLoader.php +++ b/WebLoader/Nette/CssLoader.php @@ -112,13 +112,17 @@ public function setAlternate($alternate) */ public function getElement($source) { - if ($this->alternate) { - $alternate = ' alternate'; - } else { - $alternate = ''; - } - - return Html::el("link")->rel("stylesheet".$alternate)->type($this->type)->media($this->media)->title($this->title)->href($source); + $alternate = $this->alternate ? ' alternate' : ''; + $content = $this->getCompiledFileContent($source); + $sriChecksum = $this->getSriChecksums($content); + + return Html::el('link') + ->integrity($sriChecksum) + ->rel('stylesheet' . $alternate) + ->type($this->type) + ->media($this->media) + ->title($this->title) + ->href($source); } } diff --git a/WebLoader/Nette/JavaScriptLoader.php b/WebLoader/Nette/JavaScriptLoader.php index 1b138b2..b340e50 100644 --- a/WebLoader/Nette/JavaScriptLoader.php +++ b/WebLoader/Nette/JavaScriptLoader.php @@ -20,7 +20,13 @@ class JavaScriptLoader extends WebLoader */ public function getElement($source) { - return Html::el("script")->type("text/javascript")->src($source); + $content = $this->getCompiledFileContent($source); + $sriChecksum = $this->getSriChecksums($content); + + return Html::el("script") + ->integrity($sriChecksum) + ->type("text/javascript") + ->src($source); } } \ No newline at end of file diff --git a/WebLoader/Nette/WebLoader.php b/WebLoader/Nette/WebLoader.php index 6a3fb48..59f2931 100644 --- a/WebLoader/Nette/WebLoader.php +++ b/WebLoader/Nette/WebLoader.php @@ -94,9 +94,60 @@ public function render() } } + /** + * Get content of a compiled file by its URL path + * + * @param string $source + * @return string + */ + protected function getCompiledFileContent($source) + { + $outputDir = $this->compiler->getOutputDir(); + $urlPath = parse_url($source, PHP_URL_PATH); + $fileName = basename($urlPath); + $filePath = $outputDir . '/' . $fileName; + $content = file_get_contents($filePath); + + return $content; + } + protected function getGeneratedFilePath($file) { return $this->tempPath . '/' . $file->file . '?' . $file->lastModified; } + /** + * Generate Subresource Integrity checksums for all hashing algorithms + * + * @link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity + * @param string $fileContent + * @return string + */ + protected function getSriChecksums($fileContent) + { + // TODO add to config webloader.css.default.sriHashingAlgorithm: [] + + $hashingAlgorithms = [ + 'sha256', + 'sha384', + 'sha512', + ]; + + $checksums = []; + + foreach ($hashingAlgorithms as $algorithm) { + $checksums[] = $this->getOneSriChecksum($algorithm, $fileContent); + } + + return implode(' ', $checksums); + } + + private function getOneSriChecksum($hashingAlgorithm, $fileContent) + { + $hash = hash($hashingAlgorithm, $fileContent, true); + $hashBase64 = base64_encode($hash); + + return $hashingAlgorithm . '-' . $hashBase64; + } + } From daa0b004eb05b1f92336c9408a5e7d799c7780fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Luke=C5=A1?= Date: Wed, 21 Dec 2016 16:58:51 +0100 Subject: [PATCH 2/5] WebLoader: SRI hashing algorithms can now be set via config and defaults to sha256 --- README.md | 4 ++++ WebLoader/Compiler.php | 22 ++++++++++++++++++++++ WebLoader/Nette/CssLoader.php | 2 +- WebLoader/Nette/Extension.php | 10 ++++++++++ WebLoader/Nette/JavaScriptLoader.php | 2 +- WebLoader/Nette/WebLoader.php | 21 ++++++++++----------- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 354ab36..6480fe0 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ webloader: watchFiles: # only watch modify file - {files: ["*.css", "*.less"], from: css} - {files: ["*.css", "*.less"], in: css} + sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified + - sha256 js: default: @@ -81,6 +83,8 @@ webloader: files: - %appDir%/../libs/nette/nette/client-side/netteForms.js - web.js + sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified + - sha256 ``` For older versions of Nette, you have to register the extension in `app/bootstrap.php`: diff --git a/WebLoader/Compiler.php b/WebLoader/Compiler.php index 370995e..81e7abe 100644 --- a/WebLoader/Compiler.php +++ b/WebLoader/Compiler.php @@ -34,6 +34,9 @@ class Compiler /** @var bool */ private $debugging = FALSE; + /** @var array */ + private $sriHashingAlgorithms = array(); + public function __construct(IFileCollection $files, IOutputNamingConvention $convention, $outputDir) { $this->collection = $files; @@ -303,6 +306,25 @@ public function addFileFilter($filter) $this->fileFilters[] = $filter; } + /** + * Add hashing algorithm for Subresource Integrity checksum + * + * @link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity + * @param string $algorithm + */ + public function addSriHashingAlgorithm($algorithm) + { + $this->sriHashingAlgorithms[] = $algorithm; + } + + /** + * @return array + */ + public function getSriHashingAlgorithms() + { + return $this->sriHashingAlgorithms; + } + /** * @return array */ diff --git a/WebLoader/Nette/CssLoader.php b/WebLoader/Nette/CssLoader.php index 69a6033..5331eee 100644 --- a/WebLoader/Nette/CssLoader.php +++ b/WebLoader/Nette/CssLoader.php @@ -114,7 +114,7 @@ public function getElement($source) { $alternate = $this->alternate ? ' alternate' : ''; $content = $this->getCompiledFileContent($source); - $sriChecksum = $this->getSriChecksums($content); + $sriChecksum = $this->getSriChecksums($content) ?: false; return Html::el('link') ->integrity($sriChecksum) diff --git a/WebLoader/Nette/Extension.php b/WebLoader/Nette/Extension.php index 4e5013e..8fb245f 100644 --- a/WebLoader/Nette/Extension.php +++ b/WebLoader/Nette/Extension.php @@ -38,6 +38,9 @@ public function getDefaultConfig() 'fileFilters' => array(), 'joinFiles' => TRUE, 'namingConvention' => '@' . $this->prefix('jsNamingConvention'), + 'sriHashingAlgorithms' => array( + 'sha256', + ), ), 'cssDefaults' => array( 'checkLastModified' => TRUE, @@ -52,6 +55,9 @@ public function getDefaultConfig() 'fileFilters' => array(), 'joinFiles' => TRUE, 'namingConvention' => '@' . $this->prefix('cssNamingConvention'), + 'sriHashingAlgorithms' => array( + 'sha256', + ), ), 'js' => array( @@ -149,6 +155,10 @@ private function addWebLoader(ContainerBuilder $builder, $name, $config) $compiler->addSetup('setCheckLastModified', array($config['checkLastModified'])); + foreach ($config['sriHashingAlgorithms'] as $algorithm) { + $compiler->addSetup('addSriHashingAlgorithm', array($algorithm)); + } + // todo css media } diff --git a/WebLoader/Nette/JavaScriptLoader.php b/WebLoader/Nette/JavaScriptLoader.php index b340e50..c88e082 100644 --- a/WebLoader/Nette/JavaScriptLoader.php +++ b/WebLoader/Nette/JavaScriptLoader.php @@ -21,7 +21,7 @@ class JavaScriptLoader extends WebLoader public function getElement($source) { $content = $this->getCompiledFileContent($source); - $sriChecksum = $this->getSriChecksums($content); + $sriChecksum = $this->getSriChecksums($content) ?: false; return Html::el("script") ->integrity($sriChecksum) diff --git a/WebLoader/Nette/WebLoader.php b/WebLoader/Nette/WebLoader.php index 59f2931..3726a81 100644 --- a/WebLoader/Nette/WebLoader.php +++ b/WebLoader/Nette/WebLoader.php @@ -117,31 +117,30 @@ protected function getGeneratedFilePath($file) } /** - * Generate Subresource Integrity checksums for all hashing algorithms + * Generate Subresource Integrity checksums for all set hashing algorithms * - * @link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity * @param string $fileContent * @return string */ protected function getSriChecksums($fileContent) { - // TODO add to config webloader.css.default.sriHashingAlgorithm: [] - - $hashingAlgorithms = [ - 'sha256', - 'sha384', - 'sha512', - ]; - $checksums = []; - foreach ($hashingAlgorithms as $algorithm) { + foreach ($this->compiler->getSriHashingAlgorithms() as $algorithm) { $checksums[] = $this->getOneSriChecksum($algorithm, $fileContent); } return implode(' ', $checksums); } + /** + * Generate Subresource Integrity checksum + * + * @link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity + * @param string $hashingAlgorithm + * @param string $fileContent + * @return string + */ private function getOneSriChecksum($hashingAlgorithm, $fileContent) { $hash = hash($hashingAlgorithm, $fileContent, true); From 2ae7671549733cd6e165e40a17988ad9286f61ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Luke=C5=A1?= Date: Thu, 22 Dec 2016 13:17:03 +0100 Subject: [PATCH 3/5] WebLoader: added tests --- tests/Nette/WebLoaderTest.php | 139 ++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/Nette/WebLoaderTest.php diff --git a/tests/Nette/WebLoaderTest.php b/tests/Nette/WebLoaderTest.php new file mode 100644 index 0000000..771852c --- /dev/null +++ b/tests/Nette/WebLoaderTest.php @@ -0,0 +1,139 @@ + 'sha256-Ss8LOdnEdmcJo2ifVTrAGrVQVF/6RUTfwLLOqC+6AqM=', + self::HASHES_SHA384 => 'sha384-OZ7wmy2rB2wregDCOAvEmnrP7wUiSrCbaFEn6r86mq6oPm8oqDrZMRy2GnFPUyxm', + self::HASHES_SHA512 => 'sha512-xIr1p/bUqFH8ikNO7WOKsabvaOGdvK6JSsZ8n7xbywGCuOcSOz3zyeTct2kMIxA/A9wX9UNSBxzrKk6yBLJrkQ==', + ]; + + public function setUp() + { + @mkdir(self::TEMP_PATH); + copy( + self::SOURCE_FILE_DIR_PATH . '/' . self::SOURCE_FILE_NAME, + self::TEMP_PATH . '/' . self::SOURCE_FILE_NAME + ); + } + + /** + * @dataProvider provideTestGetSriChecksums + * @param $hashingAlgorithms + * @param $fileContent + * @param $expected + */ + public function testGetSriChecksums($hashingAlgorithms, $fileContent, $expected) + { + $compiler = $this->getCompiler($hashingAlgorithms); + $webloader = $this->getWebLoader($compiler); + $sriChecksumsResult = $webloader->getSriChecksumsResult($fileContent); + + $this->assertSame($expected, $sriChecksumsResult); + } + + public function provideTestGetSriChecksums() + { + return [ + [ + [], + self::HASHES_TEST_STRING, + '', + ], + [ + [ + self::HASHES_SHA256, + ], + self::HASHES_TEST_STRING, + $this->hashes[self::HASHES_SHA256], + ], + [ + [ + self::HASHES_SHA256, + self::HASHES_SHA512, + ], + self::HASHES_TEST_STRING, + implode(' ', [ + $this->hashes[self::HASHES_SHA256], + $this->hashes[self::HASHES_SHA512], + ]), + ], + ]; + } + + public function testGetCompiledFileContent() + { + $compiler = $this->getCompiler(); + $webloader = $this->getWebLoader($compiler); + $compiledFileContentResult = $webloader->getCompiledFileContentResult( + self::SOURCE_FILE_DIR_PATH . '/' . self::SOURCE_FILE_NAME + ); + $expected = file_get_contents(self::SOURCE_FILE_DIR_PATH . '/' . self::SOURCE_FILE_NAME); + + $this->assertSame($expected, $compiledFileContentResult); + } + + /** + * @param array $hashingAlgorithms + * @return Compiler + */ + private function getCompiler($hashingAlgorithms = []) + { + $files = new FileCollection(self::FILE_COLLECTION_ROOT_PATH); + $compiler = new Compiler($files, new DefaultOutputNamingConvention(), self::TEMP_PATH); + + foreach ($hashingAlgorithms as $alhorithm) { + $compiler->addSriHashingAlgorithm($alhorithm); + } + + return $compiler; + } + + /** + * @param Compiler $compiler + * @return WebLoader + */ + private function getWebLoader(Compiler $compiler) + { + return new class($compiler, self::TEMP_PATH) extends WebLoader + { + public function getCompiledFileContentResult($source) + { + return $this->getCompiledFileContent($source); + } + + public function getSriChecksumsResult($fileContent) + { + return $this->getSriChecksums($fileContent); + } + + public function getElement($source) + { + // not important now + } + }; + } +} From f9e2590d4a72e123fc5a5bc9def0744f3db07383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Luke=C5=A1?= Date: Thu, 22 Dec 2016 14:30:24 +0100 Subject: [PATCH 4/5] fixed wrong indentation in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6480fe0..ad238b2 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ webloader: watchFiles: # only watch modify file - {files: ["*.css", "*.less"], from: css} - {files: ["*.css", "*.less"], in: css} - sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified - - sha256 + sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified + - sha256 js: default: @@ -83,8 +83,8 @@ webloader: files: - %appDir%/../libs/nette/nette/client-side/netteForms.js - web.js - sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified - - sha256 + sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified + - sha256 ``` For older versions of Nette, you have to register the extension in `app/bootstrap.php`: From 5ede9eae72847c7184f976fd82d1d7f0d4e40f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Luke=C5=A1?= Date: Thu, 22 Dec 2016 14:56:33 +0100 Subject: [PATCH 5/5] WebLoaderTest: fixed PHP 5.4 compatibility --- tests/Nette/WebLoaderTest.php | 69 +++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/tests/Nette/WebLoaderTest.php b/tests/Nette/WebLoaderTest.php index 771852c..cc3bc0f 100644 --- a/tests/Nette/WebLoaderTest.php +++ b/tests/Nette/WebLoaderTest.php @@ -17,26 +17,30 @@ class WebLoaderTest extends \PHPUnit_Framework_TestCase const HASHES_TEST_STRING = 'testString'; - const FILE_COLLECTION_ROOT_PATH = __DIR__ . '/../fixtures'; - - const SOURCE_FILE_DIR_PATH = __DIR__ . '/../fixtures/dir'; - const SOURCE_FILE_NAME = 'one.css'; - const TEMP_PATH = __DIR__ . '/../temp'; - private $hashes = [ self::HASHES_SHA256 => 'sha256-Ss8LOdnEdmcJo2ifVTrAGrVQVF/6RUTfwLLOqC+6AqM=', self::HASHES_SHA384 => 'sha384-OZ7wmy2rB2wregDCOAvEmnrP7wUiSrCbaFEn6r86mq6oPm8oqDrZMRy2GnFPUyxm', self::HASHES_SHA512 => 'sha512-xIr1p/bUqFH8ikNO7WOKsabvaOGdvK6JSsZ8n7xbywGCuOcSOz3zyeTct2kMIxA/A9wX9UNSBxzrKk6yBLJrkQ==', ]; + private $fileCollectionRootPath; + + private $sourceFileDirPath; + + private $tempPath; + public function setUp() { - @mkdir(self::TEMP_PATH); + $this->fileCollectionRootPath = __DIR__ . '/../fixtures'; + $this->sourceFileDirPath = __DIR__ . '/../fixtures/dir'; + $this->tempPath = __DIR__ . '/../temp'; + + @mkdir($this->tempPath); copy( - self::SOURCE_FILE_DIR_PATH . '/' . self::SOURCE_FILE_NAME, - self::TEMP_PATH . '/' . self::SOURCE_FILE_NAME + $this->sourceFileDirPath . '/' . self::SOURCE_FILE_NAME, + $this->tempPath . '/' . self::SOURCE_FILE_NAME ); } @@ -89,9 +93,9 @@ public function testGetCompiledFileContent() $compiler = $this->getCompiler(); $webloader = $this->getWebLoader($compiler); $compiledFileContentResult = $webloader->getCompiledFileContentResult( - self::SOURCE_FILE_DIR_PATH . '/' . self::SOURCE_FILE_NAME + $this->sourceFileDirPath . '/' . self::SOURCE_FILE_NAME ); - $expected = file_get_contents(self::SOURCE_FILE_DIR_PATH . '/' . self::SOURCE_FILE_NAME); + $expected = file_get_contents($this->sourceFileDirPath . '/' . self::SOURCE_FILE_NAME); $this->assertSame($expected, $compiledFileContentResult); } @@ -102,8 +106,8 @@ public function testGetCompiledFileContent() */ private function getCompiler($hashingAlgorithms = []) { - $files = new FileCollection(self::FILE_COLLECTION_ROOT_PATH); - $compiler = new Compiler($files, new DefaultOutputNamingConvention(), self::TEMP_PATH); + $files = new FileCollection($this->fileCollectionRootPath); + $compiler = new Compiler($files, new DefaultOutputNamingConvention(), $this->tempPath); foreach ($hashingAlgorithms as $alhorithm) { $compiler->addSriHashingAlgorithm($alhorithm); @@ -114,26 +118,29 @@ private function getCompiler($hashingAlgorithms = []) /** * @param Compiler $compiler - * @return WebLoader + * @return WebLoaderTestImplementation */ private function getWebLoader(Compiler $compiler) { - return new class($compiler, self::TEMP_PATH) extends WebLoader - { - public function getCompiledFileContentResult($source) - { - return $this->getCompiledFileContent($source); - } - - public function getSriChecksumsResult($fileContent) - { - return $this->getSriChecksums($fileContent); - } - - public function getElement($source) - { - // not important now - } - }; + return new WebLoaderTestImplementation($compiler, $this->tempPath); + } +} + + +class WebLoaderTestImplementation extends WebLoader +{ + public function getCompiledFileContentResult($source) + { + return $this->getCompiledFileContent($source); + } + + public function getSriChecksumsResult($fileContent) + { + return $this->getSriChecksums($fileContent); + } + + public function getElement($source) + { + // not important now } }