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
This commit is contained in:
Abhinav Singh 2020-07-04 18:17:11 +05:30 committed by GitHub
parent ea227b1cdf
commit 1b0ed923d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 92 deletions

View File

@ -198,7 +198,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1120;
LastUpgradeCheck = 1150;
ORGANIZATIONNAME = "Abhinav Singh";
TargetAttributes = {
AD1F92A2238864240088A917 = {
@ -432,6 +432,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = proxy.py/proxy_py.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_ASSET_PATHS = "\"proxy.py/Preview Content\"";
@ -455,6 +456,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = proxy.py/proxy_py.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_ASSET_PATHS = "\"proxy.py/Preview Content\"";

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>proxy.py.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -3,7 +3,8 @@
// proxy.py
//
// Created by Abhinav Singh on 11/22/19.
// Copyright © 2019 Abhinav Singh. All rights reserved.
// Copyright © 2013-present by Abhinav Singh and contributors.
// All rights reserved.
//
import Cocoa
@ -41,3 +42,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
statusItem.menu = menu
}
}
struct AppDelegate_Previews: PreviewProvider {
static var previews: some View {
/*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
}
}

View File

@ -3,7 +3,8 @@
// proxy.py
//
// Created by Abhinav Singh on 11/22/19.
// Copyright © 2019 Abhinav Singh. All rights reserved.
// Copyright © 2013-present by Abhinav Singh and contributors.
// All rights reserved.
//
import SwiftUI

View File

@ -145,7 +145,9 @@ class Flags:
'A future version of pip will drop support for Python 2.7.')
sys.exit(1)
# Initialize core flags.
parser = Flags.init_parser()
# Parse flags
args = parser.parse_args(input_args)
# Print version and exit
@ -159,6 +161,7 @@ class Flags:
# Setup limits
Flags.set_open_file_limit(args.open_file_limit)
# Prepare list of plugins to load based upon --enable-* and --disable-* flags
default_plugins: List[Tuple[str, bool]] = []
if args.enable_dashboard:
default_plugins.append((PLUGIN_WEB_SERVER, True))
@ -179,6 +182,7 @@ class Flags:
if args.pac_file is not None:
default_plugins.append((PLUGIN_PAC_FILE, True))
# Load default plugins along with user provided --plugins
plugins = Flags.load_plugins(
bytes_(
'%s,%s' %

View File

@ -15,9 +15,11 @@ import time
import contextlib
import errno
import logging
from abc import ABC, abstractmethod
from typing import Tuple, List, Union, Optional, Generator, Dict
from uuid import UUID
from .plugin import HttpProtocolHandlerPlugin
from .parser import HttpParser, httpParserStates, httpParserTypes
from .exception import HttpProtocolException
@ -30,83 +32,6 @@ from ..core.connection import TcpClientConnection
logger = logging.getLogger(__name__)
class HttpProtocolHandlerPlugin(ABC):
"""Base HttpProtocolHandler Plugin class.
NOTE: This is an internal plugin and in most cases only useful for core contributors.
If you are looking for proxy server plugins see `<proxy.HttpProxyBasePlugin>`.
Implements various lifecycle events for an accepted client connection.
Following events are of interest:
1. Client Connection Accepted
A new plugin instance is created per accepted client connection.
Add your logic within __init__ constructor for any per connection setup.
2. Client Request Chunk Received
on_client_data is called for every chunk of data sent by the client.
3. Client Request Complete
on_request_complete is called once client request has completed.
4. Server Response Chunk Received
on_response_chunk is called for every chunk received from the server.
5. Client Connection Closed
Add your logic within `on_client_connection_close` for any per connection teardown.
"""
def __init__(
self,
uid: UUID,
flags: Flags,
client: TcpClientConnection,
request: HttpParser,
event_queue: EventQueue):
self.uid: UUID = uid
self.flags: Flags = flags
self.client: TcpClientConnection = client
self.request: HttpParser = request
self.event_queue = event_queue
super().__init__()
def name(self) -> str:
"""A unique name for your plugin.
Defaults to name of the class. This helps plugin developers to directly
access a specific plugin by its name."""
return self.__class__.__name__
@abstractmethod
def get_descriptors(
self) -> Tuple[List[socket.socket], List[socket.socket]]:
return [], [] # pragma: no cover
@abstractmethod
def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool:
return False # pragma: no cover
@abstractmethod
def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool:
return False # pragma: no cover
@abstractmethod
def on_client_data(self, raw: memoryview) -> Optional[memoryview]:
return raw # pragma: no cover
@abstractmethod
def on_request_complete(self) -> Union[socket.socket, bool]:
"""Called right after client request parser has reached COMPLETE state."""
return False # pragma: no cover
@abstractmethod
def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]:
"""Handle data chunks as received from the server.
Return optionally modified chunk to return back to client."""
return chunk # pragma: no cover
@abstractmethod
def on_client_connection_close(self) -> None:
pass # pragma: no cover
class HttpProtocolHandler(ThreadlessWork):
"""HTTP, HTTPS, HTTP2, WebSockets protocol handler.

99
proxy/http/plugin.py Normal file
View File

@ -0,0 +1,99 @@
# -*- 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 socket
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Tuple, List, Union, Optional
from .parser import HttpParser
from ..common.flags import Flags
from ..common.types import HasFileno
from ..core.event import EventQueue
from ..core.connection import TcpClientConnection
class HttpProtocolHandlerPlugin(ABC):
"""Base HttpProtocolHandler Plugin class.
NOTE: This is an internal plugin and in most cases only useful for core contributors.
If you are looking for proxy server plugins see `<proxy.HttpProxyBasePlugin>`.
Implements various lifecycle events for an accepted client connection.
Following events are of interest:
1. Client Connection Accepted
A new plugin instance is created per accepted client connection.
Add your logic within __init__ constructor for any per connection setup.
2. Client Request Chunk Received
on_client_data is called for every chunk of data sent by the client.
3. Client Request Complete
on_request_complete is called once client request has completed.
4. Server Response Chunk Received
on_response_chunk is called for every chunk received from the server.
5. Client Connection Closed
Add your logic within `on_client_connection_close` for any per connection teardown.
"""
def __init__(
self,
uid: UUID,
flags: Flags,
client: TcpClientConnection,
request: HttpParser,
event_queue: EventQueue):
self.uid: UUID = uid
self.flags: Flags = flags
self.client: TcpClientConnection = client
self.request: HttpParser = request
self.event_queue = event_queue
super().__init__()
def name(self) -> str:
"""A unique name for your plugin.
Defaults to name of the class. This helps plugin developers to directly
access a specific plugin by its name."""
return self.__class__.__name__
@abstractmethod
def get_descriptors(
self) -> Tuple[List[socket.socket], List[socket.socket]]:
return [], [] # pragma: no cover
@abstractmethod
def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool:
return False # pragma: no cover
@abstractmethod
def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool:
return False # pragma: no cover
@abstractmethod
def on_client_data(self, raw: memoryview) -> Optional[memoryview]:
return raw # pragma: no cover
@abstractmethod
def on_request_complete(self) -> Union[socket.socket, bool]:
"""Called right after client request parser has reached COMPLETE state."""
return False # pragma: no cover
@abstractmethod
def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]:
"""Handle data chunks as received from the server.
Return optionally modified chunk to return back to client."""
return chunk # pragma: no cover
@abstractmethod
def on_client_connection_close(self) -> None:
pass # pragma: no cover

View File

@ -10,6 +10,7 @@
"""
import logging
import threading
import subprocess
import os
import ssl
import socket
@ -18,7 +19,7 @@ import errno
from typing import Optional, List, Union, Dict, cast, Any, Tuple
from .plugin import HttpProxyBasePlugin
from ..handler import HttpProtocolHandlerPlugin
from ..plugin import HttpProtocolHandlerPlugin
from ..exception import HttpProtocolException, ProxyConnectionFailed, ProxyAuthenticationFailed
from ..codes import httpStatusCodes
from ..parser import HttpParser, httpParserStates, httpParserTypes
@ -287,6 +288,9 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
# wrap_client also flushes client data before wrapping
# sending to client can raise, handle expected exceptions
self.wrap_client()
except subprocess.TimeoutExpired as e: # Popen communicate timeout
logger.exception('TimeoutExpired during certificate generation', exc_info=e)
return True
except BrokenPipeError:
logger.error(
'BrokenPipeError when wrapping client')
@ -372,13 +376,19 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
'{0}.{1}'.format(text_(self.request.host), 'pub'))
private_key_path = self.flags.ca_signing_key_file
private_key_password = ''
subject = '/CN={0}/C={1}/ST={2}/L={3}/O={4}/OU={5}'.format(
upstream_subject.get('commonName', text_(self.request.host)),
upstream_subject.get('countryName', 'NA'),
upstream_subject.get('stateOrProvinceName', 'Unavailable'),
upstream_subject.get('localityName', 'Unavailable'),
upstream_subject.get('organizationName', 'Unavailable'),
upstream_subject.get('organizationalUnitName', 'Unavailable'))
# Build certificate subject
keys = {
'CN': 'commonName',
'C': 'countryName',
'ST': 'stateOrProvinceName',
'L': 'localityName',
'O': 'organizationName',
'OU': 'organizationalUnitName',
}
subject = ''
for key in keys:
if upstream_subject.get(keys[key], None):
subject += '/{0}={1}'.format(key, upstream_subject.get(keys[key]))
alt_subj_names = [text_(self.request.host), ]
validity_in_days = 365 * 2
timeout = 10
@ -458,9 +468,10 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
self.client._conn = ssl.wrap_socket(
self.client.connection,
server_side=True,
# ca_certs=self.flags.ca_cert_file,
certfile=generated_cert,
keyfile=self.flags.ca_signing_key_file,
ssl_version=ssl.PROTOCOL_TLSv1_2)
ssl_version=ssl.PROTOCOL_TLS)
self.client.connection.setblocking(False)
logger.debug(
'TLS interception using %s', generated_cert)

View File

@ -23,7 +23,7 @@ from ..exception import HttpProtocolException
from ..websocket import WebsocketFrame, websocketOpcodes
from ..codes import httpStatusCodes
from ..parser import HttpParser, httpParserStates, httpParserTypes
from ..handler import HttpProtocolHandlerPlugin
from ..plugin import HttpProtocolHandlerPlugin
from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response
from ...common.constants import PROXY_AGENT_HEADER_VALUE

View File

@ -169,7 +169,7 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
keyfile=self.flags.ca_signing_key_file,
certfile=HttpProxyPlugin.generated_cert_file_path(
self.flags.ca_cert_dir, host),
ssl_version=ssl.PROTOCOL_TLSv1_2
ssl_version=ssl.PROTOCOL_TLS
)
self.assertEqual(self._conn.setblocking.call_count, 2)
self.assertEqual(