Skip to content

Commit 4e231fb

Browse files
bajbTomK
authored andcommitted
Relative file hashes, and hash salt (#19)
* Relative file hashes, and hash salt * Correct test hash * Test for relative hash * Do not use components from outside of the root directory
1 parent 5d71ae7 commit 4e231fb

File tree

18 files changed

+148
-52
lines changed

18 files changed

+148
-52
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
},
2929
"autoload-dev": {
3030
"psr-4": {
31+
"Packaged\\Dispatch\\Tests\\TestComponents\\": "tests/_root/src/TestComponents",
3132
"Packaged\\Dispatch\\Tests\\": "tests"
3233
}
3334
}

src/Dispatch.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class Dispatch
2424
protected $_resourceStore;
2525

2626
protected $_baseUri;
27+
protected $_requireFileHash = false;
2728

2829
const RESOURCES_DIR = 'resources';
2930
const VENDOR_DIR = 'vendor';
@@ -52,6 +53,7 @@ public static function destroy()
5253
* @var ClassLoader
5354
*/
5455
protected $_classLoader;
56+
protected $_hashSalt = 'dispatch';
5557

5658
public function __construct($projectRoot, $baseUri = null, ClassLoader $loader = null)
5759
{
@@ -61,6 +63,37 @@ public function __construct($projectRoot, $baseUri = null, ClassLoader $loader =
6163
$this->_classLoader = $loader;
6264
}
6365

66+
/**
67+
* Add salt to dispatch hashes, for additional resource security
68+
*
69+
* @param string $hashSalt
70+
*
71+
* @return $this
72+
*/
73+
public function setHashSalt(string $hashSalt)
74+
{
75+
$this->_hashSalt = $hashSalt;
76+
return $this;
77+
}
78+
79+
/**
80+
* Generate a hash against specific content, for a desired length
81+
*
82+
* @param $content
83+
* @param null $length
84+
*
85+
* @return string
86+
*/
87+
public function generateHash($content, $length = null)
88+
{
89+
$hash = md5($content . $this->_hashSalt);
90+
if($length !== null)
91+
{
92+
return substr($hash, 0, $length);
93+
}
94+
return $hash;
95+
}
96+
6497
public function getResourcesPath()
6598
{
6699
return Path::system($this->_projectRoot, self::RESOURCES_DIR);
@@ -171,7 +204,21 @@ public function handleRequest(Request $request): Response
171204

172205
$requestPath = Path::custom('/', $pathParts);
173206
$fullPath = $manager->getFilePath($requestPath);
174-
if($compareHash !== $manager->getFileHash($fullPath))
207+
208+
[$fileHash, $relativeHash] = str_split($compareHash . ' ', 8);
209+
$relativeHash = trim($relativeHash);
210+
$failedHash = true;
211+
if(!$this->_requireFileHash && $relativeHash && $relativeHash === $manager->getRelativeHash($fullPath))
212+
{
213+
$failedHash = false;
214+
}
215+
216+
if((!$relativeHash || $failedHash) && $fileHash === $manager->getFileHash($fullPath))
217+
{
218+
$failedHash = false;
219+
}
220+
221+
if($failedHash)
175222
{
176223
return Response::create("File Not Found", 404);
177224
}
@@ -229,4 +276,9 @@ public function store()
229276
return $this->_resourceStore;
230277
}
231278

279+
public function calculateRelativePath($filePath)
280+
{
281+
return ltrim(str_replace($this->_projectRoot, '', $filePath), '/\\');
282+
}
283+
232284
}

src/ResourceManager.php

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,48 +55,48 @@ public function getMapOptions()
5555
return $this->_mapOptions;
5656
}
5757

58-
public static function vendor($vendor, $package)
58+
public static function vendor($vendor, $package, $options = [])
5959
{
60-
return new static(self::MAP_VENDOR, [$vendor, $package]);
60+
return new static(self::MAP_VENDOR, [$vendor, $package], $options);
6161
}
6262

63-
public static function alias($alias)
63+
public static function alias($alias, $options = [])
6464
{
65-
return new static(self::MAP_ALIAS, [$alias]);
65+
return new static(self::MAP_ALIAS, [$alias], $options);
6666
}
6767

68-
public static function resources()
68+
public static function resources($options = [])
6969
{
70-
return new static(self::MAP_RESOURCES, []);
70+
return new static(self::MAP_RESOURCES, [], $options);
7171
}
7272

73-
public static function public()
73+
public static function public($options = [])
7474
{
75-
return new static(self::MAP_PUBLIC, []);
75+
return new static(self::MAP_PUBLIC, [], $options);
7676
}
7777

78-
public static function inline()
78+
public static function inline($options = [])
7979
{
80-
return new static(self::MAP_INLINE, []);
80+
return new static(self::MAP_INLINE, [], $options);
8181
}
8282

83-
public static function external()
83+
public static function external($options = [])
8484
{
85-
return new static(self::MAP_EXTERNAL, []);
85+
return new static(self::MAP_EXTERNAL, [], $options);
8686
}
8787

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

94-
public static function componentClass(string $componentClassName)
94+
public static function componentClass(string $componentClassName, $options = [])
9595
{
96-
return static::_componentManager($componentClassName, Dispatch::instance());
96+
return static::_componentManager($componentClassName, Dispatch::instance(), $options);
9797
}
9898

99-
protected static function _componentManager($fullClass, Dispatch $dispatch = null): ResourceManager
99+
protected static function _componentManager($fullClass, Dispatch $dispatch = null, $options = []): ResourceManager
100100
{
101101
$class = ltrim($fullClass, '\\');
102102
if($dispatch)
@@ -119,7 +119,7 @@ protected static function _componentManager($fullClass, Dispatch $dispatch = nul
119119
$parts = explode('\\', $class);
120120
array_unshift($parts, count($parts));
121121

122-
$manager = new static(self::MAP_COMPONENT, $parts);
122+
$manager = new static(self::MAP_COMPONENT, $parts, $options);
123123
$manager->_componentPath = $dispatch->componentClassResourcePath($fullClass);
124124
return $manager;
125125
}
@@ -136,17 +136,25 @@ public function getResourceUri($relativeFullPath): ?string
136136
{
137137
return $relativeFullPath;
138138
}
139-
$hash = $this->getFileHash($this->getFilePath($relativeFullPath));
139+
140+
$filePath = $this->getFilePath($relativeFullPath);
141+
$relHash = $this->getRelativeHash($filePath);
142+
$hash = $this->getFileHash($filePath);
140143
if(!$hash)
141144
{
142145
return null;
143146
}
144147
return Path::custom(
145148
'/',
146-
array_merge([Dispatch::instance()->getBaseUri()], $this->_baseUri, [$hash, $relativeFullPath])
149+
array_merge([Dispatch::instance()->getBaseUri()], $this->_baseUri, [$hash . $relHash, $relativeFullPath])
147150
);
148151
}
149152

153+
public function getRelativeHash($filePath)
154+
{
155+
return Dispatch::instance()->generateHash(Dispatch::instance()->calculateRelativePath($filePath), 4);
156+
}
157+
150158
/**
151159
* @param $relativePath
152160
*
@@ -201,8 +209,7 @@ public function getFileHash($fullPath)
201209
}
202210
}
203211

204-
$hash = substr(md5_file($fullPath), 0, 8);
205-
212+
$hash = Dispatch::instance()->generateHash(md5_file($fullPath), 8);
206213
if($hash && function_exists('apcu_store'))
207214
{
208215
apcu_store($key, $hash, 86400);
@@ -310,7 +317,7 @@ public function includeCss($toRequire, $options = null)
310317
{
311318
try
312319
{
313-
return $this->requireCss($toRequire, $options = null);
320+
return $this->requireCss($toRequire, $options);
314321
}
315322
catch(Exception $e)
316323
{

tests/DispatchTest.php

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,27 @@ public function testHandle()
4848
$response = $dispatch->handleRequest($request);
4949
$this->assertEquals(404, $response->getStatusCode());
5050

51-
$request = Request::create('/r/f643eb32/css/test.css');
51+
$request = Request::create('/r/e69b7aabcde/css/test.css');
52+
$response = $dispatch->handleRequest($request);
53+
$this->assertEquals(404, $response->getStatusCode());
54+
55+
$request = Request::create('/r/bd04a611ed6d/css/test.css');
5256
$response = $dispatch->handleRequest($request);
5357
$this->assertEquals(200, $response->getStatusCode());
54-
$this->assertContains('url(r/d68e763c/img/x.jpg)', $response->getContent());
58+
$this->assertContains('url(r/395d1a0e8999/img/x.jpg)', $response->getContent());
5559

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

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

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

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

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

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

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

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

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

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

159165
$request = Request::create($uri);
160166
$response = $dispatch->handleRequest($request);
161167
$this->assertEquals(200, $response->getStatusCode());
162168
$this->assertEquals(
163-
'import"c/1/_/d41d8cd9/dependency.css";import"c/1/_/d41d8cd9/dependency.js";import"c/1/_/d41d8cd9/dependency.js";',
169+
'import"c/1/_/942e325b3dcc/dependency.css";import"c/1/_/942e325b4521/dependency.js";import"c/1/_/942e325b4521/dependency.js";',
164170
$response->getContent()
165171
);
166172
}
173+
174+
public function testHashing()
175+
{
176+
$dispatch = new Dispatch(Path::system(__DIR__, '_root'));
177+
Dispatch::bind($dispatch);
178+
$uri = ResourceManager::public()->getResourceUri('css/placeholder.css');
179+
$this->assertEquals($uri, ResourceManager::public()->getResourceUri('css/placeholder.css'));
180+
$dispatch->setHashSalt('abc');
181+
$this->assertNotEquals($uri, ResourceManager::public()->getResourceUri('css/placeholder.css'));
182+
$this->assertEquals(substr($dispatch->generateHash('abc'), 0, 8), $dispatch->generateHash('abc', 8));
183+
}
167184
}

tests/ResourceManagerTest.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ public function testComponent()
5252
$manager->getMapOptions()
5353
);
5454
$this->assertEquals(
55-
'c/6/Packaged/Dispatch/Tests/TestComponents/DemoComponent/DemoComponent/a4197ed8/style.css',
55+
'c/6/Packaged/Dispatch/Tests/TestComponents/DemoComponent/DemoComponent/1a9ffb748d31/style.css',
5656
$manager->getResourceUri('style.css')
5757
);
5858
Dispatch::instance()->addComponentAlias('\Packaged\Dispatch\Tests\TestComponents', '');
5959
$manager = ResourceManager::component($component);
6060
$this->assertEquals(
61-
'c/3/_/DemoComponent/DemoComponent/a4197ed8/style.css',
61+
'c/3/_/DemoComponent/DemoComponent/1a9ffb748d31/style.css',
6262
$manager->getResourceUri('style.css')
6363
);
6464
}
@@ -68,7 +68,7 @@ public function testRequireJs()
6868
Dispatch::bind(new Dispatch(Path::system(__DIR__, '_root')));
6969
ResourceManager::resources()->requireJs('js/alert.js');
7070
$this->assertContains(
71-
'src="r/ef6402a7/js/alert.js"',
71+
'src="r/f417133ec50f/js/alert.js"',
7272
Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_JS)
7373
);
7474
}
@@ -91,7 +91,7 @@ public function testRequireCss()
9191
ResourceManager::resources()->includeCss('css/test.css');
9292
ResourceManager::resources()->requireCss('css/test.css');
9393
$this->assertContains(
94-
'href="r/f643eb32/css/test.css"',
94+
'href="r/bd04a6113c11/css/test.css"',
9595
Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_CSS)
9696
);
9797
}
@@ -149,7 +149,7 @@ public function testGetFileHash()
149149
for($i = 0; $i < 3; $i++)
150150
{
151151
$this->assertEquals(
152-
"7c20a3fa",
152+
"d91424cc",
153153
ResourceManager::resources()->getFileHash(Path::system(__DIR__, '_root', 'public', 'placeholder.html'))
154154
);
155155
}
@@ -164,4 +164,18 @@ public function testRequireInlineCss()
164164
Dispatch::instance()->store()->generateHtmlIncludes(ResourceStore::TYPE_CSS)
165165
);
166166
}
167+
168+
public function testRelativeHash()
169+
{
170+
Dispatch::bind(new Dispatch(Path::system(__DIR__, '_root')));
171+
$pathHash = Dispatch::instance()->generateHash('resources/css/test.css', 4);
172+
$manager = ResourceManager::resources();
173+
$resourceHash = $manager->getRelativeHash($manager->getFilePath('css/test.css'));
174+
$this->assertEquals($pathHash, $resourceHash);
175+
176+
$pathHash = Dispatch::instance()->generateHash('public/favicon.ico', 4);
177+
$manager = ResourceManager::public();
178+
$resourceHash = $manager->getRelativeHash($manager->getFilePath('favicon.ico'));
179+
$this->assertEquals($pathHash, $resourceHash);
180+
}
167181
}

0 commit comments

Comments
 (0)