Skip to content

Commit 65e9432

Browse files
alexander-schranzwouterj
authored andcommitted
Move UriSigner from HttpKernel to HttpFoundation package
1 parent 5ac0202 commit 65e9432

File tree

3 files changed

+198
-0
lines changed

3 files changed

+198
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Make `HeaderBag::getDate()`, `Response::getDate()`, `getExpires()` and `getLastModified()` return a `DateTimeImmutable`
88
* Support root-level `Generator` in `StreamedJsonResponse`
9+
* Add `UriSigner` from the HttpKernel component
910

1011
6.3
1112
---

Tests/UriSignerTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\UriSigner;
17+
18+
class UriSignerTest extends TestCase
19+
{
20+
public function testSign()
21+
{
22+
$signer = new UriSigner('foobar');
23+
24+
$this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo'));
25+
$this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo?foo=bar'));
26+
$this->assertStringContainsString('&foo=', $signer->sign('http://example.com/foo?foo=bar'));
27+
}
28+
29+
public function testCheck()
30+
{
31+
$signer = new UriSigner('foobar');
32+
33+
$this->assertFalse($signer->check('http://example.com/foo?_hash=foo'));
34+
$this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo'));
35+
$this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo&bar=foo'));
36+
37+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo')));
38+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar')));
39+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer')));
40+
41+
$this->assertSame($signer->sign('http://example.com/foo?foo=bar&bar=foo'), $signer->sign('http://example.com/foo?bar=foo&foo=bar'));
42+
}
43+
44+
public function testCheckWithDifferentArgSeparator()
45+
{
46+
$this->iniSet('arg_separator.output', '&amp;');
47+
$signer = new UriSigner('foobar');
48+
49+
$this->assertSame(
50+
'http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar',
51+
$signer->sign('http://example.com/foo?foo=bar&baz=bay')
52+
);
53+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay')));
54+
}
55+
56+
public function testCheckWithRequest()
57+
{
58+
$signer = new UriSigner('foobar');
59+
60+
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo'))));
61+
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar'))));
62+
$this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar&0=integer'))));
63+
}
64+
65+
public function testCheckWithDifferentParameter()
66+
{
67+
$signer = new UriSigner('foobar', 'qux');
68+
69+
$this->assertSame(
70+
'http://example.com/foo?baz=bay&foo=bar&qux=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D',
71+
$signer->sign('http://example.com/foo?foo=bar&baz=bay')
72+
);
73+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay')));
74+
}
75+
76+
public function testSignerWorksWithFragments()
77+
{
78+
$signer = new UriSigner('foobar');
79+
80+
$this->assertSame(
81+
'http://example.com/foo?_hash=EhpAUyEobiM3QTrKxoLOtQq5IsWyWedoXDPqIjzNj5o%3D&bar=foo&foo=bar#foobar',
82+
$signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar')
83+
);
84+
$this->assertTrue($signer->check($signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar')));
85+
}
86+
}

UriSigner.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* Signs URIs.
16+
*
17+
* @author Fabien Potencier <fabien@symfony.com>
18+
*/
19+
class UriSigner
20+
{
21+
private string $secret;
22+
private string $parameter;
23+
24+
/**
25+
* @param string $secret A secret
26+
* @param string $parameter Query string parameter to use
27+
*/
28+
public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash')
29+
{
30+
$this->secret = $secret;
31+
$this->parameter = $parameter;
32+
}
33+
34+
/**
35+
* Signs a URI.
36+
*
37+
* The given URI is signed by adding the query string parameter
38+
* which value depends on the URI and the secret.
39+
*/
40+
public function sign(string $uri): string
41+
{
42+
$url = parse_url($uri);
43+
$params = [];
44+
45+
if (isset($url['query'])) {
46+
parse_str($url['query'], $params);
47+
}
48+
49+
$uri = $this->buildUrl($url, $params);
50+
$params[$this->parameter] = $this->computeHash($uri);
51+
52+
return $this->buildUrl($url, $params);
53+
}
54+
55+
/**
56+
* Checks that a URI contains the correct hash.
57+
*/
58+
public function check(string $uri): bool
59+
{
60+
$url = parse_url($uri);
61+
$params = [];
62+
63+
if (isset($url['query'])) {
64+
parse_str($url['query'], $params);
65+
}
66+
67+
if (empty($params[$this->parameter])) {
68+
return false;
69+
}
70+
71+
$hash = $params[$this->parameter];
72+
unset($params[$this->parameter]);
73+
74+
return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash);
75+
}
76+
77+
public function checkRequest(Request $request): bool
78+
{
79+
$qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : '';
80+
81+
// we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering)
82+
return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs);
83+
}
84+
85+
private function computeHash(string $uri): string
86+
{
87+
return base64_encode(hash_hmac('sha256', $uri, $this->secret, true));
88+
}
89+
90+
private function buildUrl(array $url, array $params = []): string
91+
{
92+
ksort($params, \SORT_STRING);
93+
$url['query'] = http_build_query($params, '', '&');
94+
95+
$scheme = isset($url['scheme']) ? $url['scheme'].'://' : '';
96+
$host = $url['host'] ?? '';
97+
$port = isset($url['port']) ? ':'.$url['port'] : '';
98+
$user = $url['user'] ?? '';
99+
$pass = isset($url['pass']) ? ':'.$url['pass'] : '';
100+
$pass = ($user || $pass) ? "$pass@" : '';
101+
$path = $url['path'] ?? '';
102+
$query = $url['query'] ? '?'.$url['query'] : '';
103+
$fragment = isset($url['fragment']) ? '#'.$url['fragment'] : '';
104+
105+
return $scheme.$user.$pass.$host.$port.$path.$query.$fragment;
106+
}
107+
}
108+
109+
if (!class_exists(\Symfony\Component\HttpKernel\UriSigner::class, false)) {
110+
class_alias(UriSigner::class, \Symfony\Component\HttpKernel\UriSigner::class);
111+
}

0 commit comments

Comments
 (0)