From 08f5c6d7724e62b3f5b112c6fc41ec7c8d87d6cf Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 1 Mar 2022 21:19:27 +0900 Subject: [PATCH] Fix #604 Respect the proxy_url in respond --- slack_bolt/context/async_context.py | 6 +- slack_bolt/context/context.py | 6 +- slack_bolt/context/respond/async_respond.py | 21 +++++- slack_bolt/context/respond/respond.py | 21 +++++- tests/scenario_tests/test_app.py | 55 +++++++++++++- tests/scenario_tests_async/test_app.py | 84 +++++++++++++++++++++ 6 files changed, 184 insertions(+), 9 deletions(-) diff --git a/slack_bolt/context/async_context.py b/slack_bolt/context/async_context.py index 46eb6c69d..81105c6df 100644 --- a/slack_bolt/context/async_context.py +++ b/slack_bolt/context/async_context.py @@ -116,5 +116,9 @@ async def handle_button_clicks(ack, respond): Callable `respond()` function """ if "respond" not in self: - self["respond"] = AsyncRespond(response_url=self.response_url) + self["respond"] = AsyncRespond( + response_url=self.response_url, + proxy=self.client.proxy, + ssl=self.client.ssl, + ) return self["respond"] diff --git a/slack_bolt/context/context.py b/slack_bolt/context/context.py index a92ac05bb..570a7080e 100644 --- a/slack_bolt/context/context.py +++ b/slack_bolt/context/context.py @@ -118,5 +118,9 @@ def handle_button_clicks(ack, respond): Callable `respond()` function """ if "respond" not in self: - self["respond"] = Respond(response_url=self.response_url) + self["respond"] = Respond( + response_url=self.response_url, + proxy=self.client.proxy, + ssl=self.client.ssl, + ) return self["respond"] diff --git a/slack_bolt/context/respond/async_respond.py b/slack_bolt/context/respond/async_respond.py index 60e4fe473..a589ef792 100644 --- a/slack_bolt/context/respond/async_respond.py +++ b/slack_bolt/context/respond/async_respond.py @@ -1,4 +1,5 @@ from typing import Optional, Union, Sequence +from ssl import SSLContext from slack_sdk.models.attachments import Attachment from slack_sdk.models.blocks import Block @@ -9,9 +10,19 @@ class AsyncRespond: response_url: Optional[str] + proxy: Optional[str] + ssl: Optional[SSLContext] - def __init__(self, *, response_url: Optional[str]): - self.response_url: Optional[str] = response_url + def __init__( + self, + *, + response_url: Optional[str], + proxy: Optional[str] = None, + ssl: Optional[SSLContext] = None, + ): + self.response_url = response_url + self.proxy = proxy + self.ssl = ssl async def __call__( self, @@ -25,7 +36,11 @@ async def __call__( unfurl_media: Optional[bool] = None, ) -> WebhookResponse: if self.response_url is not None: - client = AsyncWebhookClient(self.response_url) + client = AsyncWebhookClient( + url=self.response_url, + proxy=self.proxy, + ssl=self.ssl, + ) text_or_whole_response: Union[str, dict] = text if isinstance(text_or_whole_response, str): message = _build_message( diff --git a/slack_bolt/context/respond/respond.py b/slack_bolt/context/respond/respond.py index 21de92263..6cef9c36f 100644 --- a/slack_bolt/context/respond/respond.py +++ b/slack_bolt/context/respond/respond.py @@ -1,4 +1,5 @@ from typing import Optional, Union, Sequence +from ssl import SSLContext from slack_sdk.models.attachments import Attachment from slack_sdk.models.blocks import Block @@ -9,9 +10,19 @@ class Respond: response_url: Optional[str] + proxy: Optional[str] + ssl: Optional[SSLContext] - def __init__(self, *, response_url: Optional[str]): - self.response_url: Optional[str] = response_url + def __init__( + self, + *, + response_url: Optional[str], + proxy: Optional[str] = None, + ssl: Optional[SSLContext] = None, + ): + self.response_url = response_url + self.proxy = proxy + self.ssl = ssl def __call__( self, @@ -25,7 +36,11 @@ def __call__( unfurl_media: Optional[bool] = None, ) -> WebhookResponse: if self.response_url is not None: - client = WebhookClient(self.response_url) + client = WebhookClient( + url=self.response_url, + proxy=self.proxy, + ssl=self.ssl, + ) text_or_whole_response: Union[str, dict] = text if isinstance(text_or_whole_response, str): text = text_or_whole_response diff --git a/tests/scenario_tests/test_app.py b/tests/scenario_tests/test_app.py index 936ad68ec..86e89c9e9 100644 --- a/tests/scenario_tests/test_app.py +++ b/tests/scenario_tests/test_app.py @@ -1,11 +1,12 @@ from concurrent.futures import Executor +from ssl import SSLContext import pytest from slack_sdk import WebClient from slack_sdk.oauth.installation_store import FileInstallationStore from slack_sdk.oauth.state_store import FileOAuthStateStore -from slack_bolt import App, Say, BoltRequest +from slack_bolt import App, Say, BoltRequest, BoltContext from slack_bolt.authorization import AuthorizeResult from slack_bolt.error import BoltError from slack_bolt.oauth import OAuthFlow @@ -240,3 +241,55 @@ def test_none_body_no_middleware(self): response = app.dispatch(req) assert response.status == 404 assert response.body == '{"error": "unhandled request"}' + + def test_proxy_ssl_for_respond(self): + ssl = SSLContext() + web_client = WebClient( + token=self.valid_token, + base_url=self.mock_api_server_base_url, + proxy="http://proxy-host:9000/", + ssl=ssl, + ) + app = App( + signing_secret="valid", + client=web_client, + authorize=lambda: AuthorizeResult( + enterprise_id="E111", + team_id="T111", + ), + ) + + event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63", + "type": "app_mention", + "text": "<@W111> Hi there!", + "user": "W222", + "ts": "1595926230.009600", + "team": "T111", + "channel": "C111", + "event_ts": "1595926230.009600", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, + } + + result = {"called": False} + + @app.event("app_mention") + def handle(context: BoltContext, respond): + assert context.respond.proxy == "http://proxy-host:9000/" + assert context.respond.ssl == ssl + assert respond.proxy == "http://proxy-host:9000/" + assert respond.ssl == ssl + result["called"] = True + + req = BoltRequest(body=event_body, headers={}, mode="socket_mode") + response = app.dispatch(req) + assert response.status == 200 + assert result["called"] is True diff --git a/tests/scenario_tests_async/test_app.py b/tests/scenario_tests_async/test_app.py index 1640087bc..7486027fe 100644 --- a/tests/scenario_tests_async/test_app.py +++ b/tests/scenario_tests_async/test_app.py @@ -1,17 +1,43 @@ +import asyncio +from ssl import SSLContext + import pytest from slack_sdk import WebClient from slack_sdk.oauth.installation_store import FileInstallationStore from slack_sdk.oauth.state_store import FileOAuthStateStore +from slack_sdk.web.async_client import AsyncWebClient from slack_bolt.async_app import AsyncApp from slack_bolt.authorization import AuthorizeResult +from slack_bolt.context.async_context import AsyncBoltContext from slack_bolt.error import BoltError from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings +from slack_bolt.request.async_request import AsyncBoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) from tests.utils import remove_os_env_temporarily, restore_os_env class TestAsyncApp: + signing_secret = "secret" + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + + @pytest.fixture + def event_loop(self): + old_os_env = remove_os_env_temporarily() + try: + setup_mock_web_api_server(self) + loop = asyncio.get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server(self) + finally: + restore_os_env(old_os_env) + def setup_method(self): self.old_os_env = remove_os_env_temporarily() @@ -163,3 +189,61 @@ def test_installation_store_conflicts(self): installation_store=store1, ) assert app.installation_store is store1 + + @pytest.mark.asyncio + async def test_proxy_ssl_for_respond(self): + ssl = SSLContext() + web_client = AsyncWebClient( + token=self.valid_token, + base_url=self.mock_api_server_base_url, + proxy="http://proxy-host:9000/", + ssl=ssl, + ) + + async def my_authorize(): + return AuthorizeResult( + enterprise_id="E111", + team_id="T111", + ) + + app = AsyncApp( + signing_secret="valid", + client=web_client, + authorize=my_authorize, + ) + + event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63", + "type": "app_mention", + "text": "<@W111> Hi there!", + "user": "W222", + "ts": "1595926230.009600", + "team": "T111", + "channel": "C111", + "event_ts": "1595926230.009600", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, + } + + result = {"called": False} + + @app.event("app_mention") + async def handle(context: AsyncBoltContext, respond): + assert context.respond.proxy == "http://proxy-host:9000/" + assert context.respond.ssl == ssl + assert respond.proxy == "http://proxy-host:9000/" + assert respond.ssl == ssl + result["called"] = True + + req = AsyncBoltRequest(body=event_body, headers={}, mode="socket_mode") + response = await app.async_dispatch(req) + assert response.status == 200 + await asyncio.sleep(0.5) # wait a bit after auto ack() + assert result["called"] is True