Skip to content

Commit

Permalink
add https support
Browse files Browse the repository at this point in the history
  • Loading branch information
pjbull committed Sep 16, 2024
1 parent db0b813 commit f648264
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 24 deletions.
14 changes: 11 additions & 3 deletions cloudpathlib/http/httpclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(
if self.auth is None:
self.opener = urllib.request.build_opener()
else:
self.openener = urllib.request.build_opener(self.auth)
self.opener = urllib.request.build_opener(self.auth)

self.custom_list_page_parser = custom_list_page_parser

Expand Down Expand Up @@ -103,9 +103,9 @@ def _list_dir(self, cloud_path: HttpPath, recursive: bool) -> Iterable[Tuple[Htt
if recursive and is_dir:
yield from self._list_dir(path, recursive=True)

except: # noqa E722
except Exception as e: # noqa E722
raise NotImplementedError(
"Unable to parse response as a listing of files; please provide a custom parser as `custom_list_page_parser`."
f"Unable to parse response as a listing of files; please provide a custom parser as `custom_list_page_parser`. Error raised: {e}"
)

def _upload_file(self, local_path: Union[str, os.PathLike], cloud_path: HttpPath) -> HttpPath:
Expand Down Expand Up @@ -158,3 +158,11 @@ def request(self, url: HttpPath, method: str, **kwargs) -> None:


HttpClient.HttpPath = HttpClient.CloudPath # type: ignore


@register_client_class("https")
class HttpsClient(HttpClient):
pass


HttpsClient.HttpsPath = HttpsClient.CloudPath # type: ignore
5 changes: 5 additions & 0 deletions cloudpathlib/http/httppath.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,8 @@ def delete(self, **kwargs):

def head(self, **kwargs):
return self.client.request(self, "HEAD", **kwargs)


@register_path_class("https")
class HttpsPath(HttpPath):
cloud_prefix: str = "https://"
42 changes: 39 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
from pathlib import Path, PurePosixPath
import shutil
import ssl
from tempfile import TemporaryDirectory
from typing import Dict, Optional
from urllib.parse import urlparse
from urllib.request import HTTPSHandler

from azure.storage.blob import BlobServiceClient
from azure.storage.filedatalake import (
Expand All @@ -19,8 +21,8 @@

from cloudpathlib import AzureBlobClient, AzureBlobPath, GSClient, GSPath, S3Client, S3Path
from cloudpathlib.cloudpath import implementation_registry
from cloudpathlib.http.httpclient import HttpClient
from cloudpathlib.http.httppath import HttpPath
from cloudpathlib.http.httpclient import HttpClient, HttpsClient
from cloudpathlib.http.httppath import HttpPath, HttpsPath
from cloudpathlib.local import (
local_azure_blob_implementation,
LocalAzureBlobClient,
Expand All @@ -45,7 +47,7 @@
from .mock_clients.mock_s3 import mocked_session_class_factory, DEFAULT_S3_BUCKET_NAME


from .http_fixtures import http_server # noqa: F401
from .http_fixtures import http_server, https_server, utilities_dir # noqa: F401

if os.getenv("USE_LIVE_CLOUD") == "1":
load_dotenv(find_dotenv())
Expand Down Expand Up @@ -505,6 +507,40 @@ def http_rig(request, assets_dir, http_server): # noqa: F811
)

rig.http_server_dir = server_dir
rig.client_class(**rig.required_client_kwargs).set_as_default_client() # set default client

yield rig

rig.client_class._default_client = None # reset default client
shutil.rmtree(server_dir)


@fixture()
def https_rig(request, assets_dir, https_server): # noqa: F811
test_dir = create_test_dir_name(request)

host, server_dir = https_server
drive = urlparse(host).netloc

# copy test assets
shutil.copytree(assets_dir, server_dir / test_dir)

skip_verify_ctx = ssl.SSLContext()
skip_verify_ctx.check_hostname = False
skip_verify_ctx.load_verify_locations(utilities_dir / "insecure-test.pem")

rig = CloudProviderTestRig(
path_class=HttpsPath,
client_class=HttpsClient,
drive=drive,
test_dir=test_dir,
required_client_kwargs=dict(
auth=HTTPSHandler(context=skip_verify_ctx, check_hostname=False)
),
)

rig.http_server_dir = server_dir
rig.client_class(**rig.required_client_kwargs).set_as_default_client() # set default client

yield rig

Expand Down
91 changes: 73 additions & 18 deletions tests/http_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
import os
from pathlib import Path
import shutil
import ssl
import threading
import time
from urllib.request import urlopen

from pytest import fixture


utilities_dir = Path(__file__).parent / "utilities"


class TestHTTPRequestHandler(SimpleHTTPRequestHandler):
"""Also allows PUT and DELETE requests for testing."""

Expand Down Expand Up @@ -47,40 +51,91 @@ def do_DELETE(self):
self.end_headers()


@fixture(scope="module")
def http_server(tmp_path_factory, worker_id):
hostname = "localhost"
port = (
9077 + int(worker_id.lstrip("gw")) if worker_id != "master" else 0
) # don't collide if tests running in parallel with multiple servers
def _http_server(
root_dir, port, hostname="localhost", use_ssl=False, certfile=None, keyfile=None, threaded=True
):
root_dir.mkdir(exist_ok=True)

# Create a temporary directory to serve files from
server_dir = tmp_path_factory.mktemp("server_files").resolve()
server_dir.mkdir(exist_ok=True)
scheme = "http" if not use_ssl else "https"

# Function to start the server
def start_server():
handler = partial(TestHTTPRequestHandler, directory=str(server_dir))
handler = partial(TestHTTPRequestHandler, directory=str(root_dir))
httpd = HTTPServer((hostname, port), handler)

if use_ssl:
if not certfile or not keyfile:
raise ValueError("certfile and keyfile must be provided if `ssl=True`")

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=certfile, keyfile=keyfile)
context.check_hostname = False
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)

httpd.serve_forever()

# Start the server in a separate thread
server_thread = threading.Thread(target=start_server, daemon=True)
server_thread.start()
if threaded:
server_thread = threading.Thread(target=start_server, daemon=True)
server_thread.start()

else:
start_server()

# Wait for the server to start
for _ in range(10):
try:
urlopen(f"http://{hostname}:{port}")
if use_ssl:
req_context = ssl.SSLContext()
req_context.check_hostname = False
req_context.verify_mode = ssl.CERT_NONE
else:
req_context = None

urlopen(f"{scheme}://{hostname}:{port}", context=req_context)

break
except Exception:
time.sleep(0.1)

yield f"http://{hostname}:{port}", server_dir
return f"{scheme}://{hostname}:{port}", server_thread


@fixture(scope="module")
def http_server(tmp_path_factory, worker_id):
port = 9077 + (
int(worker_id.lstrip("gw")) if worker_id != "master" else 0
) # don't collide if tests running in parallel with multiple servers

server_dir = tmp_path_factory.mktemp("server_files").resolve()

host, server_thread = _http_server(server_dir, port)

yield host, server_dir

server_thread.join(0)

if server_dir.exists():
shutil.rmtree(server_dir)


@fixture(scope="module")
def https_server(tmp_path_factory, worker_id):
port = 4443 + (
int(worker_id.lstrip("gw")) if worker_id != "master" else 0
) # don't collide if tests running in parallel with multiple servers

server_dir = tmp_path_factory.mktemp("server_files").resolve()

host, server_thread = _http_server(
server_dir,
port,
use_ssl=True,
certfile=utilities_dir / "insecure-test.pem",
keyfile=utilities_dir / "insecure-test.key",
)

yield host, server_dir

# Stop the server by exiting the thread
server_thread.join(0)

# Clean up the temporary directory if it still exists
if server_dir.exists():
shutil.rmtree(server_dir)
36 changes: 36 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from tests.conftest import CloudProviderTestRig


def test_https(https_rig: CloudProviderTestRig):
"""Basic tests for https; we run the full suite against the http_rig"""
existing_file = https_rig.create_cloud_path("dir_0/file0_0.txt")

# existence and listing
assert existing_file.exists()
assert existing_file.parent.exists()
assert existing_file.name in [f.name for f in existing_file.parent.iterdir()]

# root level checks
root = list(existing_file.parents)[-1]
assert root.exists()
assert len(list(root.iterdir())) > 0

# reading and wrirting
existing_file.write_text("Hello from 0")
assert existing_file.read_text() == "Hello from 0"

# creating new files
not_existing_file = https_rig.create_cloud_path("dir_0/new_file.txt")

assert not not_existing_file.exists()

not_existing_file.upload_from(existing_file)

assert not_existing_file.read_text() == "Hello from 0"

# deleteing
not_existing_file.unlink()
assert not not_existing_file.exists()

# metadata
assert existing_file.stat().st_mtime != 0
27 changes: 27 additions & 0 deletions tests/utilities/insecure-test.csr
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEqjCCApICAQAwSDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCENvbG9yYWRvMQ8w
DQYDVQQHDAZEZW52ZXIxFTATBgNVBAoMDGNsb3VkcGF0aGxpYjCCAiIwDQYJKoZI
hvcNAQEBBQADggIPADCCAgoCggIBAK5PvMKSP46Sf+8kEFEQdbMkcr9Oph1pzPK6
yIRwWJK2CRTduLKYjzeivyS3roqKf2RK8CI3/aPRdMENADdAlUvRkfHYy1VyJey+
9kuZ/DZfcmMXcUkNfiezv2PltGSL0eGYlWCCH2sAZc51LZrBwfnma1NAXiqDe0yD
36izMxIKgoGQ+DoatxNhQVYprDOi4VRW7qtw6V2Y/zqBFXctjBVeLyEm4c0MLdUQ
I/Ftw1mcttPmFWgfkGuOEeDdL7HFTbRj6PpzIC4mh1OSDONmv455XSQmia4egrDS
bpIrBOH8Al3fukD8R+Bwv0thWjVezFUQCxiynfASq6Lhb/kqTp93XcWw4DVaVPox
xGUDqDgfPq4XGxrKQR3ah94c/7jyhz4ih6td5KLf4hvExK77i3l61dgqW/86uj7g
gJEkWcAAY/SVnZneZSEClM82P/YyGavTTzw6ibi1n2zaRnRjuzEqiC6C92VoYlWF
F4S50o/gHhCHYWb775IIt8CAYqqryBHrN0r2vvJVU6lOmHTsnfbVv+XzGgNroBP9
NsP1jDJA04XGMCq6DT8B5V5GO6kVn37Uqb5ER6RTBTxlcHh6oqtzdoHlVxMjdLwh
HPAug/DTZn4a1b9zTyK1YqSzNIM8eV/ckmySG5YMZJQovMHd7YVzB4hjq9kVupxa
bfPhjIHxAgMBAAGgHTAbBgkqhkiG9w0BCQcxDgwMY2xvdWRwYXRobGliMA0GCSqG
SIb3DQEBCwUAA4ICAQBeTRNKjo+ol3zuKfteuKt7+mLWzL3/qtUSCmCwt6t+NebN
ebkOwZA4HW0uUt/rdht58CJAvFri+DnkEidt/ldcg/CQ/tpWALZFdNa2z0hb+qEL
Q7wHO1QkwHG8/Q7yrcBNGSDsp4l7cH+8FQBcAVJxn++ixTe4dIiyscUdNRkXywsT
/UdQlK3oULR7Zv9k3nDErXTow/6QazjxtUyrfyuFdSDTAKJaKCOLt5NcJif/Ev3G
rUMJQElNz3W0P73ci+ueuihYdaveDx1vptO9VCBnwFOyTgjCYPS9g3MB8KIh5cJz
sj2J5J5tEUsyAa8ky4hvoLyP7GE29XvPA8pH1rOtQ++lmMzpP1vkPEGe0ezXrw2y
h4LBJXeMCg3/r3otEHnppI5PRTX3m1WlHyInpFIjets6VLDKjwENyreDmO5hIfRd
4ZxjxYzG97Tekoa+v9Y9qf3YCCGvbswOwfyj8hNheoMKv2f+rG2MwSPWfYlML/oT
4UA/C3o9Y7oa7H9FdEiTuXRgLcKUZqZJ0JuVhSbdPAAYSdrQE/EF06jyU6ZENxUu
0UJRwaXLETIIii99TUxyTmJTrvWAEbo5hpwfA1P6aaCLtWj0Qm6WSD3uLjU56yaX
6Q2kdspxv1BiT2TC4RO/ZH/8OwmSfe0dSg8jEOQf2+B0DcTPD+yHjo2hZWpT0A==
-----END CERTIFICATE REQUEST-----
52 changes: 52 additions & 0 deletions tests/utilities/insecure-test.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
MTfJlrjd23Xn+aEmf/4M35SFl7OFxts=
-----END PRIVATE KEY-----
30 changes: 30 additions & 0 deletions tests/utilities/insecure-test.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFFzCCAv8CFBtqKeSAcQf/bQBPZaROIpbzIQ7UMA0GCSqGSIb3DQEBCwUAMEgx
CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhDb2xvcmFkbzEPMA0GA1UEBwwGRGVudmVy
MRUwEwYDVQQKDAxjbG91ZHBhdGhsaWIwHhcNMjQwOTEzMTExNzQzWhcNMzMxMTMw
MTExNzQzWjBIMQswCQYDVQQGEwJVUzERMA8GA1UECAwIQ29sb3JhZG8xDzANBgNV
BAcMBkRlbnZlcjEVMBMGA1UECgwMY2xvdWRwYXRobGliMIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEArk+8wpI/jpJ/7yQQURB1syRyv06mHWnM8rrIhHBY
krYJFN24spiPN6K/JLeuiop/ZErwIjf9o9F0wQ0AN0CVS9GR8djLVXIl7L72S5n8
Nl9yYxdxSQ1+J7O/Y+W0ZIvR4ZiVYIIfawBlznUtmsHB+eZrU0BeKoN7TIPfqLMz
EgqCgZD4Ohq3E2FBVimsM6LhVFbuq3DpXZj/OoEVdy2MFV4vISbhzQwt1RAj8W3D
WZy20+YVaB+Qa44R4N0vscVNtGPo+nMgLiaHU5IM42a/jnldJCaJrh6CsNJukisE
4fwCXd+6QPxH4HC/S2FaNV7MVRALGLKd8BKrouFv+SpOn3ddxbDgNVpU+jHEZQOo
OB8+rhcbGspBHdqH3hz/uPKHPiKHq13kot/iG8TErvuLeXrV2Cpb/zq6PuCAkSRZ
wABj9JWdmd5lIQKUzzY/9jIZq9NPPDqJuLWfbNpGdGO7MSqILoL3ZWhiVYUXhLnS
j+AeEIdhZvvvkgi3wIBiqqvIEes3Sva+8lVTqU6YdOyd9tW/5fMaA2ugE/02w/WM
MkDThcYwKroNPwHlXkY7qRWfftSpvkRHpFMFPGVweHqiq3N2geVXEyN0vCEc8C6D
8NNmfhrVv3NPIrVipLM0gzx5X9ySbJIblgxklCi8wd3thXMHiGOr2RW6nFpt8+GM
gfECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAVIRLRR5bitzThcTsmSCPAzqbVCf1
HSsTWGnISwiI3GD+2d+TykY+g9fw2eKbXzbfHu9VHFAMdpHfQc7Ud3d+tM45LnCo
cnvdXrpQg2EEdZaFJ76SmFMFoAnMd9LkuSdzt0P28nOlXVn/KDFp2ea8ROUUaM55
oGjo6Cj7i9h5fEnuAEE2Gcepjp9DRjJRIuwAxcihEcQSxzv4mOHqwMuCk6dpOG5S
MgVoCMiWz/9vn9U+Vyn5cjTzLgbmEQPVm5BL57QfPUhFW8cAMR5NeIeizLSpiBZQ
+RvzW/S2T+s8Cc0GgUjgiAmOLRCVMLTJ+jv1KvWFzu762POqXpreTD9UGLHnUvxI
RbhEgxj8p4169CeJSa0A19U6pFWFsZU2MLJkjHTIGlpzk5Vg5qzMyybcbk9wQQZ/
CMOg5pVaCZHyTUwrFxKF51oIv9a/tuQSe/ryj8GIj7t0mq0+7klvEn1a6wrkSr73
FzMNaEm4eLRVWYbHj8m4314vvaDjtUXCcMDRLb8j3fjyrcPPTkbO99rt1jVfU5wS
Ji7tVksGrTIHHlWkqZdbPhfZyTBIG34FjtjSClNVsOBeX+VqUuku8uQaM/9iVNZS
QamZuURGQ1x5+XHMjUQpoqAII+zXegJ1RiVfequYcF7F0bermVVVGdb/Ly2yNH1F
O5/LKKZ32+d5sm4=
-----END CERTIFICATE-----

0 comments on commit f648264

Please sign in to comment.