diff --git a/requirements/optional.txt b/requirements/optional.txt index 48c9a630..93001904 100644 --- a/requirements/optional.txt +++ b/requirements/optional.txt @@ -14,3 +14,5 @@ SQLAlchemy>=1.4,<3 # websockets 9 is not compatible with Python 3.10 websockets>=9.1,<16 websocket-client>=1,<2 +# SSL certificate bundle for certificate verification issues +certifi diff --git a/slack_sdk/web/async_base_client.py b/slack_sdk/web/async_base_client.py index ebb0eb3d..7b2c1340 100644 --- a/slack_sdk/web/async_base_client.py +++ b/slack_sdk/web/async_base_client.py @@ -18,6 +18,7 @@ _get_url, get_user_agent, ) +from .ssl_utils import create_ssl_context_with_certifi_fallback from ..proxy_env_variable_loader import load_http_proxy_from_env from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers @@ -55,7 +56,7 @@ def __init__( """The maximum number of seconds the client will wait to connect and receive a response from Slack. Default is 30 seconds.""" - self.ssl = ssl + self.ssl = create_ssl_context_with_certifi_fallback(ssl) """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) instance, helpful for specifying your own custom certificate chain.""" diff --git a/slack_sdk/web/base_client.py b/slack_sdk/web/base_client.py index 6c3714a4..fb179759 100644 --- a/slack_sdk/web/base_client.py +++ b/slack_sdk/web/base_client.py @@ -29,6 +29,7 @@ _build_unexpected_body_error_message, _upload_file_via_v2_url, ) +from .ssl_utils import create_ssl_context_with_certifi_fallback from .slack_response import SlackResponse from slack_sdk.http_retry import default_retry_handlers from slack_sdk.http_retry.handler import RetryHandler @@ -67,7 +68,7 @@ def __init__( """The maximum number of seconds the client will wait to connect and receive a response from Slack. Default is 30 seconds.""" - self.ssl = ssl + self.ssl = create_ssl_context_with_certifi_fallback(ssl) """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) instance, helpful for specifying your own custom certificate chain.""" diff --git a/slack_sdk/web/ssl_utils.py b/slack_sdk/web/ssl_utils.py new file mode 100644 index 00000000..d7261cb7 --- /dev/null +++ b/slack_sdk/web/ssl_utils.py @@ -0,0 +1,34 @@ +import os +import ssl +from ssl import SSLContext +from typing import Optional + + +def has_ssl_env_vars() -> bool: + """Check if SSL-related environment variables are set""" + ssl_env_vars = ["SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"] + return any(os.environ.get(var) for var in ssl_env_vars) + + +def create_ssl_context_with_certifi_fallback(custom_ssl: Optional[SSLContext] = None) -> Optional[SSLContext]: + """Create SSL context with certifi fallback for certificate issues + + Priority: + 1. If custom_ssl is provided or SSL env vars are set -> return custom_ssl + 2. If certifi is available -> use certifi's certificate bundle + 3. Otherwise -> return custom_ssl (usually None) + + This helps resolve SSL certificate issues on Windows by using certifi's + curated certificate bundle when no explicit SSL configuration is provided. + """ + # Use custom_ssl if provided, or respect SSL environment variables + if custom_ssl is not None or has_ssl_env_vars(): + return custom_ssl + + # Fall back to certifi if available (helps with Windows SSL issues) + try: + import certifi + + return ssl.create_default_context(cafile=certifi.where()) + except ImportError: + return custom_ssl diff --git a/tests/slack_sdk/web/test_web_client_logger.py b/tests/slack_sdk/web/test_web_client_logger.py index 23e3e0ff..3847ff2f 100644 --- a/tests/slack_sdk/web/test_web_client_logger.py +++ b/tests/slack_sdk/web/test_web_client_logger.py @@ -1,5 +1,6 @@ import logging import unittest +from unittest.mock import patch from slack_sdk import WebClient from slack_sdk.web import base_client @@ -39,11 +40,12 @@ def test_logger_property_has_no_setter(self): client.logger = self.test_logger def test_ensure_web_client_with_logger_is_copyable(self): - client = WebClient( - base_url="http://localhost:8888", - token="xoxb-api_test", - logger=self.test_logger, - ) - client_copy = create_copy(client) - self.assertEqual(client.logger, self.test_logger) - self.assertEqual(client_copy.logger, self.test_logger) + with patch("slack_sdk.web.base_client.create_ssl_context_with_certifi_fallback", return_value=None): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + logger=self.test_logger, + ) + client_copy = create_copy(client) + self.assertEqual(client.logger, self.test_logger) + self.assertEqual(client_copy.logger, self.test_logger) diff --git a/tests/slack_sdk_async/web/test_async_web_client_logger.py b/tests/slack_sdk_async/web/test_async_web_client_logger.py index 1a267cb7..eacba0f3 100644 --- a/tests/slack_sdk_async/web/test_async_web_client_logger.py +++ b/tests/slack_sdk_async/web/test_async_web_client_logger.py @@ -1,5 +1,6 @@ import logging import unittest +from unittest.mock import patch from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web import async_base_client @@ -39,11 +40,12 @@ def test_logger_property_has_no_setter(self): client.logger = self.test_logger def test_ensure_async_web_client_with_logger_is_copyable(self): - client = AsyncWebClient( - base_url="http://localhost:8888", - token="xoxb-api_test", - logger=self.test_logger, - ) - client_copy = create_copy(client) - self.assertEqual(client.logger, self.test_logger) - self.assertEqual(client_copy.logger, self.test_logger) + with patch("slack_sdk.web.async_base_client.create_ssl_context_with_certifi_fallback", return_value=None): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + logger=self.test_logger, + ) + client_copy = create_copy(client) + self.assertEqual(client.logger, self.test_logger) + self.assertEqual(client_copy.logger, self.test_logger)