Skip to content

Commit 1a73539

Browse files
DmytroVyshnevsky1Dmytro Vyshnevskyi
andauthored
fix query params sorting to build correct canonical query string (#1938)
* fix sorting * cs fix + change log * fix change log * fix for psalm * add tests * fix deprecations --------- Co-authored-by: Dmytro Vyshnevskyi <[email protected]>
1 parent 00b69a0 commit 1a73539

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## NOT RELEASED
44

5+
### Fixed
6+
7+
- SignerV4: fix sort of query parameters to build correct canoncal query string
8+
59
## 1.27.0
610

711
### Added

phpunit.xml.dist

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4-
backupGlobals="false"
5-
colors="true"
6-
bootstrap="vendor/autoload.php"
7-
failOnRisky="true"
8-
failOnWarning="true"
9-
>
10-
<coverage>
11-
<include>
12-
<directory>./src</directory>
13-
</include>
14-
</coverage>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="vendor/autoload.php" failOnRisky="true" failOnWarning="true">
153
<php>
164
<ini name="error_reporting" value="-1"/>
175
</php>
@@ -20,4 +8,9 @@
208
<directory>./tests/</directory>
219
</testsuite>
2210
</testsuites>
11+
<source>
12+
<include>
13+
<directory>./src</directory>
14+
</include>
15+
</source>
2316
</phpunit>

src/Signer/SignerV4.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,9 @@ private function buildCanonicalQuery(Request $request): string
321321
return '';
322322
}
323323

324-
ksort($query);
324+
uksort($query, static function (string $a, string $b): int {
325+
return strcmp(rawurlencode($a), rawurlencode($b));
326+
});
325327
$encodedQuery = [];
326328
foreach ($query as $key => $values) {
327329
if (!\is_array($values)) {

tests/Unit/Signer/SignerV4Test.php

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function testSignsRequests($rawRequest, $rawExpected)
7878
self::assertEquals($expected, $request);
7979
}
8080

81-
public function provideRequests()
81+
public static function provideRequests()
8282
{
8383
return [
8484
// POST headers should be signed.
@@ -140,7 +140,58 @@ public function provideRequests()
140140
];
141141
}
142142

143-
private function parseRequest(string $req): Request
143+
/**
144+
* @dataProvider provideRequestsWithQueryParams
145+
*/
146+
public function testSignsRequestsWithArrayInQueryParams($rawRequestWithoutQuery, $rawExpected, $queryParams)
147+
{
148+
$request = $this->parseRequest($rawRequestWithoutQuery, $queryParams);
149+
150+
$signer = new SignerV4('host', 'us-east-1');
151+
$context = new RequestContext(['currentDate' => new \DateTimeImmutable('20110909T233600Z')]);
152+
$credentials = new Credentials('AKIDEXAMPLE', 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY');
153+
154+
$signer->sign($request, $credentials, $context);
155+
156+
$expected = $this->parseRequest($rawExpected, $queryParams);
157+
158+
self::assertEquals($expected, $request);
159+
}
160+
161+
public static function provideRequestsWithQueryParams()
162+
{
163+
return [
164+
// GET Case with array in query params
165+
[
166+
"GET / HTTP/1.1\r\nHost: host.foo.com:443\r\n\r\n",
167+
"GET / HTTP/1.1\r\nHost: host.foo.com:443\r\nX-Amz-Date: 20110909T233600Z\r\nAuthorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=host;x-amz-date, Signature=7bd9c6fed0473be1f7ea96ff34c7d79e71a92932054a24b4210b3a56e4ab46d3\r\n\r\n",
168+
[
169+
'foos[0]' => '834127',
170+
'foos[1]' => '59',
171+
'foos[2]' => '90123',
172+
'foos[3]' => '4708',
173+
'foos[4]' => '120001',
174+
'foos[5]' => '333',
175+
'foos[6]' => '78005',
176+
'foos[7]' => '2',
177+
'foos[8]' => 'string value',
178+
'foos[9]' => '40617',
179+
'foos[10]' => '715',
180+
],
181+
],
182+
// GET Simple case with query params (copy of one of the cases above from testSignsRequests)
183+
[
184+
"GET / HTTP/1.1\r\nHost: host.foo.com:443\r\n\r\n",
185+
"GET / HTTP/1.1\r\nHost: host.foo.com:443\r\nX-Amz-Date: 20110909T233600Z\r\nAuthorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=host;x-amz-date, Signature=1c3274381ae12d8817336268d7da17672bd57e7348e39b7b9c567280f73742af\r\n\r\n",
186+
[
187+
'a' => 'foo',
188+
'b' => 'foo',
189+
],
190+
],
191+
];
192+
}
193+
194+
private function parseRequest(string $req, array $queryParams = []): Request
144195
{
145196
$lines = explode("\r\n", $req);
146197
[$method, $path] = explode(' ', array_shift($lines));
@@ -156,6 +207,12 @@ private function parseRequest(string $req): Request
156207

157208
$req = new Request($method, '/', [], $headers, StringStream::create(implode("\n", $lines)));
158209
$req->setEndpoint('https://' . $headers['Host'] . $path);
210+
211+
// currently the library cannot properly parse query params if they contain arrays, so we need to set them manually
212+
foreach ($queryParams as $k => $v) {
213+
$req->setQueryAttribute($k, $v);
214+
}
215+
159216
// Ensure that the memoized property is filled, so that comparison works consistently.
160217
$req->getEndpoint();
161218

0 commit comments

Comments
 (0)