From 375b64187b2a8b8df1ee545b32d5833468c49529 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 12 Jan 2022 00:58:16 +0530 Subject: [PATCH 1/7] Add `TlsInterceptionPropertyMixin` --- proxy/http/mixins.py | 29 +++++++++++++++++++++++++++++ proxy/http/plugin.py | 7 ++++++- proxy/http/proxy/plugin.py | 11 ++++++++++- proxy/http/proxy/server.py | 12 +++--------- 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 proxy/http/mixins.py diff --git a/proxy/http/mixins.py b/proxy/http/mixins.py new file mode 100644 index 0000000000..6831b593e2 --- /dev/null +++ b/proxy/http/mixins.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import argparse +from typing import Any + + +class TlsInterceptionPropertyMixin: + """A mixin which provides `tls_interception_enabled` property. + + This is mostly for use by core & external developer HTTP plugins. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self.flags: argparse.Namespace = args[1] + + @property + def tls_interception_enabled(self) -> bool: + return self.flags.ca_key_file is not None and \ + self.flags.ca_cert_dir is not None and \ + self.flags.ca_signing_key_file is not None and \ + self.flags.ca_cert_file is not None diff --git a/proxy/http/plugin.py b/proxy/http/plugin.py index eb9c070622..311898328e 100644 --- a/proxy/http/plugin.py +++ b/proxy/http/plugin.py @@ -19,12 +19,17 @@ from .parser import HttpParser from .descriptors import DescriptorsHandlerMixin +from .mixins import TlsInterceptionPropertyMixin if TYPE_CHECKING: from ..core.connection import UpstreamConnectionPool -class HttpProtocolHandlerPlugin(DescriptorsHandlerMixin, ABC): +class HttpProtocolHandlerPlugin( + DescriptorsHandlerMixin, + TlsInterceptionPropertyMixin, + ABC, +): """Base HttpProtocolHandler Plugin class. NOTE: This is an internal plugin and in most cases only useful for core contributors. diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py index 8e769a5105..b6389df8a0 100644 --- a/proxy/http/proxy/plugin.py +++ b/proxy/http/proxy/plugin.py @@ -13,6 +13,8 @@ from abc import ABC from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING +from ..mixins import TlsInterceptionPropertyMixin + from ..parser import HttpParser from ..descriptors import DescriptorsHandlerMixin @@ -23,7 +25,11 @@ from ...core.connection import UpstreamConnectionPool -class HttpProxyBasePlugin(DescriptorsHandlerMixin, ABC): +class HttpProxyBasePlugin( + DescriptorsHandlerMixin, + TlsInterceptionPropertyMixin, + ABC +): """Base HttpProxyPlugin Plugin class. Implement various lifecycle event methods to customize behavior.""" @@ -151,3 +157,6 @@ def on_access_log(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]: must return None to prevent other plugin.on_access_log invocation. """ return context + + def do_intercept(self) -> bool: + return self.tls_interception_enabled diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 362b9857d4..c90f5aa2dc 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -167,12 +167,6 @@ def __init__( def protocols() -> List[int]: return [httpProtocols.HTTP_PROXY] - def tls_interception_enabled(self) -> bool: - return self.flags.ca_key_file is not None and \ - self.flags.ca_cert_dir is not None and \ - self.flags.ca_signing_key_file is not None and \ - self.flags.ca_cert_file is not None - async def get_descriptors(self) -> Descriptors: r: List[int] = [] w: List[int] = [] @@ -291,7 +285,7 @@ async def read_from_descriptors(self, r: Readables) -> bool: # only for non-https requests and when # tls interception is enabled if not self.request.is_https_tunnel \ - or self.tls_interception_enabled(): + or self.tls_interception_enabled: if self.response.is_complete: self.handle_pipeline_response(raw) else: @@ -440,7 +434,7 @@ def on_client_data(self, raw: memoryview) -> Optional[memoryview]: # requests is TLS interception is enabled. if self.request.is_complete and ( not self.request.is_https_tunnel or - self.tls_interception_enabled() + self.tls_interception_enabled ): if self.pipeline_request is not None and \ self.pipeline_request.is_connection_upgrade: @@ -521,7 +515,7 @@ def on_request_complete(self) -> Union[socket.socket, bool]: if self.upstream: if self.request.is_https_tunnel: self.client.queue(PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) - if self.tls_interception_enabled(): + if self.tls_interception_enabled: return self.intercept() # If an upstream server connection was established for http request, # queue the request for upstream server. From 38dcba42f6230eead05a08ac7c4cf35e48f0e01c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 19:30:02 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- proxy/http/proxy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py index b6389df8a0..8365835360 100644 --- a/proxy/http/proxy/plugin.py +++ b/proxy/http/proxy/plugin.py @@ -28,7 +28,7 @@ class HttpProxyBasePlugin( DescriptorsHandlerMixin, TlsInterceptionPropertyMixin, - ABC + ABC, ): """Base HttpProxyPlugin Plugin class. From f58467a150674053776543d2202c7b5b95d5bc52 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 12 Jan 2022 01:13:06 +0530 Subject: [PATCH 3/7] Add `do_intercept` hook --- proxy/http/descriptors.py | 4 ++++ proxy/http/mixins.py | 1 + proxy/http/proxy/plugin.py | 10 +++++++++- proxy/http/proxy/server.py | 13 ++++++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/proxy/http/descriptors.py b/proxy/http/descriptors.py index ef73496a57..bc29bca5cf 100644 --- a/proxy/http/descriptors.py +++ b/proxy/http/descriptors.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +from typing import Any from ..common.types import Readables, Writables, Descriptors @@ -17,6 +18,9 @@ class DescriptorsHandlerMixin: include web and proxy plugins. By using DescriptorsHandlerMixin, class becomes complaint with core event loop.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # @abstractmethod async def get_descriptors(self) -> Descriptors: """Implementations must return a list of descriptions that they wish to diff --git a/proxy/http/mixins.py b/proxy/http/mixins.py index 6831b593e2..139e8e0b74 100644 --- a/proxy/http/mixins.py +++ b/proxy/http/mixins.py @@ -19,6 +19,7 @@ class TlsInterceptionPropertyMixin: """ def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) self.flags: argparse.Namespace = args[1] @property diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py index 8365835360..45255f120b 100644 --- a/proxy/http/proxy/plugin.py +++ b/proxy/http/proxy/plugin.py @@ -158,5 +158,13 @@ def on_access_log(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ return context - def do_intercept(self) -> bool: + def do_intercept(self, _request: HttpParser) -> bool: + """By default returns True (only) when necessary flags + for TLS interception are passed. + + When TLS interception is enabled, plugins can still disable + TLS interception by returning False explicitly. This hook + will allow you to run `proxy.py` with TLS interception flags + BUT conditionally enable interception for certain requests only. + """ return self.tls_interception_enabled diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index c90f5aa2dc..9e9f61947d 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -516,7 +516,18 @@ def on_request_complete(self) -> Union[socket.socket, bool]: if self.request.is_https_tunnel: self.client.queue(PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) if self.tls_interception_enabled: - return self.intercept() + # Check if any plugin wants to + # disable interception even + # with flags available + do_intercept = True + for plugin in self.plugins.values(): + do_intercept = plugin.do_intercept(self.request) + # A plugin requested to not intercept + # the request + if do_intercept is False: + break + if do_intercept: + return self.intercept() # If an upstream server connection was established for http request, # queue the request for upstream server. else: From 284b4e0db0fab936eb8233859546dcaa583e2034 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 12 Jan 2022 01:24:04 +0530 Subject: [PATCH 4/7] call super init --- proxy/http/plugin.py | 2 +- proxy/http/proxy/plugin.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/proxy/http/plugin.py b/proxy/http/plugin.py index 311898328e..2eb5ec5414 100644 --- a/proxy/http/plugin.py +++ b/proxy/http/plugin.py @@ -60,13 +60,13 @@ def __init__( event_queue: Optional[EventQueue] = None, upstream_conn_pool: Optional['UpstreamConnectionPool'] = None, ): + super().__init__(uid, flags, client, event_queue, upstream_conn_pool) self.uid: str = uid self.flags: argparse.Namespace = flags self.client: TcpClientConnection = client self.request: HttpParser = request self.event_queue = event_queue self.upstream_conn_pool = upstream_conn_pool - super().__init__() @staticmethod @abstractmethod diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py index 45255f120b..ade40f7187 100644 --- a/proxy/http/proxy/plugin.py +++ b/proxy/http/proxy/plugin.py @@ -42,6 +42,7 @@ def __init__( event_queue: EventQueue, upstream_conn_pool: Optional['UpstreamConnectionPool'] = None, ) -> None: + super().__init__(uid, flags, client, event_queue, upstream_conn_pool) self.uid = uid # pragma: no cover self.flags = flags # pragma: no cover self.client = client # pragma: no cover @@ -164,7 +165,8 @@ def do_intercept(self, _request: HttpParser) -> bool: When TLS interception is enabled, plugins can still disable TLS interception by returning False explicitly. This hook - will allow you to run `proxy.py` with TLS interception flags - BUT conditionally enable interception for certain requests only. + will allow you to run proxy instance with TLS interception + flags BUT only conditionally enable interception for + certain requests. """ return self.tls_interception_enabled From 1aa8a421a51b148af0e8e5168430f2c17340052b Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 12 Jan 2022 01:34:25 +0530 Subject: [PATCH 5/7] No super from mixin as it is followed by abc? --- proxy/http/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/http/mixins.py b/proxy/http/mixins.py index 139e8e0b74..073c687b87 100644 --- a/proxy/http/mixins.py +++ b/proxy/http/mixins.py @@ -19,7 +19,7 @@ class TlsInterceptionPropertyMixin: """ def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + # super().__init__(*args, **kwargs) self.flags: argparse.Namespace = args[1] @property From af292d19c2a114b1d63956600eb33051199bd199 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 12 Jan 2022 01:44:41 +0530 Subject: [PATCH 6/7] type ignore --- proxy/http/descriptors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/http/descriptors.py b/proxy/http/descriptors.py index bc29bca5cf..de5fd40780 100644 --- a/proxy/http/descriptors.py +++ b/proxy/http/descriptors.py @@ -19,7 +19,8 @@ class DescriptorsHandlerMixin: becomes complaint with core event loop.""" def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + # FIXME: Required for multi-level inheritence to work + super().__init__(*args, **kwargs) # type: ignore # @abstractmethod async def get_descriptors(self) -> Descriptors: From 1978390aa91afb0240ee41b7ec9a7f54990fc17b Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 12 Jan 2022 02:40:17 +0530 Subject: [PATCH 7/7] spell --- proxy/http/descriptors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/http/descriptors.py b/proxy/http/descriptors.py index de5fd40780..bae5ea3857 100644 --- a/proxy/http/descriptors.py +++ b/proxy/http/descriptors.py @@ -19,7 +19,7 @@ class DescriptorsHandlerMixin: becomes complaint with core event loop.""" def __init__(self, *args: Any, **kwargs: Any) -> None: - # FIXME: Required for multi-level inheritence to work + # FIXME: Required for multi-level inheritance to work super().__init__(*args, **kwargs) # type: ignore # @abstractmethod