Skip to content

Commit 4e3ef98

Browse files
committed
feat: allow to set a signature port for tunnel usage
Signed-off-by: Andreas Lang <[email protected]>
1 parent c8b04a5 commit 4e3ef98

File tree

4 files changed

+102
-3
lines changed

4 files changed

+102
-3
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
88
### Deprecated
99
### Removed
1010
### Fixed
11+
- Fixed allow AWSV4SignerAuth to work with a tunnel ([[#184](https://github.com/opensearch-project/opensearch-py/issues/184)
1112
### Security
1213
### Dependencies
1314
- Bumps `sphinx` from <7.1 to <7.3

opensearchpy/helpers/signer.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
#
77
# Modifications Copyright OpenSearch Contributors. See
88
# GitHub history for details.
9-
109
import sys
1110

1211
import requests
@@ -43,12 +42,22 @@ def fetch_url(prepared_request): # type: ignore
4342
return url.scheme + "://" + location + path + querystring
4443

4544

45+
def derive_signature_url(original_url: str, singing_port: int) -> str:
46+
url = urlparse(original_url)
47+
if url.hostname is None:
48+
raise RuntimeError("Cannot use derive_signature_url on urls without hostname.")
49+
else:
50+
return url._replace(netloc=url.hostname + ":" + str(singing_port)).geturl()
51+
52+
4653
class AWSV4SignerAuth(requests.auth.AuthBase):
4754
"""
4855
AWS V4 Request Signer for Requests.
4956
"""
5057

51-
def __init__(self, credentials, region, service="es"): # type: ignore
58+
def __init__(self, credentials, region, service="es", signature_port=None): # type: ignore
59+
# can be used to sign the request for a different port than the request, e.g. due to a tunnel being used
60+
self.signature_port = signature_port
5261
if not credentials:
5362
raise ValueError("Credentials cannot be empty")
5463
self.credentials = credentials
@@ -79,7 +88,9 @@ def _sign_request(self, prepared_request): # type: ignore
7988
# create an AWS request object and sign it using SigV4Auth
8089
aws_request = AWSRequest(
8190
method=prepared_request.method.upper(),
82-
url=url,
91+
url=derive_signature_url(url, self.signature_port)
92+
if self.signature_port is not None
93+
else url,
8394
data=prepared_request.body,
8495
)
8596

test_opensearchpy/test_connection.py

+53
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,65 @@ def test_aws_signer_as_http_auth(self):
336336
con = RequestsHttpConnection(http_auth=auth)
337337
prepared_request = requests.Request("GET", "http://localhost").prepare()
338338
auth(prepared_request)
339+
self._assert_auth_and_signature_headers(auth, con, prepared_request)
340+
341+
@pytest.mark.skipif(
342+
sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+"
343+
)
344+
def test_aws_signer_as_http_auth_with_query_path(self):
345+
region = "us-west-2"
346+
347+
import requests
348+
349+
from opensearchpy.helpers.signer import AWSV4SignerAuth
350+
351+
auth = AWSV4SignerAuth(self.mock_session(), region)
352+
con = RequestsHttpConnection(http_auth=auth)
353+
prepared_request = requests.Request(
354+
"GET", "http://localhost?hello=world"
355+
).prepare()
356+
auth(prepared_request)
357+
self._assert_auth_and_signature_headers(auth, con, prepared_request)
358+
359+
def _assert_auth_and_signature_headers(self, auth, con, prepared_request):
339360
self.assertEqual(auth, con.session.auth)
340361
self.assertIn("Authorization", prepared_request.headers)
341362
self.assertIn("X-Amz-Date", prepared_request.headers)
342363
self.assertIn("X-Amz-Security-Token", prepared_request.headers)
343364
self.assertIn("X-Amz-Content-SHA256", prepared_request.headers)
344365

366+
@pytest.mark.skipif(
367+
sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+"
368+
)
369+
def test_aws_signer_as_http_auth_with_sign_port_with_port_on_base_url(self):
370+
region = "us-west-2"
371+
372+
import requests
373+
374+
from opensearchpy.helpers.signer import AWSV4SignerAuth
375+
376+
auth = AWSV4SignerAuth(self.mock_session(), region, signature_port=443)
377+
con = RequestsHttpConnection(http_auth=auth)
378+
prepared_request = requests.Request("GET", "http://localhost:1045").prepare()
379+
auth(prepared_request)
380+
self._assert_auth_and_signature_headers(auth, con, prepared_request)
381+
382+
@pytest.mark.skipif(
383+
sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+"
384+
)
385+
def test_aws_signer_as_http_auth_with_sign_port_but_without_port_on_base_url(self):
386+
region = "us-west-2"
387+
388+
import requests
389+
390+
from opensearchpy.helpers.signer import AWSV4SignerAuth
391+
392+
auth = AWSV4SignerAuth(self.mock_session(), region, signature_port=443)
393+
con = RequestsHttpConnection(http_auth=auth)
394+
prepared_request = requests.Request("GET", "http://localhost").prepare()
395+
auth(prepared_request)
396+
self._assert_auth_and_signature_headers(auth, con, prepared_request)
397+
345398
@pytest.mark.skipif(
346399
sys.version_info < (3, 6), reason="AWSV4SignerAuth requires python3.6+"
347400
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# The OpenSearch Contributors require contributions made to
4+
# this file be licensed under the Apache-2.0 license or a
5+
# compatible open source license.
6+
#
7+
# Modifications Copyright OpenSearch Contributors. See
8+
# GitHub history for details.
9+
from unittest import TestCase
10+
11+
from opensearchpy.helpers.signer import derive_signature_url
12+
13+
14+
class TestUrllib3Connection(TestCase):
15+
def test_derive_signature_url(self):
16+
assert (
17+
derive_signature_url("http://localhost:10552/", singing_port=443)
18+
== "http://localhost:443/"
19+
)
20+
assert (
21+
derive_signature_url("http://localhost:10552/foo/bar", singing_port=443)
22+
== "http://localhost:443/foo/bar"
23+
)
24+
assert (
25+
derive_signature_url("http://localhost/", singing_port=443)
26+
== "http://localhost:443/"
27+
)
28+
assert (
29+
derive_signature_url("http://localhost/foo/bar", singing_port=443)
30+
== "http://localhost:443/foo/bar"
31+
)
32+
33+
def test_derive_signature_url_no_hostname(self):
34+
self.assertRaises(RuntimeError, derive_signature_url, "http://", 23)

0 commit comments

Comments
 (0)