diff --git a/proxy/http/descriptors.py b/proxy/http/descriptors.py index ef73496a57..bae5ea3857 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,10 @@ 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: + # FIXME: Required for multi-level inheritance to work + super().__init__(*args, **kwargs) # type: ignore + # @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 new file mode 100644 index 0000000000..073c687b87 --- /dev/null +++ b/proxy/http/mixins.py @@ -0,0 +1,30 @@ +# -*- 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: + # super().__init__(*args, **kwargs) + 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..2eb5ec5414 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. @@ -55,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 8e769a5105..ade40f7187 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.""" @@ -36,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 @@ -151,3 +158,15 @@ 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, _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 instance with TLS interception + flags BUT only conditionally enable interception for + certain requests. + """ + return self.tls_interception_enabled diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 362b9857d4..9e9f61947d 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,8 +515,19 @@ 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(): - return self.intercept() + if self.tls_interception_enabled: + # 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: