Skip to content

Commit 010e8f8

Browse files
On-demand TlsInterception capability, driven by plugins (#965)
* Add `TlsInterceptionPropertyMixin` * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add `do_intercept` hook * call super init * No super from mixin as it is followed by abc? * type ignore * spell Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7199459 commit 010e8f8

File tree

5 files changed

+77
-13
lines changed

5 files changed

+77
-13
lines changed

proxy/http/descriptors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
from typing import Any
1112
from ..common.types import Readables, Writables, Descriptors
1213

1314

@@ -17,6 +18,10 @@ class DescriptorsHandlerMixin:
1718
include web and proxy plugins. By using DescriptorsHandlerMixin, class
1819
becomes complaint with core event loop."""
1920

21+
def __init__(self, *args: Any, **kwargs: Any) -> None:
22+
# FIXME: Required for multi-level inheritance to work
23+
super().__init__(*args, **kwargs) # type: ignore
24+
2025
# @abstractmethod
2126
async def get_descriptors(self) -> Descriptors:
2227
"""Implementations must return a list of descriptions that they wish to

proxy/http/mixins.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import argparse
12+
from typing import Any
13+
14+
15+
class TlsInterceptionPropertyMixin:
16+
"""A mixin which provides `tls_interception_enabled` property.
17+
18+
This is mostly for use by core & external developer HTTP plugins.
19+
"""
20+
21+
def __init__(self, *args: Any, **kwargs: Any) -> None:
22+
# super().__init__(*args, **kwargs)
23+
self.flags: argparse.Namespace = args[1]
24+
25+
@property
26+
def tls_interception_enabled(self) -> bool:
27+
return self.flags.ca_key_file is not None and \
28+
self.flags.ca_cert_dir is not None and \
29+
self.flags.ca_signing_key_file is not None and \
30+
self.flags.ca_cert_file is not None

proxy/http/plugin.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@
1919

2020
from .parser import HttpParser
2121
from .descriptors import DescriptorsHandlerMixin
22+
from .mixins import TlsInterceptionPropertyMixin
2223

2324
if TYPE_CHECKING:
2425
from ..core.connection import UpstreamConnectionPool
2526

2627

27-
class HttpProtocolHandlerPlugin(DescriptorsHandlerMixin, ABC):
28+
class HttpProtocolHandlerPlugin(
29+
DescriptorsHandlerMixin,
30+
TlsInterceptionPropertyMixin,
31+
ABC,
32+
):
2833
"""Base HttpProtocolHandler Plugin class.
2934
3035
NOTE: This is an internal plugin and in most cases only useful for core contributors.
@@ -55,13 +60,13 @@ def __init__(
5560
event_queue: Optional[EventQueue] = None,
5661
upstream_conn_pool: Optional['UpstreamConnectionPool'] = None,
5762
):
63+
super().__init__(uid, flags, client, event_queue, upstream_conn_pool)
5864
self.uid: str = uid
5965
self.flags: argparse.Namespace = flags
6066
self.client: TcpClientConnection = client
6167
self.request: HttpParser = request
6268
self.event_queue = event_queue
6369
self.upstream_conn_pool = upstream_conn_pool
64-
super().__init__()
6570

6671
@staticmethod
6772
@abstractmethod

proxy/http/proxy/plugin.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from abc import ABC
1414
from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
1515

16+
from ..mixins import TlsInterceptionPropertyMixin
17+
1618
from ..parser import HttpParser
1719
from ..descriptors import DescriptorsHandlerMixin
1820

@@ -23,7 +25,11 @@
2325
from ...core.connection import UpstreamConnectionPool
2426

2527

26-
class HttpProxyBasePlugin(DescriptorsHandlerMixin, ABC):
28+
class HttpProxyBasePlugin(
29+
DescriptorsHandlerMixin,
30+
TlsInterceptionPropertyMixin,
31+
ABC,
32+
):
2733
"""Base HttpProxyPlugin Plugin class.
2834
2935
Implement various lifecycle event methods to customize behavior."""
@@ -36,6 +42,7 @@ def __init__(
3642
event_queue: EventQueue,
3743
upstream_conn_pool: Optional['UpstreamConnectionPool'] = None,
3844
) -> None:
45+
super().__init__(uid, flags, client, event_queue, upstream_conn_pool)
3946
self.uid = uid # pragma: no cover
4047
self.flags = flags # pragma: no cover
4148
self.client = client # pragma: no cover
@@ -151,3 +158,15 @@ def on_access_log(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
151158
must return None to prevent other plugin.on_access_log invocation.
152159
"""
153160
return context
161+
162+
def do_intercept(self, _request: HttpParser) -> bool:
163+
"""By default returns True (only) when necessary flags
164+
for TLS interception are passed.
165+
166+
When TLS interception is enabled, plugins can still disable
167+
TLS interception by returning False explicitly. This hook
168+
will allow you to run proxy instance with TLS interception
169+
flags BUT only conditionally enable interception for
170+
certain requests.
171+
"""
172+
return self.tls_interception_enabled

proxy/http/proxy/server.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,6 @@ def __init__(
167167
def protocols() -> List[int]:
168168
return [httpProtocols.HTTP_PROXY]
169169

170-
def tls_interception_enabled(self) -> bool:
171-
return self.flags.ca_key_file is not None and \
172-
self.flags.ca_cert_dir is not None and \
173-
self.flags.ca_signing_key_file is not None and \
174-
self.flags.ca_cert_file is not None
175-
176170
async def get_descriptors(self) -> Descriptors:
177171
r: List[int] = []
178172
w: List[int] = []
@@ -291,7 +285,7 @@ async def read_from_descriptors(self, r: Readables) -> bool:
291285
# only for non-https requests and when
292286
# tls interception is enabled
293287
if not self.request.is_https_tunnel \
294-
or self.tls_interception_enabled():
288+
or self.tls_interception_enabled:
295289
if self.response.is_complete:
296290
self.handle_pipeline_response(raw)
297291
else:
@@ -440,7 +434,7 @@ def on_client_data(self, raw: memoryview) -> Optional[memoryview]:
440434
# requests is TLS interception is enabled.
441435
if self.request.is_complete and (
442436
not self.request.is_https_tunnel or
443-
self.tls_interception_enabled()
437+
self.tls_interception_enabled
444438
):
445439
if self.pipeline_request is not None and \
446440
self.pipeline_request.is_connection_upgrade:
@@ -521,8 +515,19 @@ def on_request_complete(self) -> Union[socket.socket, bool]:
521515
if self.upstream:
522516
if self.request.is_https_tunnel:
523517
self.client.queue(PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT)
524-
if self.tls_interception_enabled():
525-
return self.intercept()
518+
if self.tls_interception_enabled:
519+
# Check if any plugin wants to
520+
# disable interception even
521+
# with flags available
522+
do_intercept = True
523+
for plugin in self.plugins.values():
524+
do_intercept = plugin.do_intercept(self.request)
525+
# A plugin requested to not intercept
526+
# the request
527+
if do_intercept is False:
528+
break
529+
if do_intercept:
530+
return self.intercept()
526531
# If an upstream server connection was established for http request,
527532
# queue the request for upstream server.
528533
else:

0 commit comments

Comments
 (0)