Skip to content

Commit 8df0b55

Browse files
Gurov Ilyafrankyncrwilcox
authored
feat(storage): improve v4 signature query parameters encoding (#48)
* feat(storage): improve v4 signature query parameters encoding * add URL encoding test * add query_parameters arg into conformance tests * add tests for _quote_param() function * declare test file encoding * fix the param type * add test with bytes * Update _signing.py Co-authored-by: Frank Natividad <[email protected]> Co-authored-by: Christopher Wilcox <[email protected]>
1 parent 0df8a91 commit 8df0b55

File tree

2 files changed

+91
-3
lines changed

2 files changed

+91
-3
lines changed

google/cloud/storage/_signing.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def generate_signed_url_v4(
509509
510510
:type query_parameters: dict
511511
:param query_parameters:
512-
(Optional) Additional query paramtersto be included as part of the
512+
(Optional) Additional query parameters to be included as part of the
513513
signed URLs. See:
514514
https://cloud.google.com/storage/docs/xml-api/reference-headers#query
515515
@@ -585,8 +585,7 @@ def generate_signed_url_v4(
585585
if generation is not None:
586586
query_parameters["generation"] = generation
587587

588-
ordered_query_parameters = sorted(query_parameters.items())
589-
canonical_query_string = six.moves.urllib.parse.urlencode(ordered_query_parameters)
588+
canonical_query_string = _url_encode(query_parameters)
590589

591590
lowercased_headers = dict(ordered_headers)
592591

@@ -672,3 +671,34 @@ def _sign_message(message, access_token, service_account_email):
672671

673672
data = json.loads(response.data.decode("utf-8"))
674673
return data["signature"]
674+
675+
676+
def _url_encode(query_params):
677+
"""Encode query params into URL.
678+
679+
:type query_params: dict
680+
:param query_params: Query params to be encoded.
681+
682+
:rtype: str
683+
:returns: URL encoded query params.
684+
"""
685+
params = [
686+
"{}={}".format(_quote_param(name), _quote_param(value))
687+
for name, value in query_params.items()
688+
]
689+
690+
return "&".join(sorted(params))
691+
692+
693+
def _quote_param(param):
694+
"""Quote query param.
695+
696+
:type param: Any
697+
:param param: Query param to be encoded.
698+
699+
:rtype: str
700+
:returns: URL encoded query param.
701+
"""
702+
if not isinstance(param, bytes):
703+
param = str(param)
704+
return six.moves.urllib.parse.quote(param, safe="~")

tests/unit/test__signing.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
#
13
# Copyright 2017 Google LLC
24
#
35
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -705,6 +707,61 @@ def test_sign_bytes_failure(self):
705707
)
706708

707709

710+
class TestCustomURLEncoding(unittest.TestCase):
711+
def test_url_encode(self):
712+
from google.cloud.storage._signing import _url_encode
713+
714+
# param1 includes safe symbol ~
715+
# param# includes symbols, which must be encoded
716+
query_params = {"param1": "value~1-2", "param#": "*value+value/"}
717+
718+
self.assertEqual(
719+
_url_encode(query_params), "param%23=%2Avalue%2Bvalue%2F&param1=value~1-2"
720+
)
721+
722+
723+
class TestQuoteParam(unittest.TestCase):
724+
def test_ascii_symbols(self):
725+
from google.cloud.storage._signing import _quote_param
726+
727+
encoded_param = _quote_param("param")
728+
self.assertIsInstance(encoded_param, str)
729+
self.assertEqual(encoded_param, "param")
730+
731+
def test_quoted_symbols(self):
732+
from google.cloud.storage._signing import _quote_param
733+
734+
encoded_param = _quote_param("!#$%&'()*+,/:;=?@[]")
735+
self.assertIsInstance(encoded_param, str)
736+
self.assertEqual(
737+
encoded_param, "%21%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"
738+
)
739+
740+
def test_unquoted_symbols(self):
741+
from google.cloud.storage._signing import _quote_param
742+
import string
743+
744+
UNQUOTED = string.ascii_letters + string.digits + ".~_-"
745+
746+
encoded_param = _quote_param(UNQUOTED)
747+
self.assertIsInstance(encoded_param, str)
748+
self.assertEqual(encoded_param, UNQUOTED)
749+
750+
def test_unicode_symbols(self):
751+
from google.cloud.storage._signing import _quote_param
752+
753+
encoded_param = _quote_param("ЁЙЦЯЩЯЩ")
754+
self.assertIsInstance(encoded_param, str)
755+
self.assertEqual(encoded_param, "%D0%81%D0%99%D0%A6%D0%AF%D0%A9%D0%AF%D0%A9")
756+
757+
def test_bytes(self):
758+
from google.cloud.storage._signing import _quote_param
759+
760+
encoded_param = _quote_param(b"bytes")
761+
self.assertIsInstance(encoded_param, str)
762+
self.assertEqual(encoded_param, "bytes")
763+
764+
708765
_DUMMY_SERVICE_ACCOUNT = None
709766

710767

@@ -731,6 +788,7 @@ def _run_conformance_test(resource, test_data):
731788
method=test_data["method"],
732789
_request_timestamp=test_data["timestamp"],
733790
headers=test_data.get("headers"),
791+
query_parameters=test_data.get("queryParameters"),
734792
)
735793

736794
assert url == test_data["expectedUrl"]

0 commit comments

Comments
 (0)