Skip to content

Commit

Permalink
Relative file hashes, and hash salt (#19)
Browse files Browse the repository at this point in the history
* Relative file hashes, and hash salt

* Correct test hash

* Test for relative hash

* Do not use components from outside of the root directory
  • Loading branch information
bajb authored and TomK committed Apr 9, 2019
1 parent 5d71ae7 commit 4e231fb
Show file tree
Hide file tree
Showing 18 changed files with 148 additions and 52 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"autoload-dev": {
"psr-4": {
"Packaged\\Dispatch\\Tests\\TestComponents\\": "tests/_root/src/TestComponents",
"Packaged\\Dispatch\\Tests\\": "tests"
}
}
Expand Down
54 changes: 53 additions & 1 deletion src/Dispatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Dispatch
protected $_resourceStore;

protected $_baseUri;
protected $_requireFileHash = false;

const RESOURCES_DIR = 'resources';
const VENDOR_DIR = 'vendor';
Expand Down Expand Up @@ -52,6 +53,7 @@ public static function destroy()
* @var ClassLoader
*/
protected $_classLoader;
protected $_hashSalt = 'dispatch';

public function __construct($projectRoot, $baseUri = null, ClassLoader $loader = null)
{
Expand All @@ -61,6 +63,37 @@ public function __construct($projectRoot, $baseUri = null, ClassLoader $loader =
$this->_classLoader = $loader;
}

/**
* Add salt to dispatch hashes, for additional resource security
*
* @param string $hashSalt
*
* @return $this
*/
public function setHashSalt(string $hashSalt)
{
$this->_hashSalt = $hashSalt;
return $this;
}

/**
* Generate a hash against specific content, for a desired length
*
* @param $content
* @param null $length
*
* @return string
*/
public function generateHash($content, $length = null)
{
$hash = md5($content . $this->_hashSalt);
if($length !== null)
{
return substr($hash, 0, $length);
}
return $hash;
}

public function getResourcesPath()
{
return Path::system($this->_projectRoot, self::RESOURCES_DIR);
Expand Down Expand Up @@ -171,7 +204,21 @@ public function handleRequest(Request $request): Response

$requestPath = Path::custom('/', $pathParts);
$fullPath = $manager->getFilePath($requestPath);
if($compareHash !== $manager->getFileHash($fullPath))

[$fileHash, $relativeHash] = str_split($compareHash . ' ', 8);
$relativeHash = trim($relativeHash);
$failedHash = true;
if(!$this->_requireFileHash && $relativeHash && $relativeHash === $manager->getRelativeHash($fullPath))
{
$failedHash = false;
}

if((!$relativeHash || $failedHash) && $fileHash === $manager->getFileHash($fullPath))
{
$failedHash = false;
}

if($failedHash)
{
return Response::create("File Not Found", 404);
}
Expand Down Expand Up @@ -229,4 +276,9 @@ public function store()
return $this->_resourceStore;
}

public function calculateRelativePath($filePath)
{
return ltrim(str_replace($this->_projectRoot, '', $filePath), '/\\');
}

}
53 changes: 30 additions & 23 deletions src/ResourceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,48 +55,48 @@ public function getMapOptions()
return $this->_mapOptions;
}

public static function vendor($vendor, $package)
public static function vendor($vendor, $package, $options = [])
{
return new static(self::MAP_VENDOR, [$vendor, $package]);
return new static(self::MAP_VENDOR, [$vendor, $package], $options);
}

public static function alias($alias)
public static function alias($alias, $options = [])
{
return new static(self::MAP_ALIAS, [$alias]);
return new static(self::MAP_ALIAS, [$alias], $options);
}

public static function resources()
public static function resources($options = [])
{
return new static(self::MAP_RESOURCES, []);
return new static(self::MAP_RESOURCES, [], $options);
}

public static function public()
public static function public($options = [])
{
return new static(self::MAP_PUBLIC, []);
return new static(self::MAP_PUBLIC, [], $options);
}

public static function inline()
public static function inline($options = [])
{
return new static(self::MAP_INLINE, []);
return new static(self::MAP_INLINE, [], $options);
}

public static function external()
public static function external($options = [])
{
return new static(self::MAP_EXTERNAL, []);
return new static(self::MAP_EXTERNAL, [], $options);
}

public static function component(DispatchableComponent $component)
public static function component(DispatchableComponent $component, $options = [])
{
$fullClass = $component instanceof FixedClassComponent ? $component->getComponentClass() : get_class($component);
return static::_componentManager($fullClass, Dispatch::instance());
return static::_componentManager($fullClass, Dispatch::instance(), $options);
}

public static function componentClass(string $componentClassName)
public static function componentClass(string $componentClassName, $options = [])
{
return static::_componentManager($componentClassName, Dispatch::instance());
return static::_componentManager($componentClassName, Dispatch::instance(), $options);
}

protected static function _componentManager($fullClass, Dispatch $dispatch = null): ResourceManager
protected static function _componentManager($fullClass, Dispatch $dispatch = null, $options = []): ResourceManager
{
$class = ltrim($fullClass, '\\');
if($dispatch)
Expand All @@ -119,7 +119,7 @@ protected static function _componentManager($fullClass, Dispatch $dispatch = nul
$parts = explode('\\', $class);
array_unshift($parts, count($parts));

$manager = new static(self::MAP_COMPONENT, $parts);
$manager = new static(self::MAP_COMPONENT, $parts, $options);
$manager->_componentPath = $dispatch->componentClassResourcePath($fullClass);
return $manager;
}
Expand All @@ -136,17 +136,25 @@ public function getResourceUri($relativeFullPath): ?string
{
return $relativeFullPath;
}
$hash = $this->getFileHash($this->getFilePath($relativeFullPath));

$filePath = $this->getFilePath($relativeFullPath);
$relHash = $this->getRelativeHash($filePath);
$hash = $this->getFileHash($filePath);
if(!$hash)
{
return null;
}
return Path::custom(
'/',
array_merge([Dispatch::instance()->getBaseUri()], $this->_baseUri, [$hash, $relativeFullPath])
array_merge([Dispatch::instance()->getBaseUri()], $this->_baseUri, [$hash . $relHash, $relativeFullPath])
);
}

public function getRelativeHash($filePath)
{
return Dispatch::instance()->generateHash(Dispatch::instance()->calculateRelativePath($filePath), 4);
}

/**
* @param $relativePath
*
Expand Down Expand Up @@ -201,8 +209,7 @@ public function getFileHash($fullPath)
}
}

$hash = substr(md5_file($fullPath), 0, 8);

$hash = Dispatch::instance()->generateHash(md5_file($fullPath), 8);
if($hash && function_exists('apcu_store'))
{
apcu_store($key, $hash, 86400);
Expand Down Expand Up @@ -310,7 +317,7 @@ public function includeCss($toRequire, $options = null)
{
try
{
return $this->requireCss($toRequire, $options = null);
return $this->requireCss($toRequire, $options);
}
catch(Exception $e)
{
Expand Down
49 changes: 33 additions & 16 deletions tests/DispatchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,27 @@ public function testHandle()
$response = $dispatch->handleRequest($request);
$this->assertEquals(404, $response->getStatusCode());

$request = Request::create('/r/f643eb32/css/test.css');
$request = Request::create('/r/e69b7aabcde/css/test.css');
$response = $dispatch->handleRequest($request);
$this->assertEquals(404, $response->getStatusCode());

$request = Request::create('/r/bd04a611ed6d/css/test.css');
$response = $dispatch->handleRequest($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertContains('url(r/d68e763c/img/x.jpg)', $response->getContent());
$this->assertContains('url(r/395d1a0e8999/img/x.jpg)', $response->getContent());

$request = Request::create('/p/d5dd9dc7/css/placeholder.css');
$uri = ResourceManager::public()->getResourceUri('css/placeholder.css');
$request = Request::create($uri);
$response = $dispatch->handleRequest($request);
$this->assertContains('font-size:14px', $response->getContent());

$dispatch->addAlias('abc', 'resources/css');
$request = Request::create('/a/abc/f643eb32/test.css');
$request = Request::create('/a/abc/bd04a611ed6d/test.css');
$response = $dispatch->handleRequest($request);
$this->assertContains('url("a/abc/d41d8cd9/sub/subimg.jpg")', $response->getContent());
$this->assertContains('url("a/abc/942e325be95f/sub/subimg.jpg")', $response->getContent());

$request = Request::create('/v/packaged/dispatch/6673b7e0/css/vendor.css');
$uri = ResourceManager::vendor('packaged', 'dispatch')->getResourceUri('css/vendor.css');
$request = Request::create($uri);
$response = $dispatch->handleRequest($request);
$this->assertContains('body{background:orange}', $response->getContent());

Expand All @@ -73,9 +79,9 @@ public function testBaseUri()
{
$dispatch = new Dispatch(Path::system(__DIR__, '_root'), 'http://assets.packaged.in');
Dispatch::bind($dispatch);
$request = Request::create('/r/f643eb32/css/test.css');
$request = Request::create('/r/bd04a611ed6d/css/test.css');
$response = $dispatch->handleRequest($request);
$this->assertContains('url(http://assets.packaged.in/r/d68e763c/img/x.jpg)', $response->getContent());
$this->assertContains('url(http://assets.packaged.in/r/395d1a0e8999/img/x.jpg)', $response->getContent());
Dispatch::destroy();
}

Expand All @@ -85,10 +91,10 @@ public function testStore()
ResourceManager::resources()->requireCss('css/test.css');
ResourceManager::resources()->requireCss('css/do-not-modify.css');
$response = Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_CSS);
$this->assertContains('href="http://assets.packaged.in/r/f643eb32/css/test.css"', $response);
$this->assertContains('href="http://assets.packaged.in/r/bd04a6113c11/css/test.css"', $response);
ResourceManager::resources()->requireJs('js/alert.js');
$response = Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_JS);
$this->assertContains('src="http://assets.packaged.in/r/ef6402a7/js/alert.js"', $response);
$this->assertContains('src="http://assets.packaged.in/r/f417133ec50f/js/alert.js"', $response);
Dispatch::destroy();
}

Expand All @@ -104,7 +110,7 @@ public function testComponent()
Dispatch::instance()->addComponentAlias('\Packaged\Dispatch\Tests\TestComponents', '');
$manager = ResourceManager::component($component);
$uri = $manager->getResourceUri('style.css');
$this->assertEquals('c/3/_/DemoComponent/DemoComponent/a4197ed8/style.css', $uri);
$this->assertEquals('c/3/_/DemoComponent/DemoComponent/1a9ffb748d31/style.css', $uri);

$request = Request::create('/' . $uri);
$response = $dispatch->handleRequest($request);
Expand All @@ -126,7 +132,7 @@ public function testComponent()
Dispatch::instance()->addComponentAlias('\Packaged\Dispatch\Tests\TestComponents\DemoComponents', 'DCRC');
$manager = ResourceManager::component(new DemoComponent());
$uri = $manager->getResourceUri('style.css');
$this->assertEquals('c/2/_DC/DemoComponent/a4197ed8/style.css', $uri);
$this->assertEquals('c/2/_DC/DemoComponent/1a9ffb748d31/style.css', $uri);

$request = Request::create('/c/3/_/MissingComponent/DemoComponent/a4197ed8/style.css');
$response = $dispatch->handleRequest($request);
Expand All @@ -135,13 +141,13 @@ public function testComponent()

$manager = ResourceManager::component(new ChildComponent());
$uri = $manager->getResourceUri('style.css');
$this->assertEquals('c/2/_/AbstractComponent/d456522a/style.css', $uri);
$this->assertEquals('c/2/_/AbstractComponent/162fe246c68b/style.css', $uri);

$request = Request::create('/' . $uri);
$response = $dispatch->handleRequest($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(
'@import "c/2/_/AbstractComponent/d41d8cd9/dependency.css";body{color:blue;background:url("c/2/_/AbstractComponent/d68e763c/img/x.jpg")}',
'@import "c/2/_/AbstractComponent/942e325b3dcc/dependency.css";body{color:blue;background:url("c/2/_/AbstractComponent/395d1a0e845f/img/x.jpg")}',
$response->getContent()
);
}
Expand All @@ -154,14 +160,25 @@ public function testImport()
Dispatch::instance()->addComponentAlias('\Packaged\Dispatch\Tests\TestComponents\AbstractComponent', '');
$manager = ResourceManager::component(new ChildComponent());
$uri = $manager->getResourceUri('import.js');
$this->assertEquals('c/1/_/e60f9588/import.js', $uri);
$this->assertEquals('c/1/_/831aff315092/import.js', $uri);

$request = Request::create($uri);
$response = $dispatch->handleRequest($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(
'import"c/1/_/d41d8cd9/dependency.css";import"c/1/_/d41d8cd9/dependency.js";import"c/1/_/d41d8cd9/dependency.js";',
'import"c/1/_/942e325b3dcc/dependency.css";import"c/1/_/942e325b4521/dependency.js";import"c/1/_/942e325b4521/dependency.js";',
$response->getContent()
);
}

public function testHashing()
{
$dispatch = new Dispatch(Path::system(__DIR__, '_root'));
Dispatch::bind($dispatch);
$uri = ResourceManager::public()->getResourceUri('css/placeholder.css');
$this->assertEquals($uri, ResourceManager::public()->getResourceUri('css/placeholder.css'));
$dispatch->setHashSalt('abc');
$this->assertNotEquals($uri, ResourceManager::public()->getResourceUri('css/placeholder.css'));
$this->assertEquals(substr($dispatch->generateHash('abc'), 0, 8), $dispatch->generateHash('abc', 8));
}
}
24 changes: 19 additions & 5 deletions tests/ResourceManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ public function testComponent()
$manager->getMapOptions()
);
$this->assertEquals(
'c/6/Packaged/Dispatch/Tests/TestComponents/DemoComponent/DemoComponent/a4197ed8/style.css',
'c/6/Packaged/Dispatch/Tests/TestComponents/DemoComponent/DemoComponent/1a9ffb748d31/style.css',
$manager->getResourceUri('style.css')
);
Dispatch::instance()->addComponentAlias('\Packaged\Dispatch\Tests\TestComponents', '');
$manager = ResourceManager::component($component);
$this->assertEquals(
'c/3/_/DemoComponent/DemoComponent/a4197ed8/style.css',
'c/3/_/DemoComponent/DemoComponent/1a9ffb748d31/style.css',
$manager->getResourceUri('style.css')
);
}
Expand All @@ -68,7 +68,7 @@ public function testRequireJs()
Dispatch::bind(new Dispatch(Path::system(__DIR__, '_root')));
ResourceManager::resources()->requireJs('js/alert.js');
$this->assertContains(
'src="r/ef6402a7/js/alert.js"',
'src="r/f417133ec50f/js/alert.js"',
Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_JS)
);
}
Expand All @@ -91,7 +91,7 @@ public function testRequireCss()
ResourceManager::resources()->includeCss('css/test.css');
ResourceManager::resources()->requireCss('css/test.css');
$this->assertContains(
'href="r/f643eb32/css/test.css"',
'href="r/bd04a6113c11/css/test.css"',
Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_CSS)
);
}
Expand Down Expand Up @@ -149,7 +149,7 @@ public function testGetFileHash()
for($i = 0; $i < 3; $i++)
{
$this->assertEquals(
"7c20a3fa",
"d91424cc",
ResourceManager::resources()->getFileHash(Path::system(__DIR__, '_root', 'public', 'placeholder.html'))
);
}
Expand All @@ -164,4 +164,18 @@ public function testRequireInlineCss()
Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_CSS)
);
}

public function testRelativeHash()
{
Dispatch::bind(new Dispatch(Path::system(__DIR__, '_root')));
$pathHash = Dispatch::instance()->generateHash('resources/css/test.css', 4);
$manager = ResourceManager::resources();
$resourceHash = $manager->getRelativeHash($manager->getFilePath('css/test.css'));
$this->assertEquals($pathHash, $resourceHash);

$pathHash = Dispatch::instance()->generateHash('public/favicon.ico', 4);
$manager = ResourceManager::public();
$resourceHash = $manager->getRelativeHash($manager->getFilePath('favicon.ico'));
$this->assertEquals($pathHash, $resourceHash);
}
}
Loading

0 comments on commit 4e231fb

Please sign in to comment.