Skip to content

Commit c27edc5

Browse files
[App Configuration] - Fix http bug (Azure#43354)
* fix http bug * add change log * use const for connection string prefixes * fix format * keep the type check for base_url * add test * revert change * fix lint * revert change
1 parent ee73a6e commit c27edc5

File tree

4 files changed

+65
-10
lines changed

4 files changed

+65
-10
lines changed

sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Breaking Changes
88

99
### Bugs Fixed
10+
- Fixed a bug where non-HTTPS endpoints would not function correctly.
1011

1112
### Other Changes
1213

sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_azure_appconfiguration_requests.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import hashlib
88
import base64
99
import hmac
10+
from urllib.parse import urlparse
1011
from azure.core.credentials import AzureKeyCredential
1112
from azure.core.pipeline.policies import HTTPPolicy
1213
from ._utils import get_current_utc_time
@@ -18,14 +19,15 @@ class AppConfigRequestsCredentialsPolicy(HTTPPolicy):
1819
def __init__(self, credential: AzureKeyCredential, endpoint: str, id_credential: str):
1920
super(AppConfigRequestsCredentialsPolicy, self).__init__()
2021
self._credential = credential
21-
self._host = str(endpoint[8:])
22+
self._endpoint = endpoint
2223
self._id_credential = id_credential
2324

2425
def _signed_request(self, request):
2526
verb = request.http_request.method.upper()
2627

27-
# Get the path and query from url, which looks like https://host/path/query
28-
query_url = str(request.http_request.url[len(self._host) + 8 :])
28+
parsed_url = urlparse(self._endpoint)
29+
# Get the path, query and hash fragment from url, which looks like https://host/path?query#fragment
30+
query_url = str(request.http_request.url[len(parsed_url.geturl()) :])
2931
# Need URL() to get a correct encoded key value, from "%2A" to "*", when transport is in type AioHttpTransport.
3032
# There's a similar scenario in azure-storage-blob, the check logic is from there.
3133
try:
@@ -53,7 +55,7 @@ def _signed_request(self, request):
5355
content_digest = hashlib.sha256((request.http_request.body.encode("utf-8"))).digest()
5456
content_hash = base64.b64encode(content_digest).decode("utf-8")
5557

56-
string_to_sign = verb + "\n" + query_url + "\n" + utc_now + ";" + self._host + ";" + content_hash
58+
string_to_sign = verb + "\n" + query_url + "\n" + utc_now + ";" + parsed_url.netloc + ";" + content_hash
5759

5860
decoded_secret = base64.b64decode(self._credential.key)
5961
digest = hmac.new(decoded_secret, string_to_sign.encode("utf-8"), hashlib.sha256).digest()

sdk/appconfiguration/azure-appconfiguration/azure/appconfiguration/_utils.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
from datetime import datetime
88
from typing import Optional, Tuple, Dict, Any
99

10+
# Connection string component prefixes
11+
_ENDPOINT_PREFIX = "Endpoint="
12+
_ID_PREFIX = "Id="
13+
_SECRET_PREFIX = "Secret="
14+
1015

1116
def parse_connection_string(connection_string: str) -> Tuple[str, str, str]:
1217
# connection_string looks like Endpoint=https://xxxxx;Id=xxxxx;Secret=xxxx
@@ -19,12 +24,12 @@ def parse_connection_string(connection_string: str) -> Tuple[str, str, str]:
1924
secret = ""
2025
for segment in segments:
2126
segment = segment.strip()
22-
if segment.startswith("Endpoint"):
23-
endpoint = str(segment[17:])
24-
elif segment.startswith("Id"):
25-
id_ = str(segment[3:])
26-
elif segment.startswith("Secret"):
27-
secret = str(segment[7:])
27+
if segment.startswith(_ENDPOINT_PREFIX):
28+
endpoint = str(segment[len(_ENDPOINT_PREFIX) :])
29+
elif segment.startswith(_ID_PREFIX):
30+
id_ = str(segment[len(_ID_PREFIX) :])
31+
elif segment.startswith(_SECRET_PREFIX):
32+
secret = str(segment[len(_SECRET_PREFIX) :])
2833
else:
2934
raise ValueError("Invalid connection string.")
3035

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
7+
import base64
8+
from types import SimpleNamespace
9+
from azure.core.credentials import AzureKeyCredential
10+
from azure.core.pipeline.transport import HttpRequest
11+
from azure.core.pipeline import PipelineRequest
12+
from azure.appconfiguration._azure_appconfiguration_requests import (
13+
AppConfigRequestsCredentialsPolicy,
14+
)
15+
from azure.appconfiguration._utils import parse_connection_string
16+
17+
18+
def test_parse_connection_string_returns_http_endpoint():
19+
# An http (not https) endpoint to ensure scheme is preserved
20+
conn_str = "Endpoint=http://localhost:8483;Id=test-id;Secret=test-secret"
21+
endpoint, id_, secret = parse_connection_string(conn_str)
22+
assert endpoint == "http://localhost:8483"
23+
assert id_ == "test-id"
24+
assert secret == "test-secret"
25+
26+
27+
def test_signed_request_with_http_endpoint():
28+
endpoint = "http://localhost:8483"
29+
request = HttpRequest("GET", endpoint + "/kv/foo?label=env")
30+
# Create pipeline request with placeholder context, then replace with a namespace that has a transport attr.
31+
pipeline_request = PipelineRequest(request, {})
32+
pipeline_request.context = SimpleNamespace(transport=object())
33+
34+
key = base64.b64encode(b"secret").decode("utf-8")
35+
credential = AzureKeyCredential(key)
36+
policy = AppConfigRequestsCredentialsPolicy(credential, endpoint, "test-id")
37+
38+
policy._signed_request(pipeline_request) # pylint: disable=protected-access
39+
40+
headers = pipeline_request.http_request.headers
41+
42+
assert pipeline_request.http_request.url.startswith("http://")
43+
assert "x-ms-date" in headers
44+
assert "x-ms-content-sha256" in headers
45+
auth = headers.get("Authorization")
46+
assert auth is not None
47+
assert auth.startswith("HMAC-SHA256 ")

0 commit comments

Comments
 (0)