diff --git a/menubar/proxy.py.xcodeproj/project.pbxproj b/menubar/proxy.py.xcodeproj/project.pbxproj index 90e415dc..9e6b47c5 100644 --- a/menubar/proxy.py.xcodeproj/project.pbxproj +++ b/menubar/proxy.py.xcodeproj/project.pbxproj @@ -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\""; diff --git a/menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate b/menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..f0463297 Binary files /dev/null and b/menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist b/menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..f806210a --- /dev/null +++ b/menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + proxy.py.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/menubar/proxy.py/AppDelegate.swift b/menubar/proxy.py/AppDelegate.swift index f4ac4988..bac63c5c 100644 --- a/menubar/proxy.py/AppDelegate.swift +++ b/menubar/proxy.py/AppDelegate.swift @@ -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@*/ + } +} diff --git a/menubar/proxy.py/ContentView.swift b/menubar/proxy.py/ContentView.swift index 52fee636..8cf29f44 100644 --- a/menubar/proxy.py/ContentView.swift +++ b/menubar/proxy.py/ContentView.swift @@ -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 diff --git a/proxy/common/flags.py b/proxy/common/flags.py index 182c2c35..26b44709 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -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' % diff --git a/proxy/http/handler.py b/proxy/http/handler.py index 9c7dd90c..d5538a26 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -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 ``. - - 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. diff --git a/proxy/http/plugin.py b/proxy/http/plugin.py new file mode 100644 index 00000000..86232ca5 --- /dev/null +++ b/proxy/http/plugin.py @@ -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 ``. + + 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 diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 5ce2948d..0396bf04 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -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) diff --git a/proxy/http/server/web.py b/proxy/http/server/web.py index b1b9475e..ea53756d 100644 --- a/proxy/http/server/web.py +++ b/proxy/http/server/web.py @@ -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 diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py index 7799c1d5..dac97cd6 100644 --- a/tests/http/test_http_proxy_tls_interception.py +++ b/tests/http/test_http_proxy_tls_interception.py @@ -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(