Skip to content

Commit 1b0ed92

Browse files
authored
Refactor plugin base classes for plugin specific flags (#388)
* Update to latest code signing recommendations * Move HttpProtocolHandlerPlugin into separate file * Dont add subject attributes if not provided by upstream. Also handle subprocess.TimeoutExpired raised during certificate generation. Instead of retries, we simply close the connection on timeout * Remove plugin specific flag initialization methods for now
1 parent ea227b1 commit 1b0ed92

File tree

11 files changed

+155
-92
lines changed

11 files changed

+155
-92
lines changed

menubar/proxy.py.xcodeproj/project.pbxproj

+3-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@
198198
isa = PBXProject;
199199
attributes = {
200200
LastSwiftUpdateCheck = 1120;
201-
LastUpgradeCheck = 1120;
201+
LastUpgradeCheck = 1150;
202202
ORGANIZATIONNAME = "Abhinav Singh";
203203
TargetAttributes = {
204204
AD1F92A2238864240088A917 = {
@@ -432,6 +432,7 @@
432432
buildSettings = {
433433
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
434434
CODE_SIGN_ENTITLEMENTS = proxy.py/proxy_py.entitlements;
435+
CODE_SIGN_IDENTITY = "-";
435436
CODE_SIGN_STYLE = Automatic;
436437
COMBINE_HIDPI_IMAGES = YES;
437438
DEVELOPMENT_ASSET_PATHS = "\"proxy.py/Preview Content\"";
@@ -455,6 +456,7 @@
455456
buildSettings = {
456457
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
457458
CODE_SIGN_ENTITLEMENTS = proxy.py/proxy_py.entitlements;
459+
CODE_SIGN_IDENTITY = "-";
458460
CODE_SIGN_STYLE = Automatic;
459461
COMBINE_HIDPI_IMAGES = YES;
460462
DEVELOPMENT_ASSET_PATHS = "\"proxy.py/Preview Content\"";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>proxy.py.xcscheme_^#shared#^_</key>
8+
<dict>
9+
<key>orderHint</key>
10+
<integer>0</integer>
11+
</dict>
12+
</dict>
13+
</dict>
14+
</plist>

menubar/proxy.py/AppDelegate.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// proxy.py
44
//
55
// Created by Abhinav Singh on 11/22/19.
6-
// Copyright © 2019 Abhinav Singh. All rights reserved.
6+
// Copyright © 2013-present by Abhinav Singh and contributors.
7+
// All rights reserved.
78
//
89

910
import Cocoa
@@ -41,3 +42,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4142
statusItem.menu = menu
4243
}
4344
}
45+
46+
struct AppDelegate_Previews: PreviewProvider {
47+
static var previews: some View {
48+
/*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
49+
}
50+
}

menubar/proxy.py/ContentView.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// proxy.py
44
//
55
// Created by Abhinav Singh on 11/22/19.
6-
// Copyright © 2019 Abhinav Singh. All rights reserved.
6+
// Copyright © 2013-present by Abhinav Singh and contributors.
7+
// All rights reserved.
78
//
89

910
import SwiftUI

proxy/common/flags.py

+4
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ def initialize(
145145
'A future version of pip will drop support for Python 2.7.')
146146
sys.exit(1)
147147

148+
# Initialize core flags.
148149
parser = Flags.init_parser()
150+
# Parse flags
149151
args = parser.parse_args(input_args)
150152

151153
# Print version and exit
@@ -159,6 +161,7 @@ def initialize(
159161
# Setup limits
160162
Flags.set_open_file_limit(args.open_file_limit)
161163

164+
# Prepare list of plugins to load based upon --enable-* and --disable-* flags
162165
default_plugins: List[Tuple[str, bool]] = []
163166
if args.enable_dashboard:
164167
default_plugins.append((PLUGIN_WEB_SERVER, True))
@@ -179,6 +182,7 @@ def initialize(
179182
if args.pac_file is not None:
180183
default_plugins.append((PLUGIN_PAC_FILE, True))
181184

185+
# Load default plugins along with user provided --plugins
182186
plugins = Flags.load_plugins(
183187
bytes_(
184188
'%s,%s' %

proxy/http/handler.py

+3-78
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
import contextlib
1616
import errno
1717
import logging
18-
from abc import ABC, abstractmethod
18+
1919
from typing import Tuple, List, Union, Optional, Generator, Dict
2020
from uuid import UUID
21+
22+
from .plugin import HttpProtocolHandlerPlugin
2123
from .parser import HttpParser, httpParserStates, httpParserTypes
2224
from .exception import HttpProtocolException
2325

@@ -30,83 +32,6 @@
3032
logger = logging.getLogger(__name__)
3133

3234

33-
class HttpProtocolHandlerPlugin(ABC):
34-
"""Base HttpProtocolHandler Plugin class.
35-
36-
NOTE: This is an internal plugin and in most cases only useful for core contributors.
37-
If you are looking for proxy server plugins see `<proxy.HttpProxyBasePlugin>`.
38-
39-
Implements various lifecycle events for an accepted client connection.
40-
Following events are of interest:
41-
42-
1. Client Connection Accepted
43-
A new plugin instance is created per accepted client connection.
44-
Add your logic within __init__ constructor for any per connection setup.
45-
2. Client Request Chunk Received
46-
on_client_data is called for every chunk of data sent by the client.
47-
3. Client Request Complete
48-
on_request_complete is called once client request has completed.
49-
4. Server Response Chunk Received
50-
on_response_chunk is called for every chunk received from the server.
51-
5. Client Connection Closed
52-
Add your logic within `on_client_connection_close` for any per connection teardown.
53-
"""
54-
55-
def __init__(
56-
self,
57-
uid: UUID,
58-
flags: Flags,
59-
client: TcpClientConnection,
60-
request: HttpParser,
61-
event_queue: EventQueue):
62-
self.uid: UUID = uid
63-
self.flags: Flags = flags
64-
self.client: TcpClientConnection = client
65-
self.request: HttpParser = request
66-
self.event_queue = event_queue
67-
super().__init__()
68-
69-
def name(self) -> str:
70-
"""A unique name for your plugin.
71-
72-
Defaults to name of the class. This helps plugin developers to directly
73-
access a specific plugin by its name."""
74-
return self.__class__.__name__
75-
76-
@abstractmethod
77-
def get_descriptors(
78-
self) -> Tuple[List[socket.socket], List[socket.socket]]:
79-
return [], [] # pragma: no cover
80-
81-
@abstractmethod
82-
def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool:
83-
return False # pragma: no cover
84-
85-
@abstractmethod
86-
def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool:
87-
return False # pragma: no cover
88-
89-
@abstractmethod
90-
def on_client_data(self, raw: memoryview) -> Optional[memoryview]:
91-
return raw # pragma: no cover
92-
93-
@abstractmethod
94-
def on_request_complete(self) -> Union[socket.socket, bool]:
95-
"""Called right after client request parser has reached COMPLETE state."""
96-
return False # pragma: no cover
97-
98-
@abstractmethod
99-
def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]:
100-
"""Handle data chunks as received from the server.
101-
102-
Return optionally modified chunk to return back to client."""
103-
return chunk # pragma: no cover
104-
105-
@abstractmethod
106-
def on_client_connection_close(self) -> None:
107-
pass # pragma: no cover
108-
109-
11035
class HttpProtocolHandler(ThreadlessWork):
11136
"""HTTP, HTTPS, HTTP2, WebSockets protocol handler.
11237

proxy/http/plugin.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 socket
12+
13+
from abc import ABC, abstractmethod
14+
from uuid import UUID
15+
from typing import Tuple, List, Union, Optional
16+
17+
from .parser import HttpParser
18+
19+
from ..common.flags import Flags
20+
from ..common.types import HasFileno
21+
from ..core.event import EventQueue
22+
from ..core.connection import TcpClientConnection
23+
24+
25+
class HttpProtocolHandlerPlugin(ABC):
26+
"""Base HttpProtocolHandler Plugin class.
27+
28+
NOTE: This is an internal plugin and in most cases only useful for core contributors.
29+
If you are looking for proxy server plugins see `<proxy.HttpProxyBasePlugin>`.
30+
31+
Implements various lifecycle events for an accepted client connection.
32+
Following events are of interest:
33+
34+
1. Client Connection Accepted
35+
A new plugin instance is created per accepted client connection.
36+
Add your logic within __init__ constructor for any per connection setup.
37+
2. Client Request Chunk Received
38+
on_client_data is called for every chunk of data sent by the client.
39+
3. Client Request Complete
40+
on_request_complete is called once client request has completed.
41+
4. Server Response Chunk Received
42+
on_response_chunk is called for every chunk received from the server.
43+
5. Client Connection Closed
44+
Add your logic within `on_client_connection_close` for any per connection teardown.
45+
"""
46+
47+
def __init__(
48+
self,
49+
uid: UUID,
50+
flags: Flags,
51+
client: TcpClientConnection,
52+
request: HttpParser,
53+
event_queue: EventQueue):
54+
self.uid: UUID = uid
55+
self.flags: Flags = flags
56+
self.client: TcpClientConnection = client
57+
self.request: HttpParser = request
58+
self.event_queue = event_queue
59+
super().__init__()
60+
61+
def name(self) -> str:
62+
"""A unique name for your plugin.
63+
64+
Defaults to name of the class. This helps plugin developers to directly
65+
access a specific plugin by its name."""
66+
return self.__class__.__name__
67+
68+
@abstractmethod
69+
def get_descriptors(
70+
self) -> Tuple[List[socket.socket], List[socket.socket]]:
71+
return [], [] # pragma: no cover
72+
73+
@abstractmethod
74+
def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool:
75+
return False # pragma: no cover
76+
77+
@abstractmethod
78+
def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool:
79+
return False # pragma: no cover
80+
81+
@abstractmethod
82+
def on_client_data(self, raw: memoryview) -> Optional[memoryview]:
83+
return raw # pragma: no cover
84+
85+
@abstractmethod
86+
def on_request_complete(self) -> Union[socket.socket, bool]:
87+
"""Called right after client request parser has reached COMPLETE state."""
88+
return False # pragma: no cover
89+
90+
@abstractmethod
91+
def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]:
92+
"""Handle data chunks as received from the server.
93+
94+
Return optionally modified chunk to return back to client."""
95+
return chunk # pragma: no cover
96+
97+
@abstractmethod
98+
def on_client_connection_close(self) -> None:
99+
pass # pragma: no cover

proxy/http/proxy/server.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"""
1111
import logging
1212
import threading
13+
import subprocess
1314
import os
1415
import ssl
1516
import socket
@@ -18,7 +19,7 @@
1819
from typing import Optional, List, Union, Dict, cast, Any, Tuple
1920

2021
from .plugin import HttpProxyBasePlugin
21-
from ..handler import HttpProtocolHandlerPlugin
22+
from ..plugin import HttpProtocolHandlerPlugin
2223
from ..exception import HttpProtocolException, ProxyConnectionFailed, ProxyAuthenticationFailed
2324
from ..codes import httpStatusCodes
2425
from ..parser import HttpParser, httpParserStates, httpParserTypes
@@ -287,6 +288,9 @@ def on_request_complete(self) -> Union[socket.socket, bool]:
287288
# wrap_client also flushes client data before wrapping
288289
# sending to client can raise, handle expected exceptions
289290
self.wrap_client()
291+
except subprocess.TimeoutExpired as e: # Popen communicate timeout
292+
logger.exception('TimeoutExpired during certificate generation', exc_info=e)
293+
return True
290294
except BrokenPipeError:
291295
logger.error(
292296
'BrokenPipeError when wrapping client')
@@ -372,13 +376,19 @@ def gen_ca_signed_certificate(self, cert_file_path: str, certificate: Dict[str,
372376
'{0}.{1}'.format(text_(self.request.host), 'pub'))
373377
private_key_path = self.flags.ca_signing_key_file
374378
private_key_password = ''
375-
subject = '/CN={0}/C={1}/ST={2}/L={3}/O={4}/OU={5}'.format(
376-
upstream_subject.get('commonName', text_(self.request.host)),
377-
upstream_subject.get('countryName', 'NA'),
378-
upstream_subject.get('stateOrProvinceName', 'Unavailable'),
379-
upstream_subject.get('localityName', 'Unavailable'),
380-
upstream_subject.get('organizationName', 'Unavailable'),
381-
upstream_subject.get('organizationalUnitName', 'Unavailable'))
379+
# Build certificate subject
380+
keys = {
381+
'CN': 'commonName',
382+
'C': 'countryName',
383+
'ST': 'stateOrProvinceName',
384+
'L': 'localityName',
385+
'O': 'organizationName',
386+
'OU': 'organizationalUnitName',
387+
}
388+
subject = ''
389+
for key in keys:
390+
if upstream_subject.get(keys[key], None):
391+
subject += '/{0}={1}'.format(key, upstream_subject.get(keys[key]))
382392
alt_subj_names = [text_(self.request.host), ]
383393
validity_in_days = 365 * 2
384394
timeout = 10
@@ -458,9 +468,10 @@ def wrap_client(self) -> None:
458468
self.client._conn = ssl.wrap_socket(
459469
self.client.connection,
460470
server_side=True,
471+
# ca_certs=self.flags.ca_cert_file,
461472
certfile=generated_cert,
462473
keyfile=self.flags.ca_signing_key_file,
463-
ssl_version=ssl.PROTOCOL_TLSv1_2)
474+
ssl_version=ssl.PROTOCOL_TLS)
464475
self.client.connection.setblocking(False)
465476
logger.debug(
466477
'TLS interception using %s', generated_cert)

proxy/http/server/web.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from ..websocket import WebsocketFrame, websocketOpcodes
2424
from ..codes import httpStatusCodes
2525
from ..parser import HttpParser, httpParserStates, httpParserTypes
26-
from ..handler import HttpProtocolHandlerPlugin
26+
from ..plugin import HttpProtocolHandlerPlugin
2727

2828
from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response
2929
from ...common.constants import PROXY_AGENT_HEADER_VALUE

tests/http/test_http_proxy_tls_interception.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def mock_connection() -> Any:
169169
keyfile=self.flags.ca_signing_key_file,
170170
certfile=HttpProxyPlugin.generated_cert_file_path(
171171
self.flags.ca_cert_dir, host),
172-
ssl_version=ssl.PROTOCOL_TLSv1_2
172+
ssl_version=ssl.PROTOCOL_TLS
173173
)
174174
self.assertEqual(self._conn.setblocking.call_count, 2)
175175
self.assertEqual(

0 commit comments

Comments
 (0)