diff --git a/pycaprio/core/adapters/http_adapter.py b/pycaprio/core/adapters/http_adapter.py index ee8fd83..fa47d51 100644 --- a/pycaprio/core/adapters/http_adapter.py +++ b/pycaprio/core/adapters/http_adapter.py @@ -1,6 +1,4 @@ -from typing import IO, Union -from typing import List -from typing import Optional +from typing import IO, Union, List, Optional from pycaprio.core.clients.retryable_client import RetryableInceptionClient from pycaprio.core.interfaces.adapter import BaseInceptionAdapter @@ -30,8 +28,12 @@ def __init__( inception_host: str, authentication: authentication_type, inception_client: Optional[BaseInceptionClient] = None, + ca_bundle: Optional[str] = None, + verify: Union[bool, str] = True, ): - self.client = inception_client or RetryableInceptionClient(inception_host, authentication) + self.client = inception_client or RetryableInceptionClient( + inception_host, authentication, ca_bundle=ca_bundle, verify=verify + ) self.default_username, _ = authentication def projects(self) -> List[Project]: diff --git a/pycaprio/core/clients/retryable_client.py b/pycaprio/core/clients/retryable_client.py index d8a8259..576e2a1 100644 --- a/pycaprio/core/clients/retryable_client.py +++ b/pycaprio/core/clients/retryable_client.py @@ -1,8 +1,11 @@ import io +import os import time -from typing import Optional +from typing import Optional, Union import requests +from requests.adapters import HTTPAdapter +from urllib3.util.ssl_ import create_urllib3_context from requests_toolbelt import MultipartEncoder from pycaprio.core.exceptions import InceptionBadResponse @@ -10,6 +13,27 @@ from pycaprio.core.interfaces.types import authentication_type +class SSLAdapter(HTTPAdapter): + """ + Custom HTTPAdapter that allows merging custom CA certificates with the system default certificates. + This enables trusting both system CAs and custom/self-signed certificates. + """ + + def __init__(self, ca_bundle: Optional[str] = None, **kwargs): + self.ca_bundle = ca_bundle + super().__init__(**kwargs) + + def init_poolmanager(self, *args, **kwargs): + """Initialize pool manager with custom SSL context that includes custom CA certificates.""" + if self.ca_bundle: + # Create a default SSL context + ctx = create_urllib3_context() + # Load both system CAs and custom CAs + ctx.load_verify_locations(cafile=self.ca_bundle) + kwargs["ssl_context"] = ctx + return super().init_poolmanager(*args, **kwargs) + + class RetryableInceptionClient(BaseInceptionClient): """ HTTP client which implements retrying with exponential backoff. @@ -18,13 +42,33 @@ class RetryableInceptionClient(BaseInceptionClient): RETRY_STATUSES = (408, 502, 503, 504) - def __init__(self, inception_host: str, authentication: authentication_type, max_retries=3): + def __init__( + self, + inception_host: str, + authentication: authentication_type, + max_retries: int = 3, + ca_bundle: Optional[str] = None, + verify: bool = True, + ): super().__init__(inception_host, authentication) self.session = requests.Session() self.session.auth = authentication assert 0 < max_retries, "max_retries must be greater than 0" self.max_retries = max_retries + if verify is True: + # If verify is True, use the SSLAdapter + self.session.verify = True + else: + # If verify is False, disable verification + self.session.verify = False + + # Mount custom SSL adapter if we have a custom CA bundle + if ca_bundle and verify is not False: + adapter = SSLAdapter(ca_bundle=ca_bundle) + self.session.mount("https://", adapter) + self.session.mount("http://", adapter) + def get(self, url: str, params: Optional[dict] = None) -> requests.Response: return self.request("get", url, params=params) diff --git a/pycaprio/core/pycaprio.py b/pycaprio/core/pycaprio.py index f1eb25d..82567a5 100644 --- a/pycaprio/core/pycaprio.py +++ b/pycaprio/core/pycaprio.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import Optional, Union from pycaprio.core.adapters.http_adapter import HttpInceptionAdapter from pycaprio.core.exceptions import ConfigurationNotProvided @@ -17,6 +17,8 @@ def __init__( inception_host: Optional[str] = None, authentication: Optional[authentication_type] = None, local_projects_dir: Optional[str] = None, + ca_bundle: Optional[str] = None, + verify: Union[bool, str] = True, ): """ Initializes Pycaprio in either remote or local mode. @@ -24,6 +26,11 @@ def __init__( :param inception_host: Hostname of the INCEpTION instance. :param authentication: Tuple of username and password for INCEpTION instance. :param local_projects_dir: Directory containing exported INCEpTION projects in ZIP format. + :param ca_bundle: Path to a custom CA certificate file to trust. + This is the recommended way to support self-signed certificates. + :param verify: Controls SSL verification behavior: + - True (default): Verify SSL certificates using system CAs + ca_bundle if provided + - False: Disable SSL verification (not recommended for production) """ inception_host = inception_host or os.getenv("INCEPTION_HOST") if inception_host: @@ -33,8 +40,8 @@ def __init__( "Authentication was not provided. " "You can set it via environment variables as 'INCEPTION_USERNAME' and 'INCEPTION_PASSWORD'" ) - - self.api = HttpInceptionAdapter(inception_host, authentication) + + self.api = HttpInceptionAdapter(inception_host, authentication, ca_bundle=ca_bundle, verify=verify) elif local_projects_dir: self.api = LocalInceptionAdapter(local_projects_dir) else: