From 568da5fb3e4f4b244028d4f27c7b1311bd2de30c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sat, 20 Jun 2020 16:00:39 +0200 Subject: [PATCH 01/79] Update mypy from 0.780 to 0.781 (#379) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 11e155094..4d758325d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -4,7 +4,7 @@ flake8==3.8.3 pytest==5.4.3 pytest-cov==2.10.0 autopep8==1.5.3 -mypy==0.780 +mypy==0.781 py-spy==0.3.3 codecov==2.1.7 tox==3.15.2 From 4117c5f0d5875e56bb3982d143892b4cf08f4175 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 21 Jun 2020 21:57:03 +0530 Subject: [PATCH 02/79] Add FilterByClientIpPlugin example (#381) --- dashboard/src/proxy.css | 2 +- proxy/common/version.py | 2 +- proxy/plugin/__init__.py | 2 ++ proxy/plugin/filter_by_client_ip.py | 43 +++++++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 proxy/plugin/filter_by_client_ip.py diff --git a/dashboard/src/proxy.css b/dashboard/src/proxy.css index e109ecfc4..36126a625 100644 --- a/dashboard/src/proxy.css +++ b/dashboard/src/proxy.css @@ -15,7 +15,7 @@ html, body { } main { - height: 88%; + height: 89.75%; } section { diff --git a/proxy/common/version.py b/proxy/common/version.py index e8af26932..d164acb27 100644 --- a/proxy/common/version.py +++ b/proxy/common/version.py @@ -8,5 +8,5 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -VERSION = (2, 2, 0) +VERSION = (2, 2, 1) __version__ = '.'.join(map(str, VERSION[0:3])) diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index 1ef0af91f..035e6f0cd 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -18,6 +18,7 @@ from .web_server_route import WebServerPlugin from .reverse_proxy import ReverseProxyPlugin from .proxy_pool import ProxyPoolPlugin +from .filter_by_client_ip import FilterByClientIpPlugin __all__ = [ 'CacheResponsesPlugin', @@ -31,4 +32,5 @@ 'WebServerPlugin', 'ReverseProxyPlugin', 'ProxyPoolPlugin', + 'FilterByClientIpPlugin', ] diff --git a/proxy/plugin/filter_by_client_ip.py b/proxy/plugin/filter_by_client_ip.py new file mode 100644 index 000000000..41ce7da2f --- /dev/null +++ b/proxy/plugin/filter_by_client_ip.py @@ -0,0 +1,43 @@ +# -*- 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. +""" +from typing import Optional + +from ..http.exception import HttpRequestRejected +from ..http.parser import HttpParser +from ..http.codes import httpStatusCodes +from ..http.proxy import HttpProxyBasePlugin + + +class FilterByClientIpPlugin(HttpProxyBasePlugin): + """Drop traffic by inspecting incoming client IP address.""" + + FILTERED_IPS = ['127.0.0.1', '::1'] + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + if self.client.addr[0] in self.FILTERED_IPS: + raise HttpRequestRejected( + status_code=httpStatusCodes.I_AM_A_TEAPOT, reason=b'I\'m a tea pot', + headers={ + b'Connection': b'close', + } + ) + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass diff --git a/setup.py b/setup.py index 0d2c67516..8570fb295 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ """ from setuptools import setup, find_packages -VERSION = (2, 2, 0) +VERSION = (2, 2, 1) __version__ = '.'.join(map(str, VERSION[0:3])) __description__ = '''⚡⚡⚡Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on Network monitoring, controls & Application development, testing, debugging.''' From b0d16572ca59365c89262b1e777ebb87d444b597 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 23 Jun 2020 12:59:57 +0200 Subject: [PATCH 03/79] Update mypy from 0.781 to 0.782 (#382) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 4d758325d..730dad7a8 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -4,7 +4,7 @@ flake8==3.8.3 pytest==5.4.3 pytest-cov==2.10.0 autopep8==1.5.3 -mypy==0.781 +mypy==0.782 py-spy==0.3.3 codecov==2.1.7 tox==3.15.2 From 19fb8577d6cefa18c09d4b84d5dc853680e49daa Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 24 Jun 2020 12:32:15 +0200 Subject: [PATCH 04/79] Update twine from 3.1.1 to 3.2.0 (#384) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index 8c2c116de..929a00571 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ -twine==3.1.1 +twine==3.2.0 wheel==0.34.2 From 0a1b7bf0029d61c64484f16c7175f7522415438e Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 28 Jun 2020 11:35:49 +0200 Subject: [PATCH 05/79] Update tox from 3.15.2 to 3.16.0 (#385) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 730dad7a8..5214b7773 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.7 -tox==3.15.2 +tox==3.16.0 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From 167afbfe886a793b9d0297d620e7fd2474d0aa4e Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 29 Jun 2020 23:16:32 +0200 Subject: [PATCH 06/79] Update tox from 3.16.0 to 3.16.1 (#386) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 5214b7773..9658f5a0c 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.7 -tox==3.16.0 +tox==3.16.1 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From ea227b1cdf8befa5d1228ae17b20e715986e2a4c Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Thu, 2 Jul 2020 03:30:29 +0530 Subject: [PATCH 07/79] Document FilterByClientIpPlugin & ModifyChunkResponsePlugin (#387) --- README.md | 53 ++++++++++++++++++++ proxy/http/parser.py | 15 +++++- proxy/plugin/__init__.py | 2 + proxy/plugin/modify_chunk_response_plugin.py | 51 +++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 proxy/plugin/modify_chunk_response_plugin.py diff --git a/README.md b/README.md index ed6f8b043..3e2faa62e 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ Table of Contents * [Cache Responses Plugin](#cacheresponsesplugin) * [Man-In-The-Middle Plugin](#maninthemiddleplugin) * [Proxy Pool Plugin](#proxypoolplugin) + * [FilterByClientIpPlugin](#filterbyclientipplugin) + * [ModifyChunkResponsePlugin](#modifychunkresponseplugin) * [HTTP Web Server Plugins](#http-web-server-plugins) * [Reverse Proxy](#reverse-proxy) * [Web Server Route](#web-server-route) @@ -669,6 +671,57 @@ Make a curl request via `8899` proxy: Verify that `8899` proxy forwards requests to upstream proxies by checking respective logs. +### FilterByClientIpPlugin + +Reject traffic from specific IP addresses. By default this +plugin blocks traffic from `127.0.0.1` and `::1`. + +Start `proxy.py` as: + +```bash +❯ proxy \ + --plugins proxy.plugin.FilterByClientIpPlugin +``` + +Send a request using `curl -v -x localhost:8899 http://google.com`: + +```bash +... [redacted] ... +> Proxy-Connection: Keep-Alive +> +< HTTP/1.1 418 I'm a tea pot +< Connection: close +< +* Closing connection 0 +``` + +Modify plugin to your taste e.g. Allow specific IP addresses only. + +### ModifyChunkResponsePlugin + +This plugin demonstrate how to modify chunked encoded responses. In able to do so, this plugin uses `proxy.py` core to parse the chunked encoded response. Then we reconstruct the response using custom hardcoded chunks, ignoring original chunks received from upstream server. + +Start `proxy.py` as: + +```bash +❯ proxy \ + --plugins proxy.plugin.ModifyChunkResponsePlugin +``` + +Verify using `curl -v -x localhost:8899 http://httpbin.org/stream/5`: + +```bash +... [redacted] ... +modify +chunk +response +plugin +* Connection #0 to host localhost left intact +* Closing connection 0 +``` + +Modify `ModifyChunkResponsePlugin` to your taste. Example, instead of sending hardcoded chunks, parse and modify the original `JSON` chunks received from the upstream server. + ## HTTP Web Server Plugins ### Reverse Proxy diff --git a/proxy/http/parser.py b/proxy/http/parser.py index 63c62b8b9..e3e23d2a0 100644 --- a/proxy/http/parser.py +++ b/proxy/http/parser.py @@ -15,7 +15,7 @@ from .chunk_parser import ChunkParser, chunkParserStates from ..common.constants import DEFAULT_DISABLE_HEADERS, COLON, CRLF, WHITESPACE, HTTP_1_1, DEFAULT_HTTP_PORT -from ..common.utils import build_http_request, find_http_line, text_ +from ..common.utils import build_http_request, build_http_response, find_http_line, text_ HttpParserStates = NamedTuple('HttpParserStates', [ @@ -237,7 +237,8 @@ def build_path(self) -> bytes: return url def build(self, disable_headers: Optional[List[bytes]] = None) -> bytes: - assert self.method and self.version and self.path + """Rebuild the request object.""" + assert self.method and self.version and self.path and self.type == httpParserTypes.REQUEST_PARSER if disable_headers is None: disable_headers = DEFAULT_DISABLE_HEADERS body: Optional[bytes] = ChunkParser.to_chunks(self.body) \ @@ -250,6 +251,16 @@ def build(self, disable_headers: Optional[List[bytes]] = None) -> bytes: body=body ) + def build_response(self) -> bytes: + """Rebuild the response object.""" + assert self.code and self.version and self.body and self.type == httpParserTypes.RESPONSE_PARSER + return build_http_response( + status_code=int(self.code), + protocol_version=self.version, + reason=self.reason, + headers={} if not self.headers else {self.headers[k][0]: self.headers[k][1] for k in self.headers}, + body=self.body if not self.is_chunked_encoded() else ChunkParser.to_chunks(self.body)) + def has_upstream_server(self) -> bool: """Host field SHOULD be None for incoming local WebServer requests.""" return True if self.host is not None else False diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index 035e6f0cd..16954c2f2 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -19,6 +19,7 @@ from .reverse_proxy import ReverseProxyPlugin from .proxy_pool import ProxyPoolPlugin from .filter_by_client_ip import FilterByClientIpPlugin +from .modify_chunk_response_plugin import ModifyChunkResponsePlugin __all__ = [ 'CacheResponsesPlugin', @@ -33,4 +34,5 @@ 'ReverseProxyPlugin', 'ProxyPoolPlugin', 'FilterByClientIpPlugin', + 'ModifyChunkResponsePlugin', ] diff --git a/proxy/plugin/modify_chunk_response_plugin.py b/proxy/plugin/modify_chunk_response_plugin.py new file mode 100644 index 000000000..707da5de5 --- /dev/null +++ b/proxy/plugin/modify_chunk_response_plugin.py @@ -0,0 +1,51 @@ +# -*- 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. +""" +from typing import Optional, Any + +from ..http.parser import HttpParser, httpParserTypes, httpParserStates +from ..http.proxy import HttpProxyBasePlugin + + +class ModifyChunkResponsePlugin(HttpProxyBasePlugin): + """Accumulate & modify chunk responses as received from upstream.""" + + DEFAULT_CHUNKS = [ + b'modify', + b'chunk', + b'response', + b'plugin', + ] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # Create a new http protocol parser for response payloads + self.response = HttpParser(httpParserTypes.RESPONSE_PARSER) + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + # Parse the response. + # Note that these chunks also include headers + self.response.parse(chunk.tobytes()) + # If response is complete, modify and dispatch to client + if self.response.state == httpParserStates.COMPLETE: + self.response.body = b'\n'.join(self.DEFAULT_CHUNKS) + b'\n' + self.client.queue(memoryview(self.response.build_response())) + return memoryview(b'') + + def on_upstream_connection_close(self) -> None: + pass From 1b0ed923d7919e8abf029d0859c3333060396cdb Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sat, 4 Jul 2020 18:17:11 +0530 Subject: [PATCH 08/79] 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 --- menubar/proxy.py.xcodeproj/project.pbxproj | 4 +- .../UserInterfaceState.xcuserstate | Bin 0 -> 20890 bytes .../xcschemes/xcschememanagement.plist | 14 +++ menubar/proxy.py/AppDelegate.swift | 9 +- menubar/proxy.py/ContentView.swift | 3 +- proxy/common/flags.py | 4 + proxy/http/handler.py | 81 +------------- proxy/http/plugin.py | 99 ++++++++++++++++++ proxy/http/proxy/server.py | 29 +++-- proxy/http/server/web.py | 2 +- .../http/test_http_proxy_tls_interception.py | 2 +- 11 files changed, 155 insertions(+), 92 deletions(-) create mode 100644 menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 proxy/http/plugin.py diff --git a/menubar/proxy.py.xcodeproj/project.pbxproj b/menubar/proxy.py.xcodeproj/project.pbxproj index 90e415dca..9e6b47c54 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 0000000000000000000000000000000000000000..f0463297602775a0e1fc5a2ea2fc4630fc2cade6 GIT binary patch literal 20890 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 F`d^ib!wLWZ literal 0 HcmV?d00001 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 000000000..f806210ad --- /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 f4ac49883..bac63c5c9 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 52fee6360..8cf29f441 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 182c2c35c..26b447097 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -145,7 +145,9 @@ def initialize( '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 @@ def initialize( # 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 @@ def initialize( 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 9c7dd90c6..d5538a267 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -15,9 +15,11 @@ 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 @@ 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 000000000..86232ca51 --- /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 5ce2948db..0396bf043 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 @@ 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 @@ def on_request_complete(self) -> Union[socket.socket, bool]: # 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 @@ def gen_ca_signed_certificate(self, cert_file_path: str, certificate: Dict[str, '{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 @@ def wrap_client(self) -> None: 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 b1b9475e7..ea53756d7 100644 --- a/proxy/http/server/web.py +++ b/proxy/http/server/web.py @@ -23,7 +23,7 @@ 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 7799c1d53..dac97cd65 100644 --- a/tests/http/test_http_proxy_tls_interception.py +++ b/tests/http/test_http_proxy_tls_interception.py @@ -169,7 +169,7 @@ def mock_connection() -> Any: 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( From 9be6c2946a3ddfdf98a9c4dcf3c51c892c709c05 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 6 Jul 2020 15:42:39 +0200 Subject: [PATCH 09/79] Update coverage from 5.1 to 5.2 (#390) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 9658f5a0c..768a9728c 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,5 +1,5 @@ python-coveralls==2.9.3 -coverage==5.1 +coverage==5.2 flake8==3.8.3 pytest==5.4.3 pytest-cov==2.10.0 From c884338f428408221ff48725285d992ff4a8d0a5 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Tue, 7 Jul 2020 18:01:49 +0530 Subject: [PATCH 10/79] Core acceptor pool doc, cleanup and standalone example (#393) * Better document acceptor module and add a TCP Echo Server example * autopep8 formating * Rename ThreadlessWork --> Work class * Make initialize, is_inactive and shutdown as optional interface methods. Also introduce Readables & Writables custom types. * Move websocket code into its own module * Add websocket client example * Cleanup websocket client --- Makefile | 5 +- examples/README.md | 5 + examples/tcp_echo_server.py | 75 +++++++++++++ examples/websocket_client.py | 44 ++++++++ proxy/common/flags.py | 15 ++- proxy/common/types.py | 8 +- proxy/common/utils.py | 4 +- proxy/core/acceptor/__init__.py | 4 + proxy/core/acceptor/acceptor.py | 20 ++-- proxy/core/acceptor/pool.py | 18 ++- proxy/core/{ => acceptor}/threadless.py | 94 +++------------- proxy/core/acceptor/work.py | 90 +++++++++++++++ proxy/http/handler.py | 17 ++- proxy/http/parser.py | 6 +- proxy/http/plugin.py | 6 +- proxy/http/proxy/server.py | 21 ++-- proxy/http/server/web.py | 6 +- proxy/http/websocket/__init__.py | 18 +++ proxy/http/websocket/client.py | 105 ++++++++++++++++++ .../http/{websocket.py => websocket/frame.py} | 98 +--------------- proxy/proxy.py | 8 +- setup.py | 3 +- tests/common/test_pki.py | 4 +- tests/http/test_http_parser.py | 13 ++- .../http/test_http_proxy_tls_interception.py | 3 +- tests/http/test_websocket_client.py | 13 ++- tests/testing/test_embed.py | 3 +- 27 files changed, 467 insertions(+), 239 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/tcp_echo_server.py create mode 100644 examples/websocket_client.py rename proxy/core/{ => acceptor}/threadless.py (69%) create mode 100644 proxy/core/acceptor/work.py create mode 100644 proxy/http/websocket/__init__.py create mode 100644 proxy/http/websocket/client.py rename proxy/http/{websocket.py => websocket/frame.py} (58%) diff --git a/Makefile b/Makefile index 17152b4e8..2fac9c297 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ devtools: pushd dashboard && npm run devtools && popd autopep8: + autopep8 --recursive --in-place --aggressive examples autopep8 --recursive --in-place --aggressive proxy autopep8 --recursive --in-place --aggressive tests autopep8 --recursive --in-place --aggressive setup.py @@ -73,8 +74,8 @@ lib-clean: rm -rf .hypothesis lib-lint: - flake8 --ignore=W504 --max-line-length=127 --max-complexity=19 proxy/ tests/ setup.py - mypy --strict --ignore-missing-imports proxy/ tests/ setup.py + flake8 --ignore=W504 --max-line-length=127 --max-complexity=19 examples/ proxy/ tests/ setup.py + mypy --strict --ignore-missing-imports examples/ proxy/ tests/ setup.py lib-test: lib-clean lib-version lib-lint pytest -v tests/ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..82765d958 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +# Proxy.py Library Examples + +This directory contains examples that demonstrate `proxy.py` core library capabilities. + +Looking for `proxy.py` plugin examples? Check [proxy/plugin](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) directory. diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py new file mode 100644 index 000000000..02d7ca3bd --- /dev/null +++ b/examples/tcp_echo_server.py @@ -0,0 +1,75 @@ +# -*- 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 time +import socket +import selectors + +from typing import Dict + +from proxy.core.acceptor import AcceptorPool, Work +from proxy.common.flags import Flags +from proxy.common.types import Readables, Writables + + +class EchoServerHandler(Work): + """EchoServerHandler implements Work interface. + + An instance of EchoServerHandler is created for each client + connection. EchoServerHandler lifecycle is controlled by + Threadless core using asyncio. Implementation must provide + get_events and handle_events method. Optionally, also implement + intialize, is_inactive and shutdown method. + """ + + def get_events(self) -> Dict[socket.socket, int]: + # We always want to read from client + # Register for EVENT_READ events + events = {self.client.connection: selectors.EVENT_READ} + # If there is pending buffer for client + # also register for EVENT_WRITE events + if self.client.has_buffer(): + events[self.client.connection] |= selectors.EVENT_WRITE + return events + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Return True to shutdown work.""" + if self.client.connection in readables: + data = self.client.recv() + if data is None: + # Client closed connection, signal shutdown + return True + # Queue data back to client + self.client.queue(data) + + if self.client.connection in writables: + self.client.flush() + + return False + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Flags(num_workers=1, threadless=True), + work_klass=EchoServerHandler) + try: + pool.setup() + while True: + time.sleep(1) + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/websocket_client.py b/examples/websocket_client.py new file mode 100644 index 000000000..5509d10da --- /dev/null +++ b/examples/websocket_client.py @@ -0,0 +1,44 @@ +# -*- 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 time +from proxy.http.websocket import WebsocketClient, WebsocketFrame, websocketOpcodes + + +# globals +client: WebsocketClient +last_dispatch_time: float +static_frame = memoryview(WebsocketFrame.text(b'hello')) +num_echos = 10 + + +def on_message(frame: WebsocketFrame) -> None: + """WebsocketClient on_message callback.""" + global client, num_echos, last_dispatch_time + print('Received %r after %d millisec' % (frame.data, (time.time() - last_dispatch_time) * 1000)) + assert(frame.data == b'hello' and frame.opcode == websocketOpcodes.TEXT_FRAME) + if num_echos > 0: + client.queue(static_frame) + last_dispatch_time = time.time() + num_echos -= 1 + else: + client.close() + + +if __name__ == '__main__': + # Constructor establishes socket connection + client = WebsocketClient(b'echo.websocket.org', 80, b'/', on_message=on_message) + # Perform handshake + client.handshake() + # Queue some data for client + client.queue(static_frame) + last_dispatch_time = time.time() + # Start event loop + client.run() diff --git a/proxy/common/flags.py b/proxy/common/flags.py index 26b447097..e22ae5d03 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -21,8 +21,9 @@ import sys import inspect -from typing import Optional, Union, Dict, List, TypeVar, Type, cast, Any, Tuple +from typing import Optional, Dict, List, TypeVar, Type, cast, Any, Tuple +from .types import IpAddress from .utils import text_, bytes_ from .constants import DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH from .constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_DISABLE_HEADERS @@ -67,8 +68,7 @@ def __init__( ca_signing_key_file: Optional[str] = None, ca_file: Optional[str] = None, num_workers: int = 0, - hostname: Union[ipaddress.IPv4Address, - ipaddress.IPv6Address] = DEFAULT_IPV6_HOSTNAME, + hostname: IpAddress = DEFAULT_IPV6_HOSTNAME, port: int = DEFAULT_PORT, backlog: int = DEFAULT_BACKLOG, static_server_dir: str = DEFAULT_STATIC_SERVER_DIR, @@ -99,8 +99,7 @@ def __init__( self.ca_signing_key_file: Optional[str] = ca_signing_key_file self.ca_file = ca_file self.num_workers: int = num_workers if num_workers > 0 else multiprocessing.cpu_count() - self.hostname: Union[ipaddress.IPv4Address, - ipaddress.IPv6Address] = hostname + self.hostname: IpAddress = hostname self.family: socket.AddressFamily = socket.AF_INET6 if hostname.version == 6 else socket.AF_INET self.port: int = port self.backlog: int = backlog @@ -161,7 +160,8 @@ def initialize( # Setup limits Flags.set_open_file_limit(args.open_file_limit) - # Prepare list of plugins to load based upon --enable-* and --disable-* flags + # 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)) @@ -249,8 +249,7 @@ def initialize( opts.get( 'ca_file', args.ca_file)), - hostname=cast(Union[ipaddress.IPv4Address, - ipaddress.IPv6Address], + hostname=cast(IpAddress, opts.get('hostname', ipaddress.ip_address(args.hostname))), port=cast(int, opts.get('port', args.port)), backlog=cast(int, opts.get('backlog', args.backlog)), diff --git a/proxy/common/types.py b/proxy/common/types.py index c41104844..279211e42 100644 --- a/proxy/common/types.py +++ b/proxy/common/types.py @@ -9,8 +9,9 @@ :license: BSD, see LICENSE for more details. """ import queue +import ipaddress -from typing import TYPE_CHECKING, Dict, Any +from typing import TYPE_CHECKING, Dict, Any, List, Union from typing_extensions import Protocol @@ -23,3 +24,8 @@ class HasFileno(Protocol): def fileno(self) -> int: ... # pragma: no cover + + +Readables = List[Union[int, HasFileno]] +Writables = List[Union[int, HasFileno]] +IpAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] diff --git a/proxy/common/utils.py b/proxy/common/utils.py index ecdc4e9fe..078bbbcbe 100644 --- a/proxy/common/utils.py +++ b/proxy/common/utils.py @@ -101,7 +101,8 @@ def build_http_pkt(line: List[bytes], def build_websocket_handshake_request( key: bytes, method: bytes = b'GET', - url: bytes = b'/') -> bytes: + url: bytes = b'/', + host: bytes = b'localhost') -> bytes: """ Build and returns a Websocket handshake request packet. @@ -112,6 +113,7 @@ def build_websocket_handshake_request( return build_http_request( method, url, headers={ + b'Host': host, b'Connection': b'upgrade', b'Upgrade': b'websocket', b'Sec-WebSocket-Key': key, diff --git a/proxy/core/acceptor/__init__.py b/proxy/core/acceptor/__init__.py index 9c0a97b33..cca3bcdb4 100644 --- a/proxy/core/acceptor/__init__.py +++ b/proxy/core/acceptor/__init__.py @@ -10,8 +10,12 @@ """ from .acceptor import Acceptor from .pool import AcceptorPool +from .work import Work +from .threadless import Threadless __all__ = [ 'Acceptor', 'AcceptorPool', + 'Work', + 'Threadless', ] diff --git a/proxy/core/acceptor/acceptor.py b/proxy/core/acceptor/acceptor.py index eeb295526..d5134b4e1 100644 --- a/proxy/core/acceptor/acceptor.py +++ b/proxy/core/acceptor/acceptor.py @@ -14,13 +14,15 @@ import selectors import socket import threading -# import time + from multiprocessing import connection from multiprocessing.reduction import send_handle, recv_handle from typing import Optional, Type, Tuple +from .work import Work +from .threadless import Threadless + from ..connection import TcpClientConnection -from ..threadless import ThreadlessWork, Threadless from ..event import EventQueue, eventNames from ...common.flags import Flags @@ -28,10 +30,12 @@ class Acceptor(multiprocessing.Process): - """Socket client acceptor. + """Socket server acceptor process. - Accepts client connection over received server socket handle and - starts a new work thread. + Accepts client connection over received server socket handle at startup. Spawns a separate + thread to handle each client request. However, when `--threadless` is enabled, Acceptor also + pre-spawns a `Threadless` process at startup. Accepted client connections are passed to + `Threadless` process which internally uses asyncio event loop to handle client connections. """ def __init__( @@ -39,7 +43,7 @@ def __init__( idd: int, work_queue: connection.Connection, flags: Flags, - work_klass: Type[ThreadlessWork], + work_klass: Type[Work], lock: multiprocessing.synchronize.Lock, event_queue: Optional[EventQueue] = None) -> None: super().__init__() @@ -108,11 +112,7 @@ def run_once(self) -> None: if len(events) == 0: return conn, addr = self.sock.accept() - - # now = time.time() - # fileno: int = conn.fileno() self.start_work(conn, addr) - # logger.info('Work started for fd %d in %f seconds', fileno, time.time() - now) def run(self) -> None: self.selector = selectors.DefaultSelector() diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py index 48cedaf96..259c5d304 100644 --- a/proxy/core/acceptor/pool.py +++ b/proxy/core/acceptor/pool.py @@ -18,7 +18,8 @@ from typing import List, Optional, Type from .acceptor import Acceptor -from ..threadless import ThreadlessWork +from .work import Work + from ..event import EventQueue, EventDispatcher from ...common.flags import Flags @@ -31,11 +32,20 @@ class AcceptorPool: """AcceptorPool. Pre-spawns worker processes to utilize all cores available on the system. Server socket connection is - dispatched over a pipe to workers. Each worker accepts incoming client request and spawns a - separate thread to handle the client request. + dispatched over a pipe to workers. Each Acceptor instance accepts for new client connection. + + Example usage: + + pool = AcceptorPool(flags=..., work_klass=...) + try: + pool.setup() + while True: + time.sleep(1) + finally: + pool.shutdown() """ - def __init__(self, flags: Flags, work_klass: Type[ThreadlessWork]) -> None: + def __init__(self, flags: Flags, work_klass: Type[Work]) -> None: self.flags = flags self.socket: Optional[socket.socket] = None self.acceptors: List[Acceptor] = [] diff --git a/proxy/core/threadless.py b/proxy/core/acceptor/threadless.py similarity index 69% rename from proxy/core/threadless.py rename to proxy/core/acceptor/threadless.py index 87be7e5b8..78a59cc9f 100644 --- a/proxy/core/threadless.py +++ b/proxy/core/acceptor/threadless.py @@ -18,81 +18,18 @@ from multiprocessing import connection from multiprocessing.reduction import recv_handle -from abc import ABC, abstractmethod -from typing import Dict, Optional, Tuple, List, Union, Generator, Any, Type -from uuid import uuid4, UUID +from typing import Dict, Optional, Tuple, List, Generator, Any, Type -from .connection import TcpClientConnection -from .event import EventQueue, eventNames +from .work import Work -from ..common.flags import Flags -from ..common.types import HasFileno -from ..common.constants import DEFAULT_TIMEOUT +from ..connection import TcpClientConnection +from ..event import EventQueue, eventNames -logger = logging.getLogger(__name__) - - -class ThreadlessWork(ABC): - """Implement ThreadlessWork to hook into the event loop provided by Threadless process.""" - - @abstractmethod - def __init__( - self, - client: TcpClientConnection, - flags: Optional[Flags], - event_queue: Optional[EventQueue] = None, - uid: Optional[UUID] = None) -> None: - self.client = client - self.flags = flags if flags else Flags() - self.event_queue = event_queue - self.uid: UUID = uid if uid is not None else uuid4() - - @abstractmethod - def initialize(self) -> None: - pass # pragma: no cover - - @abstractmethod - def is_inactive(self) -> bool: - return False # pragma: no cover - - @abstractmethod - def get_events(self) -> Dict[socket.socket, int]: - return {} # pragma: no cover +from ...common.flags import Flags +from ...common.types import Readables, Writables +from ...common.constants import DEFAULT_TIMEOUT - @abstractmethod - def handle_events( - self, - readables: List[Union[int, HasFileno]], - writables: List[Union[int, HasFileno]]) -> bool: - """Return True to shutdown work.""" - return False # pragma: no cover - - @abstractmethod - def run(self) -> None: - pass - - def publish_event( - self, - event_name: int, - event_payload: Dict[str, Any], - publisher_id: Optional[str] = None) -> None: - if not self.flags.enable_events: - return - assert self.event_queue - self.event_queue.publish( - self.uid.hex, - event_name, - event_payload, - publisher_id - ) - - def shutdown(self) -> None: - """Must close any opened resources and call super().shutdown().""" - self.publish_event( - event_name=eventNames.WORK_FINISHED, - event_payload={}, - publisher_id=self.__class__.__name__ - ) +logger = logging.getLogger(__name__) class Threadless(multiprocessing.Process): @@ -103,15 +40,15 @@ class Threadless(multiprocessing.Process): for each accepted client connection, Acceptor process sends accepted client connection to Threadless process over a pipe. - HttpProtocolHandler implements ThreadlessWork class and hooks into the - event loop provided by Threadless. + Example, HttpProtocolHandler implements Work class to hooks into the + event loop provided by Threadless process. """ def __init__( self, client_queue: connection.Connection, flags: Flags, - work_klass: Type[ThreadlessWork], + work_klass: Type[Work], event_queue: Optional[EventQueue] = None) -> None: super().__init__() self.client_queue = client_queue @@ -120,13 +57,12 @@ def __init__( self.event_queue = event_queue self.running = multiprocessing.Event() - self.works: Dict[int, ThreadlessWork] = {} + self.works: Dict[int, Work] = {} self.selector: Optional[selectors.DefaultSelector] = None self.loop: Optional[asyncio.AbstractEventLoop] = None @contextlib.contextmanager - def selected_events(self) -> Generator[Tuple[List[Union[int, HasFileno]], - List[Union[int, HasFileno]]], + def selected_events(self) -> Generator[Tuple[Readables, Writables], None, None]: events: Dict[socket.socket, int] = {} for work in self.works.values(): @@ -148,8 +84,8 @@ def selected_events(self) -> Generator[Tuple[List[Union[int, HasFileno]], async def handle_events( self, fileno: int, - readables: List[Union[int, HasFileno]], - writables: List[Union[int, HasFileno]]) -> bool: + readables: Readables, + writables: Writables) -> bool: return self.works[fileno].handle_events(readables, writables) # TODO: Use correct future typing annotations diff --git a/proxy/core/acceptor/work.py b/proxy/core/acceptor/work.py new file mode 100644 index 000000000..bcd251f7b --- /dev/null +++ b/proxy/core/acceptor/work.py @@ -0,0 +1,90 @@ +# -*- 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 uuid4, UUID +from typing import Optional, Dict, Any + +from ..event import eventNames, EventQueue +from ..connection import TcpClientConnection +from ...common.flags import Flags +from ...common.types import Readables, Writables + + +class Work(ABC): + """Implement Work to hook into the event loop provided by Threadless process.""" + + def __init__( + self, + client: TcpClientConnection, + flags: Optional[Flags], + event_queue: Optional[EventQueue] = None, + uid: Optional[UUID] = None) -> None: + self.client = client + self.flags = flags if flags else Flags() + self.event_queue = event_queue + self.uid: UUID = uid if uid is not None else uuid4() + + @abstractmethod + def get_events(self) -> Dict[socket.socket, int]: + """Return sockets and events (read or write) that we are interested in.""" + return {} # pragma: no cover + + @abstractmethod + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Handle readable and writable sockets. + + Return True to shutdown work.""" + return False # pragma: no cover + + def initialize(self) -> None: + """Perform any resource initialization.""" + pass # pragma: no cover + + def is_inactive(self) -> bool: + """Return True if connection should be considered inactive.""" + return False # pragma: no cover + + def shutdown(self) -> None: + """Implementation must close any opened resources here + and call super().shutdown().""" + self.publish_event( + event_name=eventNames.WORK_FINISHED, + event_payload={}, + publisher_id=self.__class__.__name__ + ) + + def run(self) -> None: + """run() method is not used by Threadless. It's here for backward + compatibility with threaded mode where work class is started as + a separate thread. + """ + pass + + def publish_event( + self, + event_name: int, + event_payload: Dict[str, Any], + publisher_id: Optional[str] = None) -> None: + """Convenience method provided to publish events into the global event queue.""" + if not self.flags.enable_events: + return + assert self.event_queue + self.event_queue.publish( + self.uid.hex, + event_name, + event_payload, + publisher_id + ) diff --git a/proxy/http/handler.py b/proxy/http/handler.py index d5538a267..4a1b67be9 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -24,15 +24,15 @@ from .exception import HttpProtocolException from ..common.flags import Flags -from ..common.types import HasFileno -from ..core.threadless import ThreadlessWork +from ..common.types import Readables, Writables +from ..core.acceptor.work import Work from ..core.event import EventQueue from ..core.connection import TcpClientConnection logger = logging.getLogger(__name__) -class HttpProtocolHandler(ThreadlessWork): +class HttpProtocolHandler(Work): """HTTP, HTTPS, HTTP2, WebSockets protocol handler. Accepts `Client` connection object and manages HttpProtocolHandlerPlugin invocations. @@ -100,8 +100,8 @@ def get_events(self) -> Dict[socket.socket, int]: def handle_events( self, - readables: List[Union[int, HasFileno]], - writables: List[Union[int, HasFileno]]) -> bool: + readables: Readables, + writables: Writables) -> bool: """Returns True if proxy must teardown.""" # Flush buffer for ready to write sockets teardown = self.handle_writables(writables) @@ -197,7 +197,7 @@ def flush(self) -> None: finally: self.selector.unregister(self.client.connection) - def handle_writables(self, writables: List[Union[int, HasFileno]]) -> bool: + def handle_writables(self, writables: Writables) -> bool: if self.client.has_buffer() and self.client.connection in writables: logger.debug('Client is ready for writes, flushing buffer') self.last_activity = time.time() @@ -222,7 +222,7 @@ def handle_writables(self, writables: List[Union[int, HasFileno]]) -> bool: return True return False - def handle_readables(self, readables: List[Union[int, HasFileno]]) -> bool: + def handle_readables(self, readables: Readables) -> bool: if self.client.connection in readables: logger.debug('Client is ready for reads, reading') self.last_activity = time.time() @@ -292,8 +292,7 @@ def handle_readables(self, readables: List[Union[int, HasFileno]]) -> bool: @contextlib.contextmanager def selected_events(self) -> \ - Generator[Tuple[List[Union[int, HasFileno]], - List[Union[int, HasFileno]]], + Generator[Tuple[Readables, Writables], None, None]: events = self.get_events() for fd in events: diff --git a/proxy/http/parser.py b/proxy/http/parser.py index e3e23d2a0..d40885ca7 100644 --- a/proxy/http/parser.py +++ b/proxy/http/parser.py @@ -171,7 +171,8 @@ def parse(self, raw: bytes) -> None: self.state = httpParserStates.COMPLETE more = False else: - raise NotImplementedError('Parser shouldn\'t have reached here') + raise NotImplementedError( + 'Parser shouldn\'t have reached here') else: more, raw = self.process(raw) self.buffer = raw @@ -258,7 +259,8 @@ def build_response(self) -> bytes: status_code=int(self.code), protocol_version=self.version, reason=self.reason, - headers={} if not self.headers else {self.headers[k][0]: self.headers[k][1] for k in self.headers}, + headers={} if not self.headers else { + self.headers[k][0]: self.headers[k][1] for k in self.headers}, body=self.body if not self.is_chunked_encoded() else ChunkParser.to_chunks(self.body)) def has_upstream_server(self) -> bool: diff --git a/proxy/http/plugin.py b/proxy/http/plugin.py index 86232ca51..b8d95b286 100644 --- a/proxy/http/plugin.py +++ b/proxy/http/plugin.py @@ -17,7 +17,7 @@ from .parser import HttpParser from ..common.flags import Flags -from ..common.types import HasFileno +from ..common.types import Readables, Writables from ..core.event import EventQueue from ..core.connection import TcpClientConnection @@ -71,11 +71,11 @@ def get_descriptors( return [], [] # pragma: no cover @abstractmethod - def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: + def write_to_descriptors(self, w: Writables) -> bool: return False # pragma: no cover @abstractmethod - def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool: + def read_from_descriptors(self, r: Readables) -> bool: return False # pragma: no cover @abstractmethod diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 0396bf043..3fad47e7f 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -25,7 +25,7 @@ from ..parser import HttpParser, httpParserStates, httpParserTypes from ..methods import httpMethods -from ...common.types import HasFileno +from ...common.types import Readables, Writables from ...common.constants import PROXY_AGENT_HEADER_VALUE from ...common.utils import build_http_response, text_ from ...common.pki import gen_public_key, gen_csr, sign_csr @@ -82,7 +82,7 @@ def get_descriptors( w.append(self.server.connection) return r, w - def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: + def write_to_descriptors(self, w: Writables) -> bool: if self.request.has_upstream_server() and \ self.server and not self.server.closed and \ self.server.has_buffer() and \ @@ -91,18 +91,20 @@ def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: try: self.server.flush() except ssl.SSLWantWriteError: - logger.warning('SSLWantWriteError while trying to flush to server, will retry') + logger.warning( + 'SSLWantWriteError while trying to flush to server, will retry') return False except BrokenPipeError: logger.error( 'BrokenPipeError when flushing buffer for server') return True except OSError as e: - logger.exception('OSError when flushing buffer to server', exc_info=e) + logger.exception( + 'OSError when flushing buffer to server', exc_info=e) return True return False - def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool: + def read_from_descriptors(self, r: Readables) -> bool: if self.request.has_upstream_server( ) and self.server and not self.server.closed and self.server.connection in r: logger.debug('Server is ready for reads, reading...') @@ -289,7 +291,8 @@ def on_request_complete(self) -> Union[socket.socket, bool]: # 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) + logger.exception( + 'TimeoutExpired during certificate generation', exc_info=e) return True except BrokenPipeError: logger.error( @@ -360,7 +363,8 @@ def access_log(self) -> None: self.response.total_size, connection_time_ms)) - def gen_ca_signed_certificate(self, cert_file_path: str, certificate: Dict[str, Any]) -> None: + def gen_ca_signed_certificate( + self, cert_file_path: str, certificate: Dict[str, Any]) -> None: '''CA signing key (default) is used for generating a public key for common_name, if one already doesn't exist. Using generated public key a CSR request is generated, which is then signed by @@ -388,7 +392,8 @@ def gen_ca_signed_certificate(self, cert_file_path: str, certificate: Dict[str, subject = '' for key in keys: if upstream_subject.get(keys[key], None): - subject += '/{0}={1}'.format(key, upstream_subject.get(keys[key])) + subject += '/{0}={1}'.format(key, + upstream_subject.get(keys[key])) alt_subj_names = [text_(self.request.host), ] validity_in_days = 365 * 2 timeout = 10 diff --git a/proxy/http/server/web.py b/proxy/http/server/web.py index ea53756d7..54be0ab65 100644 --- a/proxy/http/server/web.py +++ b/proxy/http/server/web.py @@ -27,7 +27,7 @@ from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response from ...common.constants import PROXY_AGENT_HEADER_VALUE -from ...common.types import HasFileno +from ...common.types import Readables, Writables logger = logging.getLogger(__name__) @@ -166,10 +166,10 @@ def on_request_complete(self) -> Union[socket.socket, bool]: self.client.queue(self.DEFAULT_404_RESPONSE) return True - def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: + def write_to_descriptors(self, w: Writables) -> bool: pass - def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool: + def read_from_descriptors(self, r: Readables) -> bool: pass def on_client_data(self, raw: memoryview) -> Optional[memoryview]: diff --git a/proxy/http/websocket/__init__.py b/proxy/http/websocket/__init__.py new file mode 100644 index 000000000..2870e3b26 --- /dev/null +++ b/proxy/http/websocket/__init__.py @@ -0,0 +1,18 @@ +# -*- 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. +""" +from .frame import WebsocketFrame, websocketOpcodes +from .client import WebsocketClient + +__all__ = [ + 'websocketOpcodes', + 'WebsocketFrame', + 'WebsocketClient', +] diff --git a/proxy/http/websocket/client.py b/proxy/http/websocket/client.py new file mode 100644 index 000000000..16e440dec --- /dev/null +++ b/proxy/http/websocket/client.py @@ -0,0 +1,105 @@ +# -*- 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 base64 +import selectors +import socket +import secrets +import ssl + +from typing import Optional, Union, Callable + +from .frame import WebsocketFrame + +from ..parser import httpParserTypes, HttpParser + +from ...common.constants import DEFAULT_BUFFER_SIZE +from ...common.utils import new_socket_connection, build_websocket_handshake_request, text_ +from ...core.connection import tcpConnectionTypes, TcpConnection + + +class WebsocketClient(TcpConnection): + + def __init__(self, + hostname: bytes, + port: int, + path: bytes = b'/', + on_message: Optional[Callable[[WebsocketFrame], None]] = None) -> None: + super().__init__(tcpConnectionTypes.CLIENT) + self.hostname: bytes = hostname + self.port: int = port + self.path: bytes = path + self.sock: socket.socket = new_socket_connection( + (socket.gethostbyname(text_(self.hostname)), self.port)) + self.on_message: Optional[Callable[[ + WebsocketFrame], None]] = on_message + self.selector: selectors.DefaultSelector = selectors.DefaultSelector() + + @property + def connection(self) -> Union[ssl.SSLSocket, socket.socket]: + return self.sock + + def handshake(self) -> None: + self.upgrade() + self.sock.setblocking(False) + + def upgrade(self) -> None: + key = base64.b64encode(secrets.token_bytes(16)) + self.sock.send(build_websocket_handshake_request(key, url=self.path, host=self.hostname)) + response = HttpParser(httpParserTypes.RESPONSE_PARSER) + response.parse(self.sock.recv(DEFAULT_BUFFER_SIZE)) + accept = response.header(b'Sec-Websocket-Accept') + assert WebsocketFrame.key_to_accept(key) == accept + + def ping(self, data: Optional[bytes] = None) -> None: + pass + + def pong(self, data: Optional[bytes] = None) -> None: + pass + + def shutdown(self, _data: Optional[bytes] = None) -> None: + """Closes connection with the server.""" + super().close() + + def run_once(self) -> bool: + ev = selectors.EVENT_READ + if self.has_buffer(): + ev |= selectors.EVENT_WRITE + self.selector.register(self.sock.fileno(), ev) + events = self.selector.select(timeout=1) + self.selector.unregister(self.sock) + for _, mask in events: + if mask & selectors.EVENT_READ and self.on_message: + raw = self.recv() + if raw is None or raw.tobytes() == b'': + self.closed = True + return True + frame = WebsocketFrame() + # TODO(abhinavsingh): Remove .tobytes after parser is + # memoryview compliant + frame.parse(raw.tobytes()) + self.on_message(frame) + elif mask & selectors.EVENT_WRITE: + self.flush() + return False + + def run(self) -> None: + try: + while not self.closed: + teardown = self.run_once() + if teardown: + break + except KeyboardInterrupt: + pass + finally: + if not self.closed: + self.selector.unregister(self.sock) + self.sock.shutdown(socket.SHUT_WR) + self.sock.close() diff --git a/proxy/http/websocket.py b/proxy/http/websocket/frame.py similarity index 58% rename from proxy/http/websocket.py rename to proxy/http/websocket/frame.py index a6eb5a337..55f9d91b1 100644 --- a/proxy/http/websocket.py +++ b/proxy/http/websocket/frame.py @@ -10,22 +10,12 @@ """ import hashlib import base64 -import selectors import struct -import socket import secrets -import ssl -import ipaddress import logging import io -from typing import TypeVar, Type, Optional, NamedTuple, Union, Callable - -from .parser import httpParserTypes, HttpParser - -from ..common.constants import DEFAULT_BUFFER_SIZE -from ..common.utils import new_socket_connection, build_websocket_handshake_request -from ..core.connection import tcpConnectionTypes, TcpConnection +from typing import TypeVar, Type, Optional, NamedTuple WebsocketOpcodes = NamedTuple('WebsocketOpcodes', [ @@ -180,89 +170,3 @@ def key_to_accept(key: bytes) -> bytes: sha1 = hashlib.sha1() sha1.update(key + WebsocketFrame.GUID) return base64.b64encode(sha1.digest()) - - -class WebsocketClient(TcpConnection): - - def __init__(self, - hostname: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], - port: int, - path: bytes = b'/', - on_message: Optional[Callable[[WebsocketFrame], None]] = None) -> None: - super().__init__(tcpConnectionTypes.CLIENT) - self.hostname: Union[ipaddress.IPv4Address, - ipaddress.IPv6Address] = hostname - self.port: int = port - self.path: bytes = path - self.sock: socket.socket = new_socket_connection( - (str(self.hostname), self.port)) - self.on_message: Optional[Callable[[ - WebsocketFrame], None]] = on_message - self.upgrade() - self.sock.setblocking(False) - self.selector: selectors.DefaultSelector = selectors.DefaultSelector() - - @property - def connection(self) -> Union[ssl.SSLSocket, socket.socket]: - return self.sock - - def upgrade(self) -> None: - key = base64.b64encode(secrets.token_bytes(16)) - self.sock.send(build_websocket_handshake_request(key, url=self.path)) - response = HttpParser(httpParserTypes.RESPONSE_PARSER) - response.parse(self.sock.recv(DEFAULT_BUFFER_SIZE)) - accept = response.header(b'Sec-Websocket-Accept') - assert WebsocketFrame.key_to_accept(key) == accept - - def ping(self, data: Optional[bytes] = None) -> None: - pass - - def pong(self, data: Optional[bytes] = None) -> None: - pass - - def shutdown(self, _data: Optional[bytes] = None) -> None: - """Closes connection with the server.""" - super().close() - - def run_once(self) -> bool: - ev = selectors.EVENT_READ - if self.has_buffer(): - ev |= selectors.EVENT_WRITE - self.selector.register(self.sock.fileno(), ev) - events = self.selector.select(timeout=1) - self.selector.unregister(self.sock) - for _, mask in events: - if mask & selectors.EVENT_READ and self.on_message: - raw = self.recv() - if raw is None or raw.tobytes() == b'': - self.closed = True - logger.debug('Websocket connection closed by server') - return True - frame = WebsocketFrame() - # TODO(abhinavsingh): Remove .tobytes after parser is - # memoryview compliant - frame.parse(raw.tobytes()) - self.on_message(frame) - elif mask & selectors.EVENT_WRITE: - logger.debug(self.buffer) - self.flush() - return False - - def run(self) -> None: - logger.debug('running') - try: - while not self.closed: - teardown = self.run_once() - if teardown: - break - except KeyboardInterrupt: - pass - finally: - try: - self.selector.unregister(self.sock) - self.sock.shutdown(socket.SHUT_WR) - except Exception as e: - logging.exception( - 'Exception while shutdown of websocket client', exc_info=e) - self.sock.close() - logger.info('done') diff --git a/proxy/proxy.py b/proxy/proxy.py index 547903268..643e85dcb 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -26,6 +26,11 @@ class Proxy: + """Context manager for controlling core AcceptorPool server lifecycle. + + By default this context manager starts AcceptorPool with HttpProtocolHandler + worker class. + """ def __init__(self, input_args: Optional[List[str]], **opts: Any) -> None: self.flags = Flags.initialize(input_args, **opts) @@ -78,7 +83,8 @@ def main( **opts: Any) -> None: try: with Proxy(input_args=input_args, **opts): - # TODO: Introduce cron feature instead of mindless sleep + # TODO: Introduce cron feature + # https://github.com/abhinavsingh/proxy.py/issues/392 while True: time.sleep(1) except KeyboardInterrupt: diff --git a/setup.py b/setup.py index 8570fb295..63df325b7 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ author_email=__author_email__, url=__homepage__, description=__description__, - long_description=open('README.md', 'r', encoding='utf-8').read().strip(), + long_description=open( + 'README.md', 'r', encoding='utf-8').read().strip(), long_description_content_type='text/markdown', download_url=__download_url__, license=__license__, diff --git a/tests/common/test_pki.py b/tests/common/test_pki.py index ebeb767db..e55c06379 100644 --- a/tests/common/test_pki.py +++ b/tests/common/test_pki.py @@ -112,7 +112,9 @@ def _gen_public_private_key(self) -> Tuple[str, str, str]: def _gen_private_key(self) -> Tuple[str, str]: key_path = os.path.join(tempfile.gettempdir(), 'test_gen_private.key') - nopass_key_path = os.path.join(tempfile.gettempdir(), 'test_gen_private_nopass.key') + nopass_key_path = os.path.join( + tempfile.gettempdir(), + 'test_gen_private_nopass.key') pki.gen_private_key(key_path, 'password') pki.remove_passphrase(key_path, 'password', nopass_key_path) return (key_path, nopass_key_path) diff --git a/tests/http/test_http_parser.py b/tests/http/test_http_parser.py index c98e6381e..e8d78cbea 100644 --- a/tests/http/test_http_parser.py +++ b/tests/http/test_http_parser.py @@ -139,7 +139,8 @@ def test_get_full_parse(self) -> None: self.assertEqual(self.parser.url.port, None) self.assertEqual(self.parser.version, b'HTTP/1.1') self.assertEqual(self.parser.state, httpParserStates.COMPLETE) - self.assertEqual(self.parser.headers[b'host'], (b'Host', b'example.com')) + self.assertEqual( + self.parser.headers[b'host'], (b'Host', b'example.com')) self.parser.del_headers([b'host']) self.parser.add_headers([(b'Host', b'example.com')]) self.assertEqual( @@ -193,7 +194,10 @@ def test_get_partial_parse1(self) -> None: self.parser.parse(CRLF * 2) self.assertEqual(self.parser.total_size, len(pkt) + (3 * len(CRLF)) + len(host_hdr)) - self.assertEqual(self.parser.headers[b'host'], (b'Host', b'localhost:8080')) + self.assertEqual( + self.parser.headers[b'host'], + (b'Host', + b'localhost:8080')) self.assertEqual(self.parser.state, httpParserStates.COMPLETE) def test_get_partial_parse2(self) -> None: @@ -210,7 +214,10 @@ def test_get_partial_parse2(self) -> None: self.assertEqual(self.parser.state, httpParserStates.LINE_RCVD) self.parser.parse(b'localhost:8080' + CRLF) - self.assertEqual(self.parser.headers[b'host'], (b'Host', b'localhost:8080')) + self.assertEqual( + self.parser.headers[b'host'], + (b'Host', + b'localhost:8080')) self.assertEqual(self.parser.buffer, b'') self.assertEqual( self.parser.state, diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py index dac97cd65..2790f8e53 100644 --- a/tests/http/test_http_proxy_tls_interception.py +++ b/tests/http/test_http_proxy_tls_interception.py @@ -146,7 +146,8 @@ def mock_connection() -> Any: self.mock_server_conn.return_value.connection.setblocking.assert_called_with( False) - self.mock_ssl_context.assert_called_with(ssl.Purpose.SERVER_AUTH, cafile=None) + self.mock_ssl_context.assert_called_with( + ssl.Purpose.SERVER_AUTH, cafile=None) # self.assertEqual(self.mock_ssl_context.return_value.options, # ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | # ssl.OP_NO_TLSv1_1) diff --git a/tests/http/test_websocket_client.py b/tests/http/test_websocket_client.py index 060fba106..faf18b2ac 100644 --- a/tests/http/test_websocket_client.py +++ b/tests/http/test_websocket_client.py @@ -13,21 +13,26 @@ from proxy.common.utils import build_websocket_handshake_response, build_websocket_handshake_request from proxy.http.websocket import WebsocketClient, WebsocketFrame -from proxy.common.constants import DEFAULT_IPV4_HOSTNAME, DEFAULT_PORT +from proxy.common.constants import DEFAULT_PORT class TestWebsocketClient(unittest.TestCase): + @mock.patch('proxy.http.websocket.client.socket.gethostbyname') @mock.patch('base64.b64encode') - @mock.patch('proxy.http.websocket.new_socket_connection') + @mock.patch('proxy.http.websocket.client.new_socket_connection') def test_handshake(self, mock_connect: mock.Mock, - mock_b64encode: mock.Mock) -> None: + mock_b64encode: mock.Mock, + mock_gethostbyname: mock.Mock) -> None: key = b'MySecretKey' mock_b64encode.return_value = key + mock_gethostbyname.return_value = '127.0.0.1' mock_connect.return_value.recv.return_value = \ build_websocket_handshake_response( WebsocketFrame.key_to_accept(key)) - _ = WebsocketClient(DEFAULT_IPV4_HOSTNAME, DEFAULT_PORT) + client = WebsocketClient(b'localhost', DEFAULT_PORT) + mock_connect.return_value.send.assert_not_called() + client.handshake() mock_connect.return_value.send.assert_called_with( build_websocket_handshake_request(key) ) diff --git a/tests/testing/test_embed.py b/tests/testing/test_embed.py index 6a55af269..e69c5ffe5 100644 --- a/tests/testing/test_embed.py +++ b/tests/testing/test_embed.py @@ -21,7 +21,8 @@ from proxy.http.methods import httpMethods -@unittest.skipIf(os.name == 'nt', 'Disabled for Windows due to weird permission issues.') +@unittest.skipIf( + os.name == 'nt', 'Disabled for Windows due to weird permission issues.') class TestProxyPyEmbedded(TestCase): """This test case is a demonstration of proxy.TestCase and also serves as integration test suite for proxy.py.""" From 682114e9e0f52de578428c85ec6c517be31f0912 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 8 Jul 2020 13:11:12 +0530 Subject: [PATCH 11/79] Decouple SSL wrap logic into connection classes (#394) * Move wrap functionality within respective connection classes. Also decouple websocket client handshake method * Add a TCP echo client example that works with TCP echo server example --- examples/tcp_echo_client.py | 21 +++++++++++++++ examples/tcp_echo_server.py | 27 ++++++++++++++----- examples/websocket_client.py | 12 ++++++--- proxy/core/connection/client.py | 12 +++++++++ proxy/core/connection/server.py | 13 ++++++++- proxy/http/proxy/server.py | 24 +++-------------- proxy/http/websocket/client.py | 6 ++++- .../http/test_http_proxy_tls_interception.py | 7 ++++- ...ttp_proxy_plugins_with_tls_interception.py | 6 ++++- 9 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 examples/tcp_echo_client.py diff --git a/examples/tcp_echo_client.py b/examples/tcp_echo_client.py new file mode 100644 index 000000000..decabb505 --- /dev/null +++ b/examples/tcp_echo_client.py @@ -0,0 +1,21 @@ +# -*- 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. +""" +from proxy.common.utils import socket_connection +from proxy.common.constants import DEFAULT_BUFFER_SIZE + +if __name__ == '__main__': + with socket_connection(('::', 12345)) as client: + while True: + client.send(b'hello') + data = client.recv(DEFAULT_BUFFER_SIZE) + if data is None: + break + print(data) diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py index 02d7ca3bd..031963de7 100644 --- a/examples/tcp_echo_server.py +++ b/examples/tcp_echo_server.py @@ -12,7 +12,7 @@ import socket import selectors -from typing import Dict +from typing import Dict, Any from proxy.core.acceptor import AcceptorPool, Work from proxy.common.flags import Flags @@ -29,6 +29,10 @@ class EchoServerHandler(Work): intialize, is_inactive and shutdown method. """ + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + print('Connection accepted from {0}'.format(self.client.addr)) + def get_events(self) -> Dict[socket.socket, int]: # We always want to read from client # Register for EVENT_READ events @@ -45,12 +49,21 @@ def handle_events( writables: Writables) -> bool: """Return True to shutdown work.""" if self.client.connection in readables: - data = self.client.recv() - if data is None: - # Client closed connection, signal shutdown + try: + data = self.client.recv() + if data is None: + # Client closed connection, signal shutdown + print( + 'Connection closed by client {0}'.format( + self.client.addr)) + return True + # Echo data back to client + self.client.queue(data) + except ConnectionResetError: + print( + 'Connection reset by client {0}'.format( + self.client.addr)) return True - # Queue data back to client - self.client.queue(data) if self.client.connection in writables: self.client.flush() @@ -61,7 +74,7 @@ def handle_events( def main() -> None: # This example requires `threadless=True` pool = AcceptorPool( - flags=Flags(num_workers=1, threadless=True), + flags=Flags(port=12345, num_workers=1, threadless=True), work_klass=EchoServerHandler) try: pool.setup() diff --git a/examples/websocket_client.py b/examples/websocket_client.py index 5509d10da..c87ed3e4f 100644 --- a/examples/websocket_client.py +++ b/examples/websocket_client.py @@ -22,8 +22,10 @@ def on_message(frame: WebsocketFrame) -> None: """WebsocketClient on_message callback.""" global client, num_echos, last_dispatch_time - print('Received %r after %d millisec' % (frame.data, (time.time() - last_dispatch_time) * 1000)) - assert(frame.data == b'hello' and frame.opcode == websocketOpcodes.TEXT_FRAME) + print('Received %r after %d millisec' % + (frame.data, (time.time() - last_dispatch_time) * 1000)) + assert(frame.data == b'hello' and frame.opcode == + websocketOpcodes.TEXT_FRAME) if num_echos > 0: client.queue(static_frame) last_dispatch_time = time.time() @@ -34,7 +36,11 @@ def on_message(frame: WebsocketFrame) -> None: if __name__ == '__main__': # Constructor establishes socket connection - client = WebsocketClient(b'echo.websocket.org', 80, b'/', on_message=on_message) + client = WebsocketClient( + b'echo.websocket.org', + 80, + b'/', + on_message=on_message) # Perform handshake client.handshake() # Queue some data for client diff --git a/proxy/core/connection/client.py b/proxy/core/connection/client.py index 28995a58a..62597a10d 100644 --- a/proxy/core/connection/client.py +++ b/proxy/core/connection/client.py @@ -30,3 +30,15 @@ def connection(self) -> Union[ssl.SSLSocket, socket.socket]: if self._conn is None: raise TcpConnectionUninitializedException() return self._conn + + def wrap(self, keyfile: str, certfile: str) -> None: + self.connection.setblocking(True) + self.flush() + self._conn = ssl.wrap_socket( + self.connection, + server_side=True, + # ca_certs=self.flags.ca_cert_file, + certfile=certfile, + keyfile=keyfile, + ssl_version=ssl.PROTOCOL_TLS) + self.connection.setblocking(False) diff --git a/proxy/core/connection/server.py b/proxy/core/connection/server.py index cbb9806a9..c5636e6a9 100644 --- a/proxy/core/connection/server.py +++ b/proxy/core/connection/server.py @@ -8,8 +8,8 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -import socket import ssl +import socket from typing import Optional, Union, Tuple from .connection import TcpConnection, tcpConnectionTypes, TcpConnectionUninitializedException @@ -34,3 +34,14 @@ def connect(self) -> None: if self._conn is not None: return self._conn = new_socket_connection(self.addr) + + def wrap(self, hostname: str, ca_file: Optional[str]) -> None: + ctx = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, cafile=ca_file) + ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 + ctx.check_hostname = True + self.connection.setblocking(True) + self._conn = ctx.wrap_socket( + self.connection, + server_hostname=hostname) + self.connection.setblocking(False) diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 3fad47e7f..c5c9a75ff 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -453,31 +453,15 @@ def generate_upstream_certificate( def wrap_server(self) -> None: assert self.server is not None assert isinstance(self.server.connection, socket.socket) - ctx = ssl.create_default_context( - ssl.Purpose.SERVER_AUTH, cafile=self.flags.ca_file) - ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 - ctx.check_hostname = True - self.server.connection.setblocking(True) - self.server._conn = ctx.wrap_socket( - self.server.connection, - server_hostname=text_(self.request.host)) - self.server.connection.setblocking(False) + self.server.wrap(text_(self.request.host), self.flags.ca_file) + assert isinstance(self.server.connection, ssl.SSLSocket) def wrap_client(self) -> None: - assert self.server is not None + assert self.server is not None and self.flags.ca_signing_key_file is not None assert isinstance(self.server.connection, ssl.SSLSocket) generated_cert = self.generate_upstream_certificate( cast(Dict[str, Any], self.server.connection.getpeercert())) - self.client.connection.setblocking(True) - self.client.flush() - 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_TLS) - self.client.connection.setblocking(False) + self.client.wrap(self.flags.ca_signing_key_file, generated_cert) logger.debug( 'TLS interception using %s', generated_cert) diff --git a/proxy/http/websocket/client.py b/proxy/http/websocket/client.py index 16e440dec..716d0faea 100644 --- a/proxy/http/websocket/client.py +++ b/proxy/http/websocket/client.py @@ -52,7 +52,11 @@ def handshake(self) -> None: def upgrade(self) -> None: key = base64.b64encode(secrets.token_bytes(16)) - self.sock.send(build_websocket_handshake_request(key, url=self.path, host=self.hostname)) + self.sock.send( + build_websocket_handshake_request( + key, + url=self.path, + host=self.hostname)) response = HttpParser(httpParserTypes.RESPONSE_PARSER) response.parse(self.sock.recv(DEFAULT_BUFFER_SIZE)) accept = response.header(b'Sec-Websocket-Accept') diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py index 2790f8e53..c904e69c8 100644 --- a/tests/http/test_http_proxy_tls_interception.py +++ b/tests/http/test_http_proxy_tls_interception.py @@ -17,7 +17,7 @@ from typing import Any from unittest import mock -from proxy.core.connection import TcpClientConnection +from proxy.core.connection import TcpClientConnection, TcpServerConnection from proxy.http.handler import HttpProtocolHandler from proxy.http.proxy import HttpProxyPlugin from proxy.http.methods import httpMethods @@ -71,6 +71,11 @@ def mock_connection() -> Any: return ssl_connection return plain_connection + # Do not mock the original wrap method + self.mock_server_conn.return_value.wrap.side_effect = \ + lambda x, y: TcpServerConnection.wrap( + self.mock_server_conn.return_value, x, y) + type(self.mock_server_conn.return_value).connection = \ mock.PropertyMock(side_effect=mock_connection) diff --git a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py index 0c1ec0a46..f3289e0dd 100644 --- a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py +++ b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py @@ -19,7 +19,7 @@ from proxy.common.utils import bytes_ from proxy.common.flags import Flags from proxy.common.utils import build_http_request, build_http_response -from proxy.core.connection import TcpClientConnection +from proxy.core.connection import TcpClientConnection, TcpServerConnection from proxy.http.codes import httpStatusCodes from proxy.http.methods import httpMethods from proxy.http.handler import HttpProtocolHandler @@ -98,6 +98,10 @@ def mock_connection() -> Any: return self.server_ssl_connection return self._conn + # Do not mock the original wrap method + self.server.wrap.side_effect = \ + lambda x, y: TcpServerConnection.wrap(self.server, x, y) + self.server.has_buffer.side_effect = has_buffer type(self.server).closed = mock.PropertyMock(side_effect=closed) type( From b7c4c5f17511dd610bd75c073ac08d8b7c7c2b53 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 8 Jul 2020 16:38:58 +0530 Subject: [PATCH 12/79] Add SSL echo server & client example (#395) * Move wrap_socket for SSL server within utils. Also complete proxy.common.pki gen_csr and sign_csr actions. Used by Makefile sign-https-certificates. * Add SSL echo server and client example * Add examples documentation --- Makefile | 16 ++++++ README.md | 16 ++++++ examples/README.md | 84 ++++++++++++++++++++++++++++- examples/ssl_echo_client.py | 29 ++++++++++ examples/ssl_echo_server.py | 104 ++++++++++++++++++++++++++++++++++++ examples/tcp_echo_server.py | 3 ++ proxy/common/pki.py | 28 ++++++++++ proxy/common/utils.py | 16 ++++++ proxy/http/handler.py | 13 +---- proxy/http/proxy/server.py | 1 + 10 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 examples/ssl_echo_client.py create mode 100644 examples/ssl_echo_server.py diff --git a/Makefile b/Makefile index 2fac9c297..92ae5d0e6 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,8 @@ IMAGE_TAG := $(NS)/$(IMAGE_NAME):$(VERSION) HTTPS_KEY_FILE_PATH := https-key.pem HTTPS_CERT_FILE_PATH := https-cert.pem +HTTPS_CSR_FILE_PATH := https-csr.pem +HTTPS_SIGNED_CERT_FILE_PATH := https-signed-cert.pem CA_KEY_FILE_PATH := ca-key.pem CA_CERT_FILE_PATH := ca-cert.pem @@ -41,6 +43,20 @@ https-certificates: --private-key-path $(HTTPS_KEY_FILE_PATH) \ --public-key-path $(HTTPS_CERT_FILE_PATH) +sign-https-certificates: + # Generate CSR request + python -m proxy.common.pki gen_csr \ + --csr-path $(HTTPS_CSR_FILE_PATH) \ + --private-key-path $(HTTPS_KEY_FILE_PATH) \ + --public-key-path $(HTTPS_CERT_FILE_PATH) + # Sign CSR with CA + python -m proxy.common.pki sign_csr \ + --csr-path $(HTTPS_CSR_FILE_PATH) \ + --crt-path $(HTTPS_SIGNED_CERT_FILE_PATH) \ + --hostname example.com \ + --private-key-path $(CA_KEY_FILE_PATH) \ + --public-key-path $(CA_CERT_FILE_PATH) + ca-certificates: # Generate CA key python -m proxy.common.pki gen_private_key \ diff --git a/README.md b/README.md index 3e2faa62e..81f69b5e6 100644 --- a/README.md +++ b/README.md @@ -827,6 +827,22 @@ Verify using `curl -x https://localhost:8899 --proxy-cacert https-cert.pem https } ``` +If you want to avoid passing `--proxy-cacert` flag, also consider signing generated SSL certificates. Example: + +First, generate CA certificates: + +```bash +make ca-certificates +``` + +Then, sign SSL certificate: + +```bash +make sign-https-certificates +``` + +Now restart the server with `--cert-file https-signed-cert.pem` flag. Note that you must also trust generated `ca-cert.pem` in your system keychain. + TLS Interception ================= diff --git a/examples/README.md b/examples/README.md index 82765d958..64fe17cdd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,87 @@ -# Proxy.py Library Examples +# Proxy Library Examples This directory contains examples that demonstrate `proxy.py` core library capabilities. Looking for `proxy.py` plugin examples? Check [proxy/plugin](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) directory. + +## WebSocket Client + +1. Makes use of `proxy.http.websocket.WebsocketClient` which is built on-top of `asyncio` +2. `websocket_client.py` by default opens a WebSocket connection to `ws://echo.websocket.org`. +3. Client will exchange `num_echos = 10` packets with the server and then shutdown. + +Start `websocket_client.py` as: + +```bash +❯ PYTHONPATH=. python examples/websocket_client.py +Received b'hello' after 306 millisec +Received b'hello' after 308 millisec +Received b'hello' after 277 millisec +Received b'hello' after 334 millisec +Received b'hello' after 296 millisec +Received b'hello' after 317 millisec +Received b'hello' after 307 millisec +Received b'hello' after 307 millisec +Received b'hello' after 306 millisec +Received b'hello' after 307 millisec +Received b'hello' after 309 millisec +``` + +## TCP Echo Server + +1. Makes use of `proxy.core.acceptor.AcceptorPool`, same multicore acceptor used internally by `proxy.py` server. +2. Implements `proxy.core.acceptor.Work` interface to handle incoming client connections. + +Start `tcp_echo_server.py` as: + +```bash +❯ PYTHONPATH=. python examples/tcp_echo_server.py +Connection accepted from ('::1', 53285, 0, 0) +Connection closed by client ('::1', 53285, 0, 0) +``` + +## TCP Echo Client + +1. Makes use of `proxy.common.utils.socket_connection` to establish a TCP socket connection with our TCP echo server. +2. Exchanges packet with server in an infinite loop. Press `CTRL+C` to stop. + +Start `tcp_echo_client.py` as: + +```bash +❯ PYTHONPATH=. python examples/tcp_echo_client.py +b'hello' +b'hello' +b'hello' +b'hello' +b'hello' +... +... +... +^CTraceback (most recent call last): + File "examples/tcp_echo_client.py", line 18, in + data = client.recv(DEFAULT_BUFFER_SIZE) +KeyboardInterrupt +``` + +## SSL Echo Server + +1. Same as `tcp_echo_server.py`. +2. Internally uses `proxy.common.utils.wrap_socket` to enable SSL encryption. +3. Uses `https-key.pem` and `https-signed-cert.pem` for SSL encryption. + +Start `ssl_echo_server.py` as: + +```bash +❯ PYTHONPATH=. python examples/ssl_echo_server.py +``` + +## SSL Echo Client + +1. Makes use of `proxy.core.connection.TcpServerConnection` to establish a SSL connection with our `ssl_echo_server.py`. +2. Uses generated `ca-cert.pem` for SSL certificate verification. + +Start `ssl_echo_client.py` as: + +```bash +❯ PYTHONPATH=. python examples/ssl_echo_client.py +``` diff --git a/examples/ssl_echo_client.py b/examples/ssl_echo_client.py new file mode 100644 index 000000000..227b26c94 --- /dev/null +++ b/examples/ssl_echo_client.py @@ -0,0 +1,29 @@ +# -*- 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. +""" +from proxy.core.connection import TcpServerConnection +from proxy.common.constants import DEFAULT_BUFFER_SIZE + +if __name__ == '__main__': + client = TcpServerConnection('::', 12345) + client.connect() + client.wrap('example.com', ca_file='ca-cert.pem') + # wrap() will by default set connection to nonblocking + # flip it back to blocking + client.connection.setblocking(True) + try: + while True: + client.send(b'hello') + data = client.recv(DEFAULT_BUFFER_SIZE) + if data is None: + break + print(data.tobytes()) + finally: + client.close() diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py new file mode 100644 index 000000000..98401d587 --- /dev/null +++ b/examples/ssl_echo_server.py @@ -0,0 +1,104 @@ +# -*- 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 time +import socket +import selectors + +from typing import Dict, Any + +from proxy.core.acceptor import AcceptorPool, Work +from proxy.core.connection import TcpClientConnection +from proxy.common.flags import Flags +from proxy.common.types import Readables, Writables +from proxy.common.utils import wrap_socket + + +class EchoSSLServerHandler(Work): + """EchoSSLServerHandler implements Work interface. + + An instance of EchoServerHandler is created for each client + connection. EchoServerHandler lifecycle is controlled by + Threadless core using asyncio. Implementation must provide + get_events and handle_events method. Optionally, also implement + intialize, is_inactive and shutdown method. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + print('Connection accepted from {0}'.format(self.client.addr)) + + def initialize(self) -> None: + # Acceptors don't perform TLS handshake. Perform the same + # here using wrap_socket() utility. + assert self.flags.keyfile is not None and self.flags.certfile is not None + conn = wrap_socket(self.client.connection, self.flags.keyfile, self.flags.certfile) + conn.setblocking(False) + # Upgrade plain TcpClientConnection to SSL connection object + self.client = TcpClientConnection(conn=conn, addr=self.client.addr) + + def get_events(self) -> Dict[socket.socket, int]: + # We always want to read from client + # Register for EVENT_READ events + events = {self.client.connection: selectors.EVENT_READ} + # If there is pending buffer for client + # also register for EVENT_WRITE events + if self.client.has_buffer(): + events[self.client.connection] |= selectors.EVENT_WRITE + return events + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Return True to shutdown work.""" + if self.client.connection in readables: + try: + data = self.client.recv() + if data is None: + # Client closed connection, signal shutdown + print( + 'Connection closed by client {0}'.format( + self.client.addr)) + return True + # Echo data back to client + self.client.queue(data) + except ConnectionResetError: + print( + 'Connection reset by client {0}'.format( + self.client.addr)) + return True + + if self.client.connection in writables: + self.client.flush() + + return False + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Flags( + port=12345, + num_workers=1, + threadless=True, + keyfile='https-key.pem', + certfile='https-signed-cert.pem'), + work_klass=EchoSSLServerHandler) + try: + pool.setup() + while True: + time.sleep(1) + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py index 031963de7..be75bf34a 100644 --- a/examples/tcp_echo_server.py +++ b/examples/tcp_echo_server.py @@ -33,6 +33,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) print('Connection accepted from {0}'.format(self.client.addr)) + def initialize(self) -> None: + self.client.connection.setblocking(False) + def get_events(self) -> Dict[socket.socket, int]: # We always want to read from client # Register for EVENT_READ events diff --git a/proxy/common/pki.py b/proxy/common/pki.py index 6e6e512e3..80711fd4c 100644 --- a/proxy/common/pki.py +++ b/proxy/common/pki.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import time import sys import argparse import contextlib @@ -256,6 +257,24 @@ def run_openssl_command(command: List[str], timeout: int) -> bool: default='/CN=example.com', help='Subject to use for public key generation. Default: /CN=example.com', ) + parser.add_argument( + '--csr-path', + type=str, + default=None, + help='CSR file path. Use with gen_csr and sign_csr action.', + ) + parser.add_argument( + '--crt-path', + type=str, + default=None, + help='Signed certificate path. Use with sign_csr action.', + ) + parser.add_argument( + '--hostname', + type=str, + default=None, + help='Alternative subject names to use during CSR signing.', + ) args = parser.parse_args(sys.argv[1:]) # Validation @@ -280,3 +299,12 @@ def run_openssl_command(command: List[str], timeout: int) -> bool: elif args.action == 'remove_passphrase': remove_passphrase(args.private_key_path, args.password, args.private_key_path) + elif args.action == 'gen_csr': + gen_csr( + args.csr_path, + args.private_key_path, + args.password, + args.public_key_path) + elif args.action == 'sign_csr': + sign_csr(args.csr_path, args.crt_path, args.private_key_path, args.password, + args.public_key_path, str(int(time.time())), alt_subj_names=[args.hostname, ]) diff --git a/proxy/common/utils.py b/proxy/common/utils.py index 078bbbcbe..3edc63274 100644 --- a/proxy/common/utils.py +++ b/proxy/common/utils.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import ssl import contextlib import functools import ipaddress @@ -150,6 +151,21 @@ def find_http_line(raw: bytes) -> Tuple[Optional[bytes], bytes]: return line, rest +def wrap_socket(conn: socket.socket, keyfile: str, + certfile: str) -> ssl.SSLSocket: + ctx = ssl.create_default_context( + ssl.Purpose.CLIENT_AUTH) + ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + ctx.verify_mode = ssl.CERT_NONE + ctx.load_cert_chain( + certfile=certfile, + keyfile=keyfile) + return ctx.wrap_socket( + conn, + server_side=True, + ) + + def new_socket_connection( addr: Tuple[str, int], timeout: int = DEFAULT_TIMEOUT) -> socket.socket: conn = None diff --git a/proxy/http/handler.py b/proxy/http/handler.py index 4a1b67be9..c93cae5f6 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -25,6 +25,7 @@ from ..common.flags import Flags from ..common.types import Readables, Writables +from ..common.utils import wrap_socket from ..core.acceptor.work import Work from ..core.event import EventQueue from ..core.connection import TcpClientConnection @@ -162,18 +163,8 @@ def optionally_wrap_socket( Shutdown and closes client connection upon error. """ if self.flags.encryption_enabled(): - ctx = ssl.create_default_context( - ssl.Purpose.CLIENT_AUTH) - ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 - ctx.verify_mode = ssl.CERT_NONE assert self.flags.keyfile and self.flags.certfile - ctx.load_cert_chain( - certfile=self.flags.certfile, - keyfile=self.flags.keyfile) - conn = ctx.wrap_socket( - conn, - server_side=True, - ) + conn = wrap_socket(conn, self.flags.keyfile, self.flags.certfile) return conn def connection_inactive_for(self) -> float: diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index c5c9a75ff..1c95c7896 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -380,6 +380,7 @@ def gen_ca_signed_certificate( '{0}.{1}'.format(text_(self.request.host), 'pub')) private_key_path = self.flags.ca_signing_key_file private_key_password = '' + # Build certificate subject keys = { 'CN': 'commonName', From 73bcf1d0577c52a7dbb97fc2cd4689e5dff7100a Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Thu, 9 Jul 2020 21:31:29 +0530 Subject: [PATCH 13/79] Add core pubsub eventing example and add menubar item skeleton (#396) * Initialize menu bar items with click handler and open a popover for preferences * Add Core PubSub eventing example * Remove hardcoded request ids --- examples/README.md | 30 +++++ examples/base_echo_server.py | 72 ++++++++++++ examples/pubsub_eventing.py | 107 ++++++++++++++++++ examples/ssl_echo_server.py | 67 ++--------- examples/tcp_echo_server.py | 59 +--------- .../UserInterfaceState.xcuserstate | Bin 20890 -> 24880 bytes menubar/proxy.py/AppDelegate.swift | 75 ++++++++++-- menubar/proxy.py/Info.plist | 2 +- proxy/core/event/dispatcher.py | 2 + proxy/core/event/subscriber.py | 8 +- 10 files changed, 298 insertions(+), 124 deletions(-) create mode 100644 examples/base_echo_server.py create mode 100644 examples/pubsub_eventing.py diff --git a/examples/README.md b/examples/README.md index 64fe17cdd..5594a4f95 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,6 +4,15 @@ This directory contains examples that demonstrate `proxy.py` core library capabi Looking for `proxy.py` plugin examples? Check [proxy/plugin](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) directory. +Table of Contents +================= +* [WebSocket Client](#websocket-client) +* [TCP Echo Server](#tcp-echo-server) +* [TCP Echo Client](#tcp-echo-client) +* [SSL Echo Server](#ssl-echo-server) +* [SSL Echo Client](#ssl-echo-client) +* [PubSub Eventing](#pubsub-eventing) + ## WebSocket Client 1. Makes use of `proxy.http.websocket.WebsocketClient` which is built on-top of `asyncio` @@ -85,3 +94,24 @@ Start `ssl_echo_client.py` as: ```bash ❯ PYTHONPATH=. python examples/ssl_echo_client.py ``` + +## PubSub Eventing + +1. Makes use of `proxy.py` core eventing module. +2. A `proxy.core.event.EventDispatcher` thread is started. +3. A `proxy.core.event.EventSubscriber` thread is started. +4. A `multiprocessing.Process` publisher is started. +5. Main thread also publishes into `EventDispatcher` queue. +6. Events from both the main thread and another process are received by the subscriber. + +Start `pubsub_eventing.py` as: + +```bash +❯ PYTHONPATH=. python examples/pubsub_eventing.py +DEBUG:proxy.core.event.subscriber:Subscribed relay sub id 5eb22010764f4d44900f41e2fb408ca6 from core events +publisher starting +^Cpublisher shutdown +bye!!! +DEBUG:proxy.core.event.subscriber:Un-subscribed relay sub id 5eb22010764f4d44900f41e2fb408ca6 from core events +Received 52724 events from main thread, 60172 events from another process, in 21.50117802619934 seconds +``` diff --git a/examples/base_echo_server.py b/examples/base_echo_server.py new file mode 100644 index 000000000..70e875bd6 --- /dev/null +++ b/examples/base_echo_server.py @@ -0,0 +1,72 @@ +# -*- 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 +import selectors + +from typing import Dict, Any + +from proxy.core.acceptor import Work +from proxy.common.types import Readables, Writables + + +class BaseEchoServerHandler(Work): + """BaseEchoServerHandler implements Work interface. + + An instance of EchoServerHandler is created for each client + connection. EchoServerHandler lifecycle is controlled by + Threadless core using asyncio. Implementation must provide + get_events and handle_events method. Optionally, also implement + intialize, is_inactive and shutdown method. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + print('Connection accepted from {0}'.format(self.client.addr)) + + def initialize(self) -> None: + pass + + def get_events(self) -> Dict[socket.socket, int]: + # We always want to read from client + # Register for EVENT_READ events + events = {self.client.connection: selectors.EVENT_READ} + # If there is pending buffer for client + # also register for EVENT_WRITE events + if self.client.has_buffer(): + events[self.client.connection] |= selectors.EVENT_WRITE + return events + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Return True to shutdown work.""" + if self.client.connection in readables: + try: + data = self.client.recv() + if data is None: + # Client closed connection, signal shutdown + print( + 'Connection closed by client {0}'.format( + self.client.addr)) + return True + # Echo data back to client + self.client.queue(data) + except ConnectionResetError: + print( + 'Connection reset by client {0}'.format( + self.client.addr)) + return True + + if self.client.connection in writables: + self.client.flush() + + return False diff --git a/examples/pubsub_eventing.py b/examples/pubsub_eventing.py new file mode 100644 index 000000000..3e247c38e --- /dev/null +++ b/examples/pubsub_eventing.py @@ -0,0 +1,107 @@ +# -*- 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 time +import threading +import multiprocessing +import logging + +from typing import Dict, Any + +from proxy.core.event import EventQueue, EventSubscriber, EventDispatcher, eventNames + +# Enable debug logging to view core event logs +logging.basicConfig(level=logging.DEBUG) + +# Eventing requires a multiprocess safe queue +# so that events can be safely published and received +# between processes. +manager = multiprocessing.Manager() + +main_publisher_request_id = '1234' +process_publisher_request_id = '12345' +num_events_received = [0, 0] + + +def publisher_process(shutdown_event: multiprocessing.synchronize.Event, + dispatcher_queue: EventQueue) -> None: + print('publisher starting') + try: + while not shutdown_event.is_set(): + dispatcher_queue.publish( + request_id=process_publisher_request_id, + event_name=eventNames.WORK_STARTED, + event_payload={'time': time.time()}, + publisher_id='eventing_pubsub_process' + ) + except KeyboardInterrupt: + pass + print('publisher shutdown') + + +def on_event(payload: Dict[str, Any]) -> None: + '''Subscriber callback.''' + global num_events_received + if payload['request_id'] == main_publisher_request_id: + num_events_received[0] += 1 + else: + num_events_received[1] += 1 + # print(payload) + + +if __name__ == '__main__': + start_time = time.time() + + # Start dispatcher thread + dispatcher_queue = EventQueue(manager.Queue()) + dispatcher_shutdown_event = threading.Event() + dispatcher = EventDispatcher( + shutdown=dispatcher_shutdown_event, + event_queue=dispatcher_queue) + dispatcher_thread = threading.Thread(target=dispatcher.run) + dispatcher_thread.start() + + # Create a subscriber + subscriber = EventSubscriber(dispatcher_queue) + # Internally, subscribe will start a separate thread + # to receive incoming published messages + subscriber.subscribe(on_event) + + # Start a publisher process to demonstrate safe exchange + # of messages between processes. + publisher_shutdown_event = multiprocessing.Event() + publisher = multiprocessing.Process( + target=publisher_process, args=( + publisher_shutdown_event, dispatcher_queue, )) + publisher.start() + + try: + while True: + # Dispatch event from main process + dispatcher_queue.publish( + request_id=main_publisher_request_id, + event_name=eventNames.WORK_STARTED, + event_payload={'time': time.time()}, + publisher_id='eventing_pubsub_main' + ) + except KeyboardInterrupt: + print('bye!!!') + finally: + # Stop publisher + publisher_shutdown_event.set() + publisher.join() + # Stop subscriber thread + subscriber.unsubscribe() + # Signal dispatcher to shutdown + dispatcher_shutdown_event.set() + # Wait for dispatcher shutdown + dispatcher_thread.join() + print('Received {0} events from main thread, {1} events from another process, in {2} seconds'.format( + num_events_received[0], num_events_received[1], time.time() - start_time)) diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py index 98401d587..1bb401bfc 100644 --- a/examples/ssl_echo_server.py +++ b/examples/ssl_echo_server.py @@ -9,77 +9,30 @@ :license: BSD, see LICENSE for more details. """ import time -import socket -import selectors -from typing import Dict, Any - -from proxy.core.acceptor import AcceptorPool, Work +from proxy.core.acceptor import AcceptorPool from proxy.core.connection import TcpClientConnection from proxy.common.flags import Flags -from proxy.common.types import Readables, Writables from proxy.common.utils import wrap_socket +from examples.base_echo_server import BaseEchoServerHandler -class EchoSSLServerHandler(Work): - """EchoSSLServerHandler implements Work interface. - - An instance of EchoServerHandler is created for each client - connection. EchoServerHandler lifecycle is controlled by - Threadless core using asyncio. Implementation must provide - get_events and handle_events method. Optionally, also implement - intialize, is_inactive and shutdown method. - """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - print('Connection accepted from {0}'.format(self.client.addr)) +class EchoSSLServerHandler(BaseEchoServerHandler): # type: ignore + """Wraps client socket during initialization.""" def initialize(self) -> None: # Acceptors don't perform TLS handshake. Perform the same # here using wrap_socket() utility. assert self.flags.keyfile is not None and self.flags.certfile is not None - conn = wrap_socket(self.client.connection, self.flags.keyfile, self.flags.certfile) + conn = wrap_socket( + self.client.connection, # type: ignore + self.flags.keyfile, + self.flags.certfile) conn.setblocking(False) # Upgrade plain TcpClientConnection to SSL connection object - self.client = TcpClientConnection(conn=conn, addr=self.client.addr) - - def get_events(self) -> Dict[socket.socket, int]: - # We always want to read from client - # Register for EVENT_READ events - events = {self.client.connection: selectors.EVENT_READ} - # If there is pending buffer for client - # also register for EVENT_WRITE events - if self.client.has_buffer(): - events[self.client.connection] |= selectors.EVENT_WRITE - return events - - def handle_events( - self, - readables: Readables, - writables: Writables) -> bool: - """Return True to shutdown work.""" - if self.client.connection in readables: - try: - data = self.client.recv() - if data is None: - # Client closed connection, signal shutdown - print( - 'Connection closed by client {0}'.format( - self.client.addr)) - return True - # Echo data back to client - self.client.queue(data) - except ConnectionResetError: - print( - 'Connection reset by client {0}'.format( - self.client.addr)) - return True - - if self.client.connection in writables: - self.client.flush() - - return False + self.client = TcpClientConnection( + conn=conn, addr=self.client.addr) # type: ignore def main() -> None: diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py index be75bf34a..fa5da58af 100644 --- a/examples/tcp_echo_server.py +++ b/examples/tcp_echo_server.py @@ -9,70 +9,19 @@ :license: BSD, see LICENSE for more details. """ import time -import socket -import selectors -from typing import Dict, Any - -from proxy.core.acceptor import AcceptorPool, Work +from proxy.core.acceptor import AcceptorPool from proxy.common.flags import Flags -from proxy.common.types import Readables, Writables - -class EchoServerHandler(Work): - """EchoServerHandler implements Work interface. +from examples.base_echo_server import BaseEchoServerHandler - An instance of EchoServerHandler is created for each client - connection. EchoServerHandler lifecycle is controlled by - Threadless core using asyncio. Implementation must provide - get_events and handle_events method. Optionally, also implement - intialize, is_inactive and shutdown method. - """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - print('Connection accepted from {0}'.format(self.client.addr)) +class EchoServerHandler(BaseEchoServerHandler): # type: ignore + """Sets client socket to non-blocking during initialization.""" def initialize(self) -> None: self.client.connection.setblocking(False) - def get_events(self) -> Dict[socket.socket, int]: - # We always want to read from client - # Register for EVENT_READ events - events = {self.client.connection: selectors.EVENT_READ} - # If there is pending buffer for client - # also register for EVENT_WRITE events - if self.client.has_buffer(): - events[self.client.connection] |= selectors.EVENT_WRITE - return events - - def handle_events( - self, - readables: Readables, - writables: Writables) -> bool: - """Return True to shutdown work.""" - if self.client.connection in readables: - try: - data = self.client.recv() - if data is None: - # Client closed connection, signal shutdown - print( - 'Connection closed by client {0}'.format( - self.client.addr)) - return True - # Echo data back to client - self.client.queue(data) - except ConnectionResetError: - print( - 'Connection reset by client {0}'.format( - self.client.addr)) - return True - - if self.client.connection in writables: - self.client.flush() - - return False - def main() -> None: # This example requires `threadless=True` 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 index f0463297602775a0e1fc5a2ea2fc4630fc2cade6..c7640e77c75fa5390319d04a9c3e65e858726da2 100644 GIT binary patch delta 11340 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 al5g6|OrO@U*kkn1ZhDfSgJYpC!R#ydwj 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 $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2019 Abhinav Singh. All rights reserved. + Copyright © 2013-present by Abhinav Singh and contributors. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/proxy/core/event/dispatcher.py b/proxy/core/event/dispatcher.py index f6bb849e5..d978bebe0 100644 --- a/proxy/core/event/dispatcher.py +++ b/proxy/core/event/dispatcher.py @@ -83,6 +83,8 @@ def run(self) -> None: self.run_once() except queue.Empty: pass + except BrokenPipeError: + pass except EOFError: pass except KeyboardInterrupt: diff --git a/proxy/core/event/subscriber.py b/proxy/core/event/subscriber.py index ec6afe623..90648e0d8 100644 --- a/proxy/core/event/subscriber.py +++ b/proxy/core/event/subscriber.py @@ -57,7 +57,13 @@ def unsubscribe(self) -> None: assert self.relay_channel assert self.relay_sub_id - self.event_queue.unsubscribe(self.relay_sub_id) + try: + self.event_queue.unsubscribe(self.relay_sub_id) + except BrokenPipeError: + pass + except EOFError: + pass + self.relay_shutdown.set() self.relay_thread.join() logger.debug( From af3bdc23e782925d87d03c2731543b9380e1b1b8 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 12 Jul 2020 20:36:34 +0530 Subject: [PATCH 14/79] Move codecov.yml to top level directory (#400) --- tests/codecov.yml => codecov.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/codecov.yml => codecov.yml (100%) diff --git a/tests/codecov.yml b/codecov.yml similarity index 100% rename from tests/codecov.yml rename to codecov.yml From 1867d58338e5e4d1c3a6ae9801ebfbef49a848d8 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 12 Jul 2020 21:24:20 +0530 Subject: [PATCH 15/79] Add cross ref for how to generate SSL certs. (#401) --- examples/README.md | 2 +- proxy/plugin/__init__.py | 2 +- ...modify_chunk_response_plugin.py => modify_chunk_response.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename proxy/plugin/{modify_chunk_response_plugin.py => modify_chunk_response.py} (100%) diff --git a/examples/README.md b/examples/README.md index 5594a4f95..69e3084c5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -76,7 +76,7 @@ KeyboardInterrupt 1. Same as `tcp_echo_server.py`. 2. Internally uses `proxy.common.utils.wrap_socket` to enable SSL encryption. -3. Uses `https-key.pem` and `https-signed-cert.pem` for SSL encryption. +3. Uses `https-key.pem` and `https-signed-cert.pem` for SSL encryption. See [End-to-End Encryption](https://github.com/abhinavsingh/proxy.py#end-to-end-encryption) for instructions on how to generate SSL certificates. Start `ssl_echo_server.py` as: diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index 16954c2f2..eca6f399e 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -19,7 +19,7 @@ from .reverse_proxy import ReverseProxyPlugin from .proxy_pool import ProxyPoolPlugin from .filter_by_client_ip import FilterByClientIpPlugin -from .modify_chunk_response_plugin import ModifyChunkResponsePlugin +from .modify_chunk_response import ModifyChunkResponsePlugin __all__ = [ 'CacheResponsesPlugin', diff --git a/proxy/plugin/modify_chunk_response_plugin.py b/proxy/plugin/modify_chunk_response.py similarity index 100% rename from proxy/plugin/modify_chunk_response_plugin.py rename to proxy/plugin/modify_chunk_response.py From aedf5933f23ecc62e1d9c13effd2e9f938b8ceb8 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 13 Jul 2020 13:10:34 +0800 Subject: [PATCH 16/79] Add plugin "FilterByURLRegexPlugin" (#397) * Initial draft of filter_by_url_regex.py * Add FilterByURLRegexPlugin * Fix dictionary key & add logging * Add proper logging * Add better logging * Add logging * move code to handle_client_request * development logging * development * development * development * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * Fix blocked log * Add to FILTER_LIST, some tidy up * Update FILTER_LIST * dev * remove scheme from url * Add to FILTER_LIST * Add to FILTER_LIST * Update FILTER_LIST * commenting * Update FILTER_LIST * After autopep8 * Fix Anomalous backslash in string (pep8) * Address code quality checks - flake8 F401 & W605 * Address flake8 errors * Attempt to fix flake8 errors * Fix linting issues * Address flake8 W292 * Attempt to create tests * Add FilterByURLRegexPlugin * Rename test * Work on tests * Work on tests * Work on tests Co-authored-by: Abhinav Singh --- proxy/plugin/__init__.py | 2 + proxy/plugin/filter_by_url_regex.py | 136 ++++++++++++++++++++++++ tests/plugin/test_http_proxy_plugins.py | 27 +++++ tests/plugin/utils.py | 4 +- 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 proxy/plugin/filter_by_url_regex.py diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index eca6f399e..8450e8f66 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -19,6 +19,7 @@ from .reverse_proxy import ReverseProxyPlugin from .proxy_pool import ProxyPoolPlugin from .filter_by_client_ip import FilterByClientIpPlugin +from .filter_by_url_regex import FilterByURLRegexPlugin from .modify_chunk_response import ModifyChunkResponsePlugin __all__ = [ @@ -35,4 +36,5 @@ 'ProxyPoolPlugin', 'FilterByClientIpPlugin', 'ModifyChunkResponsePlugin', + 'FilterByURLRegexPlugin', ] diff --git a/proxy/plugin/filter_by_url_regex.py b/proxy/plugin/filter_by_url_regex.py new file mode 100644 index 000000000..a4106c94f --- /dev/null +++ b/proxy/plugin/filter_by_url_regex.py @@ -0,0 +1,136 @@ +# -*- 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 logging + +from typing import Optional, List, Dict, Any + +from ..http.exception import HttpRequestRejected +from ..http.parser import HttpParser +from ..http.codes import httpStatusCodes +from ..http.proxy import HttpProxyBasePlugin +from ..common.utils import text_ + +import re + +logger = logging.getLogger(__name__) + + +class FilterByURLRegexPlugin(HttpProxyBasePlugin): + """ + Drop traffic by inspecting request URL, + checking against a list of regular expressions, + then returning a HTTP status code. + """ + + FILTER_LIST: List[Dict[str, Any]] = [ + { + 'regex': b'tpc.googlesyndication.com/simgad/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google image ads', + }, + { + 'regex': b'tpc.googlesyndication.com/sadbundle/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google animated ad bundles', + }, + { + 'regex': b'pagead\\d+.googlesyndication.com/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google tracking', + }, + { + 'regex': b'(www){0,1}.google-analytics.com/r/collect\\?.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google tracking', + }, + { + 'regex': b'(www){0,1}.facebook.com/tr/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Facebook tracking', + }, + { + 'regex': b'tpc.googlesyndication.com/daca_images/simgad/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google image ads', + }, + { + 'regex': b'.*.2mdn.net/videoplayback/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Twitch.tv video ads', + }, + { + 'regex': b'(www.){0,1}google.com(.*)/pagead/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google ads', + }, + ] + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + + # determine host + request_host = None + if request.host: + request_host = request.host + else: + if b'host' in request.headers: + request_host = request.header(b'host') + + if not request_host: + logger.error("Cannot determine host") + return request + + # build URL + url = b'%s%s' % ( + request_host, + request.path, + ) + + # check URL against list + rule_number = 1 + for blocked_entry in self.FILTER_LIST: + + # if regex matches on URL + if re.search(text_(blocked_entry['regex']), text_(url)): + + # log that the request has been filtered + logger.info("Blocked: %r with status_code '%r' by rule number '%r'" % ( + text_(url), + blocked_entry['status_code'], + rule_number, + )) + + # close the connection with the status code from the filter + # list + raise HttpRequestRejected( + status_code=blocked_entry['status_code'], + headers={b'Connection': b'close'}, + reason=b'Blocked', + ) + + # stop looping through filter list + break + + # increment rule number + rule_number += 1 + + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass diff --git a/tests/plugin/test_http_proxy_plugins.py b/tests/plugin/test_http_proxy_plugins.py index 84ca5a697..51b3cf607 100644 --- a/tests/plugin/test_http_proxy_plugins.py +++ b/tests/plugin/test_http_proxy_plugins.py @@ -254,3 +254,30 @@ def closed() -> bool: httpStatusCodes.OK, reason=b'OK', body=b'Hello from man in the middle') ) + + @mock.patch('proxy.http.proxy.server.TcpServerConnection') + def test_filter_by_url_regex_plugin( + self, mock_server_conn: mock.Mock) -> None: + request = build_http_request( + b'GET', b'http://www.facebook.com/tr/', + headers={ + b'Host': b'www.facebook.com', + } + ) + self._conn.recv.return_value = request + self.mock_selector.return_value.select.side_effect = [ + [(selectors.SelectorKey( + fileobj=self._conn, + fd=self._conn.fileno, + events=selectors.EVENT_READ, + data=None), selectors.EVENT_READ)], ] + self.protocol_handler.run_once() + + self.assertEqual( + self.protocol_handler.client.buffer[0].tobytes(), + build_http_response( + status_code=httpStatusCodes.NOT_FOUND, + reason=b'Blocked', + headers={b'Connection': b'close'}, + ) + ) diff --git a/tests/plugin/utils.py b/tests/plugin/utils.py index 093436796..3e58ff12f 100644 --- a/tests/plugin/utils.py +++ b/tests/plugin/utils.py @@ -12,7 +12,7 @@ from proxy.http.proxy import HttpProxyBasePlugin from proxy.plugin import ModifyPostDataPlugin, ProposedRestApiPlugin, RedirectToCustomServerPlugin, \ - FilterByUpstreamHostPlugin, CacheResponsesPlugin, ManInTheMiddlePlugin + FilterByUpstreamHostPlugin, CacheResponsesPlugin, ManInTheMiddlePlugin, FilterByURLRegexPlugin def get_plugin_by_test_name(test_name: str) -> Type[HttpProxyBasePlugin]: @@ -29,4 +29,6 @@ def get_plugin_by_test_name(test_name: str) -> Type[HttpProxyBasePlugin]: plugin = CacheResponsesPlugin elif test_name == 'test_man_in_the_middle_plugin': plugin = ManInTheMiddlePlugin + elif test_name == 'test_filter_by_url_regex_plugin': + plugin = FilterByURLRegexPlugin return plugin From 1642677029f949ab1726e8eefe11ab19d8ed4f93 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 15 Jul 2020 07:26:39 +0200 Subject: [PATCH 17/79] Update tox from 3.16.1 to 3.17.0 (#402) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 768a9728c..42c767b9d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.7 -tox==3.16.1 +tox==3.17.0 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From 448c2eb36e1793e1de410fbf0d29ca225392a652 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Thu, 16 Jul 2020 07:42:50 +0200 Subject: [PATCH 18/79] Update codecov from 2.1.7 to 2.1.8 (#404) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 42c767b9d..bdf645e15 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -6,7 +6,7 @@ pytest-cov==2.10.0 autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 -codecov==2.1.7 +codecov==2.1.8 tox==3.17.0 mccabe==0.6.1 pylint==2.5.3 From 3da85a452d855c869add50996ff98e19767737ff Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Thu, 16 Jul 2020 07:44:48 +0200 Subject: [PATCH 19/79] Update tox from 3.17.0 to 3.17.1 (#403) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index bdf645e15..ebd8c6fdd 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.8 -tox==3.17.0 +tox==3.17.1 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From a33c9657f90692e1c508238c929d404e00bbee12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 12:28:55 +0530 Subject: [PATCH 20/79] Bump lodash from 4.17.15 to 4.17.19 in /dashboard (#405) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dashboard/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index a28f32795..0525427e8 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2331,9 +2331,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.sortby": { From 35d69205199c1e2950ed4189323a817e92451ccd Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Thu, 23 Jul 2020 11:39:37 +0200 Subject: [PATCH 21/79] Update tox from 3.17.1 to 3.18.0 (#406) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index ebd8c6fdd..1e99cdb49 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.8 -tox==3.17.1 +tox==3.18.0 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From d42a3762013650469e5762590a134030b255a9fa Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 24 Jul 2020 13:56:48 +0200 Subject: [PATCH 22/79] Update coverage from 5.2 to 5.2.1 (#407) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 1e99cdb49..b230ce8ce 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,5 +1,5 @@ python-coveralls==2.9.3 -coverage==5.2 +coverage==5.2.1 flake8==3.8.3 pytest==5.4.3 pytest-cov==2.10.0 From 4482838c7091f17f02ed682f67a9ebf672e0d60c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 28 Jul 2020 13:25:04 +0200 Subject: [PATCH 23/79] Update tox from 3.18.0 to 3.18.1 (#408) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index b230ce8ce..2a2312948 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.8 -tox==3.18.0 +tox==3.18.1 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From fe2b9a0c820df4737201307b8cd25b82dbb8ade6 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 9 Aug 2020 21:36:11 +0530 Subject: [PATCH 24/79] Fix docker build by using correct pip flags (#417) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fb079fcb5..d751a1201 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY README.md /app/ COPY proxy/ /app/proxy/ WORKDIR /app RUN pip install --upgrade pip && \ - pip install --install-option="--prefix=/deps" . + pip install --prefix=/deps . FROM base From cc36d4cd33f6e228c9e270213d9707a90c7fc08f Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 9 Aug 2020 18:21:30 +0200 Subject: [PATCH 25/79] Update tox from 3.18.1 to 3.19.0 (#416) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 2a2312948..7c0a8bfae 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.3 mypy==0.782 py-spy==0.3.3 codecov==2.1.8 -tox==3.18.1 +tox==3.19.0 mccabe==0.6.1 pylint==2.5.3 rope==0.17.0 From 1462ca4734aef235988359ef4dfd5b5d2b8a419b Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 9 Aug 2020 19:23:03 +0200 Subject: [PATCH 26/79] Update autopep8 from 1.5.3 to 1.5.4 (#412) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 7c0a8bfae..dcd8328f9 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -3,7 +3,7 @@ coverage==5.2.1 flake8==3.8.3 pytest==5.4.3 pytest-cov==2.10.0 -autopep8==1.5.3 +autopep8==1.5.4 mypy==0.782 py-spy==0.3.3 codecov==2.1.8 From 63917db7a1dafa8d56513d4eb278d06bff314579 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 9 Aug 2020 19:35:26 +0200 Subject: [PATCH 27/79] Update pytest from 5.4.3 to 6.0.1 (#410) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index dcd8328f9..fe5d3465b 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.2.1 flake8==3.8.3 -pytest==5.4.3 +pytest==6.0.1 pytest-cov==2.10.0 autopep8==1.5.4 mypy==0.782 From 9b4263777bea7b3e00a1a3546511f2117f210a2b Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Mon, 10 Aug 2020 20:57:39 +0530 Subject: [PATCH 28/79] npm upgrade (#418) --- dashboard/package-lock.json | 2293 ++++++++++++++++++++++++++++------- dashboard/package.json | 32 +- 2 files changed, 1849 insertions(+), 476 deletions(-) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 0525427e8..bde5bf1e5 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -5,25 +5,41 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.10.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -32,12 +48,20 @@ "requires": { "@nodelib/fs.stat": "2.0.3", "run-parallel": "^1.1.9" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + } } }, "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, "@nodelib/fs.walk": { @@ -63,62 +87,61 @@ "dev": true }, "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", + "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", "dev": true }, "@types/fs-extra": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.1.tgz", - "integrity": "sha512-J00cVDALmi/hJOYsunyT52Hva5TnJeKP5yd1r+mH/ZU0mbYZflR0Z5kw5kITtKTRYMhm1JMClOFYdHnQszEvqw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", + "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", "dev": true, "requires": { "@types/node": "*" } }, "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { - "@types/events": "*", "@types/minimatch": "*", "@types/node": "*" } }, "@types/jasmine": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.4.tgz", - "integrity": "sha512-+/sHcTPyDS1JQacDRRRWb+vNrjBwnD+cKvTaWlxlJ/uOOFvzCkjOwNaqVjYMLfsjzNi0WtDH9RyReDXPG1Cdug==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.12.tgz", + "integrity": "sha512-vJaQ58oceFao+NzpKNqLOWwHPsqA7YEhKv+mOXvYU4/qh+BfVWIxaBtL0Ck5iCS67yOkNwGkDCrzepnzIWF+7g==", "dev": true }, "@types/jquery": { - "version": "3.3.31", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz", - "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg==", "dev": true, "requires": { "@types/sizzle": "*" } }, "@types/js-cookie": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.4.tgz", - "integrity": "sha512-WTfSE1Eauak/Nrg6cA9FgPTFvVawejsai6zXoq0QYTQ3mxONeRtGhKxa7wMlUzWWmzrmTeV+rwLjHgsCntdrsA==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz", + "integrity": "sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==", "dev": true }, "@types/json-schema": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", - "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, "@types/minimatch": { @@ -128,9 +151,9 @@ "dev": true }, "@types/node": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", - "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", + "version": "14.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", + "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", "dev": true }, "@types/sizzle": { @@ -146,52 +169,65 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz", - "integrity": "sha512-ddrJZxp5ns1Lh5ofZQYk3P8RyvKfyz/VcRR4ZiJLHO/ljnQAO8YvTfj268+WJOOadn99mvDiqJA65+HAKoeSPA==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.5.0", - "eslint-utils": "^1.4.2", + "@typescript-eslint/experimental-utils": "2.34.0", "functional-red-black-tree": "^1.0.1", - "regexpp": "^2.0.1", + "regexpp": "^3.0.0", "tsutils": "^3.17.1" } }, "@typescript-eslint/experimental-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.5.0.tgz", - "integrity": "sha512-UgcQGE0GKJVChyRuN1CWqDW8Pnu7+mVst0aWrhiyuUD1J9c+h8woBdT4XddCvhcXDodTDVIfE3DzGHVjp7tUeQ==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.5.0", - "eslint-scope": "^5.0.0" + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } } }, "@typescript-eslint/parser": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.5.0.tgz", - "integrity": "sha512-9UBMiAwIDWSl79UyogaBdj3hidzv6exjKUx60OuZuFnJf56tq/UMpdPcX09YmGqE8f4AnAueYtBxV8IcAT3jdQ==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.5.0", - "@typescript-eslint/typescript-estree": "2.5.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.5.0.tgz", - "integrity": "sha512-AXURyF8NcA3IsnbjNX1v9qbwa0dDoY9YPcKYR2utvMHoUcu3636zrz0gRWtVAyxbPCkhyKuGg6WZIyi2Fc79CA==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", "dev": true, "requires": { "debug": "^4.1.1", - "glob": "^7.1.4", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", "is-glob": "^4.0.1", - "lodash.unescape": "4.0.1", - "semver": "^6.3.0" + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "dependencies": { "debug": { @@ -204,23 +240,23 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true } } }, "abab": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz", - "integrity": "sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==", "dev": true }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, "acorn-globals": { @@ -231,8 +267,22 @@ "requires": { "acorn": "^6.0.1", "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + } } }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -240,12 +290,12 @@ "dev": true }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -288,6 +338,24 @@ "sprintf-js": "~1.0.2" } }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, "array-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", @@ -295,13 +363,14 @@ "dev": true }, "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" } }, "array-union": { @@ -310,6 +379,22 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -331,6 +416,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -346,18 +437,18 @@ "lodash": "^4.17.14" } }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -365,9 +456,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, "babel-polyfill": { @@ -413,6 +504,61 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, "basic-auth": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", @@ -439,18 +585,38 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, "buffer-from": { @@ -459,6 +625,29 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -478,9 +667,9 @@ "dev": true }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -501,11 +690,34 @@ "dev": true }, "chrome-devtools-frontend": { - "version": "1.0.706688", - "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.706688.tgz", - "integrity": "sha512-2zOpA/bTouVt9/hhLQbaNMPVHHJH+dW1OkR/PFagCqYp2+mrPgXr1vi44BdzMOEw+qIhj7x5wg2mZFK02/30MQ==", + "version": "1.0.792622", + "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.792622.tgz", + "integrity": "sha512-ulWZkHn4aEJpnQU982/QMt/kNlNSQZwMJw1NHnEcPhFVO2EmcQ26Pqev5fewj0EwJ1dBo9+Rcz49KPuTiRgL2A==", "dev": true }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -551,6 +763,16 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -567,9 +789,9 @@ "dev": true }, "colorette": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz", - "integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, "colors": { @@ -587,6 +809,12 @@ "delayed-stream": "~1.0.0" } }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -599,6 +827,12 @@ "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", "dev": true }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, "core-js": { "version": "2.6.10", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz", @@ -635,15 +869,15 @@ "dev": true }, "cssom": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.1.tgz", - "integrity": "sha512-6Aajq0XmukE7HdXUU6IoSWuH1H6gH9z6qmagsstTiN7cW2FNTsb+J2Chs+ufPgZCsV/yo8oaEudQLrb9dGxSVQ==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, "cssstyle": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.0.0.tgz", - "integrity": "sha512-QXSAu2WBsSRXCPjvI43Y40m6fMevvyRm8JVAuF9ksQz5jha4pWP1wpaK7Yu5oLFc6+XAY+hj8YhefyXcBB53gg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "requires": { "cssom": "~0.3.6" @@ -692,6 +926,12 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -707,6 +947,47 @@ "object-keys": "^1.0.12" } }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -777,9 +1058,9 @@ } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "encoding": { @@ -801,27 +1082,28 @@ } }, "es-abstract": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", - "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -835,10 +1117,31 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, "eslint": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", - "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -848,19 +1151,19 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^11.7.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -869,7 +1172,7 @@ "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^6.1.2", @@ -880,36 +1183,67 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", - "dev": true - }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -940,17 +1274,6 @@ "ms": "^2.1.1" } }, - "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", - "dev": true, - "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" - } - }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -962,6 +1285,21 @@ "tmp": "^0.0.33" } }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -969,45 +1307,138 @@ "dev": true }, "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-regex": "^5.0.0" } } } }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "onetime": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.1.tgz", + "integrity": "sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -1015,24 +1446,47 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true } } }, "eslint-config-standard": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", - "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", + "integrity": "sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==", "dev": true }, "eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", "dev": true, "requires": { "debug": "^2.6.9", - "resolve": "^1.5.0" + "resolve": "^1.13.1" }, "dependencies": { "debug": { @@ -1049,16 +1503,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } } } }, "eslint-module-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", - "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", "dev": true, "requires": { - "debug": "^2.6.8", + "debug": "^2.6.9", "pkg-dir": "^2.0.0" }, "dependencies": { @@ -1098,22 +1561,24 @@ } }, "eslint-plugin-import": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", - "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", + "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", "dev": true, "requires": { - "array-includes": "^3.0.3", + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-module-utils": "^2.6.0", "has": "^1.0.3", "minimatch": "^3.0.4", - "object.values": "^1.1.0", + "object.values": "^1.1.1", "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" }, "dependencies": { "debug": { @@ -1140,6 +1605,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } } } }, @@ -1178,9 +1652,9 @@ "dev": true }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -1202,6 +1676,17 @@ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, "esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", @@ -1209,12 +1694,20 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "esrecurse": { @@ -1227,9 +1720,9 @@ } }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "estree-walker": { @@ -1265,12 +1758,83 @@ "strip-eof": "^1.0.0" } }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", @@ -1282,6 +1846,71 @@ "tmp": "^0.0.33" } }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1289,28 +1918,52 @@ "dev": true }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz", - "integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", "dev": true, "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2" + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + } } }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { @@ -1320,12 +1973,12 @@ "dev": true }, "fastq": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", - "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", "dev": true, "requires": { - "reusify": "^1.0.0" + "reusify": "^1.0.4" } }, "figures": { @@ -1347,12 +2000,26 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { - "to-regex-range": "^5.0.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "find-up": { @@ -1376,9 +2043,9 @@ } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "follow-redirects": { @@ -1390,6 +2057,12 @@ "debug": "^3.0.0" } }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1407,6 +2080,15 @@ "mime-types": "^2.1.12" } }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -1448,6 +2130,12 @@ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1458,9 +2146,9 @@ } }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -1472,20 +2160,29 @@ } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", "dev": true }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, "globby": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", @@ -1500,6 +2197,71 @@ "ignore": "^5.1.1", "merge2": "^1.2.3", "slash": "^3.0.0" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } } }, "google-libphonenumber": { @@ -1521,12 +2283,12 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, @@ -1555,11 +2317,43 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1637,9 +2431,9 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -1728,6 +2522,26 @@ "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", "dev": true }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -1741,15 +2555,60 @@ "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", "dev": true }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true }, "is-extglob": { @@ -1777,18 +2636,32 @@ } }, "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } }, "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "requires": { - "isobject": "^4.0.0" + "isobject": "^3.0.1" } }, "is-promise": { @@ -1798,12 +2671,12 @@ "dev": true }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-stream": { @@ -1812,13 +2685,19 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -1827,6 +2706,12 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1840,9 +2725,9 @@ "dev": true }, "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, "isstream": { @@ -1852,19 +2737,19 @@ "dev": true }, "jasmine": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", - "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.1.tgz", + "integrity": "sha512-Jqp8P6ZWkTVFGmJwBK46p+kJNrZCdqkQ4GL+PGuBXZwK1fM4ST9BizkYgIwCFqYYqnTizAy6+XG2Ej5dFrej9Q==", "dev": true, "requires": { - "glob": "^7.1.4", - "jasmine-core": "~3.5.0" + "fast-glob": "^2.2.6", + "jasmine-core": "~3.6.0" } }, "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", + "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==", "dev": true }, "jasmine-ts": { @@ -2136,9 +3021,9 @@ } }, "jquery": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", - "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==", "dev": true }, "js-cookie": { @@ -2160,9 +3045,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -2184,9 +3069,9 @@ "dev": true }, "jsdom": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.0.tgz", - "integrity": "sha512-+hRyEfjRPFwTYMmSQ3/f7U9nP8ZNZmbkmUek760ZpxnCPWJIhaaLRuUSvpJ36fZKCGENxLwxClzwpOpnXNfChQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", "dev": true, "requires": { "abab": "^2.0.0", @@ -2199,7 +3084,7 @@ "domexception": "^1.0.1", "escodegen": "^1.11.1", "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.1.4", + "nwsapi": "^2.2.0", "parse5": "5.1.0", "pn": "^1.1.0", "request": "^2.88.0", @@ -2215,33 +3100,6 @@ "whatwg-url": "^7.0.0", "ws": "^7.0.0", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - }, - "escodegen": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", - "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } } }, "json-schema": { @@ -2268,6 +3126,15 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2289,6 +3156,12 @@ "verror": "1.10.0" } }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -2342,12 +3215,6 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", - "dev": true - }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -2364,6 +3231,21 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, "md5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", @@ -2385,19 +3267,30 @@ } }, "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, "mime": { @@ -2407,18 +3300,18 @@ "dev": true }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", "dev": true }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "dev": true, "requires": { - "mime-db": "1.40.0" + "mime-db": "1.44.0" } }, "mimic-fn": { @@ -2442,6 +3335,27 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -2463,6 +3377,25 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2519,9 +3452,9 @@ "dev": true }, "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, "oauth-sign": { @@ -2536,10 +3469,41 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "object-inspect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "dev": true }, "object-keys": { @@ -2548,14 +3512,44 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", + "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" } @@ -2726,6 +3720,18 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -2766,9 +3772,9 @@ "dev": true }, "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, "pify": { @@ -2818,6 +3824,12 @@ "mkdirp": "^0.5.1" } }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -2837,9 +3849,9 @@ "dev": true }, "psl": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, "punycode": { @@ -2875,16 +3887,38 @@ "read-pkg": "^2.0.0" } }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -2894,7 +3928,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -2904,17 +3938,11 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -2922,33 +3950,33 @@ "dev": true }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" } } } }, "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.11" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.2", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, @@ -2998,6 +4026,12 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -3008,6 +4042,12 @@ "signal-exit": "^3.0.2" } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3024,35 +4064,35 @@ } }, "rollup": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.24.0.tgz", - "integrity": "sha512-PiFETY/rPwodQ8TTC52Nz2DSCYUATznGh/ChnxActCr8rV5FIk3afBUb3uxNritQW/Jpbdn3kq1Rwh1HHYMwdQ==", + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", + "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", "dev": true, "requires": { "@types/estree": "*", "@types/node": "*", "acorn": "^7.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - } } }, "rollup-plugin-copy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.1.0.tgz", - "integrity": "sha512-oVw3ljRV5jv7Yw/6eCEHntVs9Mc+NFglc0iU0J8ei76gldYmtBQ0M/j6WAkZUFVRSrhgfCrEakUllnN87V2f4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.3.0.tgz", + "integrity": "sha512-euDjCUSBXZa06nqnwCNADbkAcYDfzwowfZQkto9K/TFhiH+QG7I4PUsEMwM9tDgomGWJc//z7KLW8t+tZwxADA==", "dev": true, "requires": { - "@types/fs-extra": "^8.0.0", + "@types/fs-extra": "^8.0.1", "colorette": "^1.1.0", "fs-extra": "^8.1.0", "globby": "10.0.1", "is-plain-object": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true + } } }, "rollup-plugin-javascript-obfuscator": { @@ -3105,20 +4145,29 @@ "dev": true }, "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3152,6 +4201,29 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -3198,12 +4270,153 @@ } } }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -3214,6 +4427,12 @@ "source-map": "^0.6.0" } }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -3246,6 +4465,15 @@ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3269,6 +4497,27 @@ "tweetnacl": "~0.14.0" } }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -3314,24 +4563,24 @@ } } }, - "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, - "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, "strip-ansi": { @@ -3356,9 +4605,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { @@ -3394,6 +4643,12 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -3443,13 +4698,46 @@ "os-tmpdir": "~1.0.2" } }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "requires": { - "is-number": "^7.0.0" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" } }, "tough-cookie": { @@ -3488,10 +4776,22 @@ "yn": "^2.0.0" } }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", "dev": true }, "tsutils": { @@ -3527,10 +4827,16 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "union": { @@ -3542,12 +4848,64 @@ "qs": "^6.4.0" } }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -3557,22 +4915,34 @@ "punycode": "^2.1.0" } }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, "url-join": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", "dev": true }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "validate-npm-package-license": { @@ -3597,12 +4967,12 @@ } }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "dev": true, "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "w3c-xmlserializer": { @@ -3663,6 +5033,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -3708,13 +5084,10 @@ } }, "ws": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", - "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", - "dev": true, - "requires": { - "async-limiter": "^1.0.0" - } + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/dashboard/package.json b/dashboard/package.json index 0f63c92d8..c3ee183c5 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -25,31 +25,31 @@ }, "homepage": "https://github.com/abhinavsingh/proxy.py#readme", "devDependencies": { - "@types/jasmine": "^3.4.4", - "@types/jquery": "^3.3.31", - "@types/js-cookie": "^2.2.4", - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", - "chrome-devtools-frontend": "^1.0.706688", - "eslint": "^6.5.1", - "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.18.2", + "@types/jasmine": "^3.5.12", + "@types/jquery": "^3.5.1", + "@types/js-cookie": "^2.2.6", + "@typescript-eslint/eslint-plugin": "^2.34.0", + "@typescript-eslint/parser": "^2.34.0", + "chrome-devtools-frontend": "^1.0.792622", + "eslint": "^6.8.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.22.0", "eslint-plugin-node": "^10.0.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", "http-server": "^0.12.3", - "jasmine": "^3.5.0", + "jasmine": "^3.6.1", "jasmine-ts": "^0.3.0", - "jquery": "^3.5.0", + "jquery": "^3.5.1", "js-cookie": "^2.2.1", - "jsdom": "^15.2.0", + "jsdom": "^15.2.1", "ncp": "^2.0.0", - "rollup": "^1.24.0", - "rollup-plugin-copy": "^3.1.0", + "rollup": "^1.32.1", + "rollup-plugin-copy": "^3.3.0", "rollup-plugin-javascript-obfuscator": "^1.0.4", "rollup-plugin-typescript": "^1.0.1", "ts-node": "^7.0.1", - "typescript": "^3.6.4", - "ws": "^7.2.0" + "typescript": "^3.9.7", + "ws": "^7.3.1" } } From 929800200eb72950aa8643f5aad76e978994554b Mon Sep 17 00:00:00 2001 From: Pascal COMBES Date: Tue, 11 Aug 2020 16:12:28 +0200 Subject: [PATCH 29/79] Remove test for 'HttpWebServerRouteHandler' This does not exist (as fas as I can see) and it bother the linter (Mypy), when I tell it `klass` is a `type` instance. --- proxy/common/flags.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/proxy/common/flags.py b/proxy/common/flags.py index e22ae5d03..522a02ed2 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -534,13 +534,7 @@ def load_plugins(plugins: bytes) -> Dict[bytes, List[type]]: pass base_klass = next(iterator) p[bytes_(base_klass.__name__)].append(klass) - logger.info( - 'Loaded %s %s.%s', - 'plugin' if klass.__name__ != 'HttpWebServerRouteHandler' else 'route', - module_name, - # HttpWebServerRouteHandler route decorator adds a special - # staticmethod to return decorated function name - klass.__name__ if klass.__name__ != 'HttpWebServerRouteHandler' else klass.name()) + logger.info('Loaded plugin %s.%s', module_name, klass.__name__) return p @staticmethod From 1b8d9a8e6793d64ef686915ec3dd944c23b0a3f2 Mon Sep 17 00:00:00 2001 From: Pascal COMBES Date: Tue, 11 Aug 2020 16:28:32 +0200 Subject: [PATCH 30/79] Pass a list plugin class objects or bytes to proxy when used in embeded mode. No automated tests for the feature yet. --- proxy/common/flags.py | 35 ++++++++++++++++------------- tests/http/test_protocol_handler.py | 24 +++++++++++++------- tests/http/test_web_server.py | 32 +++++++++++++++++--------- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/proxy/common/flags.py b/proxy/common/flags.py index 522a02ed2..7f92fd58c 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -21,7 +21,7 @@ import sys import inspect -from typing import Optional, Dict, List, TypeVar, Type, cast, Any, Tuple +from typing import Optional, Dict, List, TypeVar, Type, cast, Any, Tuple, Union from .types import IpAddress from .utils import text_, bytes_ @@ -184,10 +184,9 @@ def initialize( # Load default plugins along with user provided --plugins plugins = Flags.load_plugins( - bytes_( - '%s,%s' % - (text_(COMMA).join(collections.OrderedDict(default_plugins).keys()), - opts.get('plugins', args.plugins)))) + [bytes_(p) for p in collections.OrderedDict(default_plugins).keys()] + + [p if isinstance(p, type) else bytes_(p) for p in opts.get('plugins', args.plugins.split(text_(COMMA)))] + ) # proxy.py currently cannot serve over HTTPS and perform TLS interception # at the same time. Check if user is trying to enable both feature @@ -508,7 +507,7 @@ def set_open_file_limit(soft_limit: int) -> None: 'Open file soft limit set to %d', soft_limit) @staticmethod - def load_plugins(plugins: bytes) -> Dict[bytes, List[type]]: + def load_plugins(plugins: List[Union[bytes, type]]) -> Dict[bytes, List[type]]: """Accepts a comma separated list of Python modules and returns a list of respective Python classes.""" p: Dict[bytes, List[type]] = { @@ -517,16 +516,20 @@ def load_plugins(plugins: bytes) -> Dict[bytes, List[type]]: b'HttpWebServerBasePlugin': [], b'ProxyDashboardWebsocketPlugin': [] } - for plugin_ in plugins.split(COMMA): - plugin = text_(plugin_.strip()) - if plugin == '': - continue - module_name, klass_name = plugin.rsplit(text_(DOT), 1) - klass = getattr( - importlib.import_module( - module_name.replace( - os.path.sep, text_(DOT))), - klass_name) + for plugin_ in plugins: + if isinstance(plugin_, type): + module_name = None + klass = plugin_ + else: + plugin = text_(plugin_.strip()) + if plugin == '': + continue + module_name, klass_name = plugin.rsplit(text_(DOT), 1) + klass = getattr( + importlib.import_module( + module_name.replace( + os.path.sep, text_(DOT))), + klass_name) mro = list(inspect.getmro(klass)) mro.reverse() iterator = iter(mro) diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index ca4dac037..c0b9688f2 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -40,8 +40,10 @@ def setUp(self, self.http_server_port = 65535 self.flags = Flags() - self.flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + self.flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.mock_selector = mock_selector self.protocol_handler = HttpProtocolHandler( @@ -173,8 +175,10 @@ def test_proxy_authentication_failed( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) self.protocol_handler.initialize() @@ -205,8 +209,10 @@ def test_authenticated_proxy_http_get( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -253,8 +259,10 @@ def test_authenticated_proxy_http_tunnel( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin' + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 3a21fc9e1..6ab15a966 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -34,8 +34,10 @@ def setUp(self, mock_fromfd: mock.Mock, mock_selector: mock.Mock) -> None: self._conn = mock_fromfd.return_value self.mock_selector = mock_selector self.flags = Flags() - self.flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + self.flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=self.flags) @@ -95,8 +97,10 @@ def test_default_web_server_returns_404( events=selectors.EVENT_READ, data=None), selectors.EVENT_READ), ] flags = Flags() - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -146,8 +150,10 @@ def test_static_web_server_serves( flags = Flags( enable_static_server=True, static_server_dir=static_server_dir) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -194,8 +200,10 @@ def test_static_web_server_serves_404( data=None), selectors.EVENT_WRITE)], ] flags = Flags(enable_static_server=True) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -230,9 +238,11 @@ def test_on_client_connection_called_on_teardown( def init_and_make_pac_file_request(self, pac_file: str) -> None: flags = Flags(pac_file=pac_file) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin,' - b'proxy.http.server.HttpWebServerPacFilePlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + b'proxy.http.server.HttpWebServerPacFilePlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) From 46ec410610c766030d6ed604d60ac50898edc24f Mon Sep 17 00:00:00 2001 From: Pascal COMBES Date: Tue, 11 Aug 2020 22:11:50 +0200 Subject: [PATCH 31/79] Tests for Flags.load_plugins method. --- tests/common/test_flags.py | 111 +++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/common/test_flags.py diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py new file mode 100644 index 000000000..2cc10fd0c --- /dev/null +++ b/tests/common/test_flags.py @@ -0,0 +1,111 @@ +# -*- 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 unittest + +from proxy.common.flags import Flags +from proxy.http.proxy import HttpProxyPlugin +from proxy.plugin import CacheResponsesPlugin +from proxy.plugin import FilterByUpstreamHostPlugin + +class TestFlags(unittest.TestCase): + def assert_plugins(self, expected): + for k in expected: + self.assertIn(k.encode(), self.flags.plugins) + for p in expected[k]: + self.assertIn(p, self.flags.plugins[k.encode()]) + self.assertEqual(len([o for o in self.flags.plugins[k.encode()] if o == p]), 1) + + def test_load_plugin_from_bytes(self): + self.flags = Flags.initialize([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_bytes(self): + self.flags = Flags.initialize([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + b'proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugin_from_args(self): + self.flags = Flags.initialize([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_args(self): + self.flags = Flags.initialize([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin,proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugin_from_class(self): + self.flags = Flags.initialize([], plugins=[ + CacheResponsesPlugin, + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_class(self): + self.flags = Flags.initialize([], plugins=[ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugins_from_bytes_and_class(self): + self.flags = Flags.initialize([], plugins=[ + CacheResponsesPlugin, + b'proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + @unittest.expectedFailure + def test_unique_plugin_from_bytes(self): + self.flags = Flags.initialize([], plugins=[ + b'proxy.http.proxy.HttpProxyPlugin', + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + @unittest.expectedFailure + def test_unique_plugin_from_args(self): + self.flags = Flags.initialize([ + '--plugins', 'proxy.http.proxy.HttpProxyPlugin', + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + @unittest.expectedFailure + def test_unique_plugin_from_class(self): + self.flags = Flags.initialize([], plugins=[ + HttpProxyPlugin, + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + +if __name__ == '__main__': + unittest.main() From e3a1b7f963d3119c582162c87d3573e322036243 Mon Sep 17 00:00:00 2001 From: Pascal COMBES Date: Tue, 11 Aug 2020 22:14:51 +0200 Subject: [PATCH 32/79] Ensure plugins are loaded only once. Also changed module name for plugins passed by type. --- proxy/common/flags.py | 5 +++-- tests/common/test_flags.py | 29 +++++++++++++++-------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/proxy/common/flags.py b/proxy/common/flags.py index 7f92fd58c..12318c97f 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -518,7 +518,7 @@ def load_plugins(plugins: List[Union[bytes, type]]) -> Dict[bytes, List[type]]: } for plugin_ in plugins: if isinstance(plugin_, type): - module_name = None + module_name = '__main__' klass = plugin_ else: plugin = text_(plugin_.strip()) @@ -536,7 +536,8 @@ def load_plugins(plugins: List[Union[bytes, type]]) -> Dict[bytes, List[type]]: while next(iterator) is not abc.ABC: pass base_klass = next(iterator) - p[bytes_(base_klass.__name__)].append(klass) + if klass not in p[bytes_(base_klass.__name__)]: + p[bytes_(base_klass.__name__)].append(klass) logger.info('Loaded plugin %s.%s', module_name, klass.__name__) return p diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py index 2cc10fd0c..93774fc36 100644 --- a/tests/common/test_flags.py +++ b/tests/common/test_flags.py @@ -10,26 +10,29 @@ """ import unittest +from typing import List, Dict + from proxy.common.flags import Flags from proxy.http.proxy import HttpProxyPlugin from proxy.plugin import CacheResponsesPlugin from proxy.plugin import FilterByUpstreamHostPlugin + class TestFlags(unittest.TestCase): - def assert_plugins(self, expected): + def assert_plugins(self, expected: Dict[str, List[type]]) -> None: for k in expected: self.assertIn(k.encode(), self.flags.plugins) for p in expected[k]: self.assertIn(p, self.flags.plugins[k.encode()]) self.assertEqual(len([o for o in self.flags.plugins[k.encode()] if o == p]), 1) - def test_load_plugin_from_bytes(self): + def test_load_plugin_from_bytes(self) -> None: self.flags = Flags.initialize([], plugins=[ b'proxy.plugin.CacheResponsesPlugin', ]) self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) - def test_load_plugins_from_bytes(self): + def test_load_plugins_from_bytes(self) -> None: self.flags = Flags.initialize([], plugins=[ b'proxy.plugin.CacheResponsesPlugin', b'proxy.plugin.FilterByUpstreamHostPlugin', @@ -39,13 +42,13 @@ def test_load_plugins_from_bytes(self): FilterByUpstreamHostPlugin, ]}) - def test_load_plugin_from_args(self): + def test_load_plugin_from_args(self) -> None: self.flags = Flags.initialize([ '--plugins', 'proxy.plugin.CacheResponsesPlugin', ]) self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) - def test_load_plugins_from_args(self): + def test_load_plugins_from_args(self) -> None: self.flags = Flags.initialize([ '--plugins', 'proxy.plugin.CacheResponsesPlugin,proxy.plugin.FilterByUpstreamHostPlugin', ]) @@ -54,13 +57,13 @@ def test_load_plugins_from_args(self): FilterByUpstreamHostPlugin, ]}) - def test_load_plugin_from_class(self): + def test_load_plugin_from_class(self) -> None: self.flags = Flags.initialize([], plugins=[ CacheResponsesPlugin, ]) self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) - def test_load_plugins_from_class(self): + def test_load_plugins_from_class(self) -> None: self.flags = Flags.initialize([], plugins=[ CacheResponsesPlugin, FilterByUpstreamHostPlugin, @@ -70,7 +73,7 @@ def test_load_plugins_from_class(self): FilterByUpstreamHostPlugin, ]}) - def test_load_plugins_from_bytes_and_class(self): + def test_load_plugins_from_bytes_and_class(self) -> None: self.flags = Flags.initialize([], plugins=[ CacheResponsesPlugin, b'proxy.plugin.FilterByUpstreamHostPlugin', @@ -80,8 +83,7 @@ def test_load_plugins_from_bytes_and_class(self): FilterByUpstreamHostPlugin, ]}) - @unittest.expectedFailure - def test_unique_plugin_from_bytes(self): + def test_unique_plugin_from_bytes(self) -> None: self.flags = Flags.initialize([], plugins=[ b'proxy.http.proxy.HttpProxyPlugin', ]) @@ -89,8 +91,7 @@ def test_unique_plugin_from_bytes(self): HttpProxyPlugin, ]}) - @unittest.expectedFailure - def test_unique_plugin_from_args(self): + def test_unique_plugin_from_args(self) -> None: self.flags = Flags.initialize([ '--plugins', 'proxy.http.proxy.HttpProxyPlugin', ]) @@ -98,8 +99,7 @@ def test_unique_plugin_from_args(self): HttpProxyPlugin, ]}) - @unittest.expectedFailure - def test_unique_plugin_from_class(self): + def test_unique_plugin_from_class(self) -> None: self.flags = Flags.initialize([], plugins=[ HttpProxyPlugin, ]) @@ -107,5 +107,6 @@ def test_unique_plugin_from_class(self): HttpProxyPlugin, ]}) + if __name__ == '__main__': unittest.main() From 7cd0bd5803a9b79e312779d0b1c4f6ab13ef751c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 14 Aug 2020 10:38:34 +0200 Subject: [PATCH 33/79] Update wheel from 0.34.2 to 0.35.0 (#421) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index 929a00571..ac0a280cd 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ twine==3.2.0 -wheel==0.34.2 +wheel==0.35.0 From 505d316002f754a868dce5fcefd85b2059732acb Mon Sep 17 00:00:00 2001 From: pasccom Date: Fri, 14 Aug 2020 10:58:00 +0200 Subject: [PATCH 34/79] Allow to use types when embeding Proxy (#420) * Remove test for 'HttpWebServerRouteHandler' This does not exist (as fas as I can see) and it bother the linter (Mypy), when I tell it `klass` is a `type` instance. * Pass a list plugin class objects or bytes to proxy when used in embeded mode. No automated tests for the feature yet. * Tests for Flags.load_plugins method. * Ensure plugins are loaded only once. Also changed module name for plugins passed by type. Co-authored-by: Abhinav Singh --- proxy/common/flags.py | 46 ++++++------ tests/common/test_flags.py | 112 ++++++++++++++++++++++++++++ tests/http/test_protocol_handler.py | 24 ++++-- tests/http/test_web_server.py | 32 +++++--- 4 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 tests/common/test_flags.py diff --git a/proxy/common/flags.py b/proxy/common/flags.py index e22ae5d03..12318c97f 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -21,7 +21,7 @@ import sys import inspect -from typing import Optional, Dict, List, TypeVar, Type, cast, Any, Tuple +from typing import Optional, Dict, List, TypeVar, Type, cast, Any, Tuple, Union from .types import IpAddress from .utils import text_, bytes_ @@ -184,10 +184,9 @@ def initialize( # Load default plugins along with user provided --plugins plugins = Flags.load_plugins( - bytes_( - '%s,%s' % - (text_(COMMA).join(collections.OrderedDict(default_plugins).keys()), - opts.get('plugins', args.plugins)))) + [bytes_(p) for p in collections.OrderedDict(default_plugins).keys()] + + [p if isinstance(p, type) else bytes_(p) for p in opts.get('plugins', args.plugins.split(text_(COMMA)))] + ) # proxy.py currently cannot serve over HTTPS and perform TLS interception # at the same time. Check if user is trying to enable both feature @@ -508,7 +507,7 @@ def set_open_file_limit(soft_limit: int) -> None: 'Open file soft limit set to %d', soft_limit) @staticmethod - def load_plugins(plugins: bytes) -> Dict[bytes, List[type]]: + def load_plugins(plugins: List[Union[bytes, type]]) -> Dict[bytes, List[type]]: """Accepts a comma separated list of Python modules and returns a list of respective Python classes.""" p: Dict[bytes, List[type]] = { @@ -517,30 +516,29 @@ def load_plugins(plugins: bytes) -> Dict[bytes, List[type]]: b'HttpWebServerBasePlugin': [], b'ProxyDashboardWebsocketPlugin': [] } - for plugin_ in plugins.split(COMMA): - plugin = text_(plugin_.strip()) - if plugin == '': - continue - module_name, klass_name = plugin.rsplit(text_(DOT), 1) - klass = getattr( - importlib.import_module( - module_name.replace( - os.path.sep, text_(DOT))), - klass_name) + for plugin_ in plugins: + if isinstance(plugin_, type): + module_name = '__main__' + klass = plugin_ + else: + plugin = text_(plugin_.strip()) + if plugin == '': + continue + module_name, klass_name = plugin.rsplit(text_(DOT), 1) + klass = getattr( + importlib.import_module( + module_name.replace( + os.path.sep, text_(DOT))), + klass_name) mro = list(inspect.getmro(klass)) mro.reverse() iterator = iter(mro) while next(iterator) is not abc.ABC: pass base_klass = next(iterator) - p[bytes_(base_klass.__name__)].append(klass) - logger.info( - 'Loaded %s %s.%s', - 'plugin' if klass.__name__ != 'HttpWebServerRouteHandler' else 'route', - module_name, - # HttpWebServerRouteHandler route decorator adds a special - # staticmethod to return decorated function name - klass.__name__ if klass.__name__ != 'HttpWebServerRouteHandler' else klass.name()) + if klass not in p[bytes_(base_klass.__name__)]: + p[bytes_(base_klass.__name__)].append(klass) + logger.info('Loaded plugin %s.%s', module_name, klass.__name__) return p @staticmethod diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py new file mode 100644 index 000000000..93774fc36 --- /dev/null +++ b/tests/common/test_flags.py @@ -0,0 +1,112 @@ +# -*- 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 unittest + +from typing import List, Dict + +from proxy.common.flags import Flags +from proxy.http.proxy import HttpProxyPlugin +from proxy.plugin import CacheResponsesPlugin +from proxy.plugin import FilterByUpstreamHostPlugin + + +class TestFlags(unittest.TestCase): + def assert_plugins(self, expected: Dict[str, List[type]]) -> None: + for k in expected: + self.assertIn(k.encode(), self.flags.plugins) + for p in expected[k]: + self.assertIn(p, self.flags.plugins[k.encode()]) + self.assertEqual(len([o for o in self.flags.plugins[k.encode()] if o == p]), 1) + + def test_load_plugin_from_bytes(self) -> None: + self.flags = Flags.initialize([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_bytes(self) -> None: + self.flags = Flags.initialize([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + b'proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugin_from_args(self) -> None: + self.flags = Flags.initialize([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_args(self) -> None: + self.flags = Flags.initialize([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin,proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugin_from_class(self) -> None: + self.flags = Flags.initialize([], plugins=[ + CacheResponsesPlugin, + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_class(self) -> None: + self.flags = Flags.initialize([], plugins=[ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugins_from_bytes_and_class(self) -> None: + self.flags = Flags.initialize([], plugins=[ + CacheResponsesPlugin, + b'proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_unique_plugin_from_bytes(self) -> None: + self.flags = Flags.initialize([], plugins=[ + b'proxy.http.proxy.HttpProxyPlugin', + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + def test_unique_plugin_from_args(self) -> None: + self.flags = Flags.initialize([ + '--plugins', 'proxy.http.proxy.HttpProxyPlugin', + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + def test_unique_plugin_from_class(self) -> None: + self.flags = Flags.initialize([], plugins=[ + HttpProxyPlugin, + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index ca4dac037..c0b9688f2 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -40,8 +40,10 @@ def setUp(self, self.http_server_port = 65535 self.flags = Flags() - self.flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + self.flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.mock_selector = mock_selector self.protocol_handler = HttpProtocolHandler( @@ -173,8 +175,10 @@ def test_proxy_authentication_failed( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) self.protocol_handler.initialize() @@ -205,8 +209,10 @@ def test_authenticated_proxy_http_get( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -253,8 +259,10 @@ def test_authenticated_proxy_http_tunnel( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin' + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 3a21fc9e1..6ab15a966 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -34,8 +34,10 @@ def setUp(self, mock_fromfd: mock.Mock, mock_selector: mock.Mock) -> None: self._conn = mock_fromfd.return_value self.mock_selector = mock_selector self.flags = Flags() - self.flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + self.flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=self.flags) @@ -95,8 +97,10 @@ def test_default_web_server_returns_404( events=selectors.EVENT_READ, data=None), selectors.EVENT_READ), ] flags = Flags() - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -146,8 +150,10 @@ def test_static_web_server_serves( flags = Flags( enable_static_server=True, static_server_dir=static_server_dir) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -194,8 +200,10 @@ def test_static_web_server_serves_404( data=None), selectors.EVENT_WRITE)], ] flags = Flags(enable_static_server=True) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -230,9 +238,11 @@ def test_on_client_connection_called_on_teardown( def init_and_make_pac_file_request(self, pac_file: str) -> None: flags = Flags(pac_file=pac_file) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin,' - b'proxy.http.server.HttpWebServerPacFilePlugin') + flags.plugins = Flags.load_plugins([ + b'proxy.http.proxy.HttpProxyPlugin', + b'proxy.http.server.HttpWebServerPlugin', + b'proxy.http.server.HttpWebServerPacFilePlugin', + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) From b21ad9e4aa9985160bad22cdd7b18db9f0cb54d2 Mon Sep 17 00:00:00 2001 From: pasccom Date: Fri, 14 Aug 2020 13:50:19 +0200 Subject: [PATCH 35/79] Documentation for plugin loading in embedded mode (#422) --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 81f69b5e6..70d9b7d23 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Table of Contents * [Embed proxy.py](#embed-proxypy) * [Blocking Mode](#blocking-mode) * [Non-blocking Mode](#non-blocking-mode) + * [Loading Plugins](#loading-plugins) * [Unit testing with proxy.py](#unit-testing-with-proxypy) * [proxy.TestCase](#proxytestcase) * [Override Startup Flags](#override-startup-flags) @@ -1172,6 +1173,36 @@ Note that: input arguments e.g. `start(['--port', '8899'])` or by using passing flags as kwargs e.g. `start(port=8899)`. +## Loading Plugins + +You can, of course, list plugins to load in the input arguments list of `proxy.main`, `proxy.start` or the `Proxy` constructor. Use the `--plugins` flag as when starting from command line: + +```python +import proxy + +if __name__ == '__main__': + proxy.main([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin', + ]) +``` + +However, for simplicity you can pass the list of plugins to load as a keyword argument to `proxy.main`, `proxy.start` or the `Proxy` constructor: + +```python +import proxy +from proxy.plugin import FilterByUpstreamHostPlugin + +if __name__ == '__main__': + proxy.main([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + FilterByUpstreamHostPlugin, + ]) +``` + +Note that it supports: +1. The fully-qualified name of a class as `bytes` +2. Any `type` instance for a Proxy.py plugin class. This is espacially useful for custom plugins defined locally. + Unit testing with proxy.py ========================== From de13d39d6dd255b8370ea729af8f8412cfd95558 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 14 Aug 2020 19:58:25 +0200 Subject: [PATCH 36/79] Update pytest-cov from 2.10.0 to 2.10.1 (#423) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index fe5d3465b..f955a561f 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -2,7 +2,7 @@ python-coveralls==2.9.3 coverage==5.2.1 flake8==3.8.3 pytest==6.0.1 -pytest-cov==2.10.0 +pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.782 py-spy==0.3.3 From 220ad4c879acf9f0784fc3d376b7d3f9b0e05c50 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sat, 15 Aug 2020 05:36:31 +0200 Subject: [PATCH 37/79] Update wheel from 0.35.0 to 0.35.1 (#424) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index ac0a280cd..a3024372b 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ twine==3.2.0 -wheel==0.35.0 +wheel==0.35.1 From 9459971ffcfc6596beef84b14a8a3b50e1028515 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 25 Aug 2020 08:26:10 +0200 Subject: [PATCH 38/79] Update typing-extensions from 3.7.4.2 to 3.7.4.3 (#428) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f0c0fb602..9ae66d027 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -typing-extensions==3.7.4.2 +typing-extensions==3.7.4.3 From 6f21b6bfc38ccc6a1bcb8e7f7b2280ae171c4e2f Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 25 Aug 2020 08:26:33 +0200 Subject: [PATCH 39/79] Update codecov from 2.1.8 to 2.1.9 (#427) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index f955a561f..839839cbd 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -6,7 +6,7 @@ pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.782 py-spy==0.3.3 -codecov==2.1.8 +codecov==2.1.9 tox==3.19.0 mccabe==0.6.1 pylint==2.5.3 From 98a2e5f8f5379f7148d12566669d8224f13eb306 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 25 Aug 2020 08:26:57 +0200 Subject: [PATCH 40/79] Update pylint from 2.5.3 to 2.6.0 (#426) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 839839cbd..71a0b670d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -9,5 +9,5 @@ py-spy==0.3.3 codecov==2.1.9 tox==3.19.0 mccabe==0.6.1 -pylint==2.5.3 +pylint==2.6.0 rope==0.17.0 From f644105a1b214fa864ad6a2adfda86e91ba878a3 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 2 Sep 2020 08:23:05 +0200 Subject: [PATCH 41/79] Update paramiko from 2.7.1 to 2.7.2 (#429) --- requirements-tunnel.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tunnel.txt b/requirements-tunnel.txt index ada4a3659..58f39bb28 100644 --- a/requirements-tunnel.txt +++ b/requirements-tunnel.txt @@ -1 +1 @@ -paramiko==2.7.1 +paramiko==2.7.2 From 7f7201f5177bb1bb078328016c95e4da63279b5b Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 2 Oct 2020 08:22:54 +0200 Subject: [PATCH 42/79] Update pytest from 6.0.1 to 6.1.0 (#436) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 71a0b670d..a62627b46 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.2.1 flake8==3.8.3 -pytest==6.0.1 +pytest==6.1.0 pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.782 From 3bd66f8392f8e0a1c7a58af6a88ffcd121919e97 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 2 Oct 2020 08:50:57 +0200 Subject: [PATCH 43/79] Update coverage from 5.2.1 to 5.3 (#433) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index a62627b46..cac09a882 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,5 +1,5 @@ python-coveralls==2.9.3 -coverage==5.2.1 +coverage==5.3 flake8==3.8.3 pytest==6.1.0 pytest-cov==2.10.1 From 715b13a10e07b4bf5de65b9feda54f4d9b24d8ea Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 2 Oct 2020 08:51:16 +0200 Subject: [PATCH 44/79] Update tox from 3.19.0 to 3.20.0 (#430) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index cac09a882..5c7f8976c 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.4 mypy==0.782 py-spy==0.3.3 codecov==2.1.9 -tox==3.19.0 +tox==3.20.0 mccabe==0.6.1 pylint==2.6.0 rope==0.17.0 From 2c9b06b11be6b0b8e84785adb090304b4a046825 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sat, 3 Oct 2020 05:55:47 +0200 Subject: [PATCH 45/79] Update flake8 from 3.8.3 to 3.8.4 (#439) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 5c7f8976c..06412d638 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,6 +1,6 @@ python-coveralls==2.9.3 coverage==5.3 -flake8==3.8.3 +flake8==3.8.4 pytest==6.1.0 pytest-cov==2.10.1 autopep8==1.5.4 From 8cc349be488bd8fc5c23dea4f1f83c8d0c02a6f0 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sat, 3 Oct 2020 16:25:43 +0530 Subject: [PATCH 46/79] Allow plugins to add custom command line flags (#438) * Allow plugins to add custom command line flags. Addresses #301 * Reduce dependency over Flags class. This will be deprecated so that adhoc flags can be added without any additional manual configuration * Fix: Argument 1 to "mock_default_args" of "TestMain" has incompatible type "Namespace"; expected "Mock" * Reduce Flags class to just the initializer. * Store list of action dest in FlagParser --- menubar/proxy.py.xcodeproj/project.pbxproj | 4 +- .../UserInterfaceState.xcuserstate | Bin 24880 -> 26859 bytes proxy/common/constants.py | 4 + proxy/common/flag.py | 55 ++ proxy/common/flags.py | 488 +----------------- proxy/core/acceptor/acceptor.py | 26 +- proxy/core/acceptor/pool.py | 61 ++- proxy/http/handler.py | 39 +- proxy/http/inspector/devtools.py | 11 + proxy/http/proxy/server.py | 75 ++- proxy/http/server/pac_plugin.py | 17 + proxy/http/server/web.py | 19 +- proxy/proxy.py | 325 +++++++++++- tests/common/test_flags.py | 25 +- tests/http/test_protocol_handler.py | 9 +- tests/http/test_web_server.py | 11 +- tests/test_main.py | 55 +- tests/test_set_open_file_limit.py | 8 +- 18 files changed, 662 insertions(+), 570 deletions(-) create mode 100644 proxy/common/flag.py diff --git a/menubar/proxy.py.xcodeproj/project.pbxproj b/menubar/proxy.py.xcodeproj/project.pbxproj index 9e6b47c54..5c4462150 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 = 1150; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Abhinav Singh"; TargetAttributes = { AD1F92A2238864240088A917 = { @@ -340,6 +340,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -400,6 +401,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; 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 index c7640e77c75fa5390319d04a9c3e65e858726da2..f4e17991b460897ec013f5be4772e272a742d3d1 100644 GIT binary patch delta 11138 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 X++(QwxWzzN^X<{1=DY5*$MF9Fn2|pL delta 9409 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 Yb1!QV!Z diff --git a/proxy/common/constants.py b/proxy/common/constants.py index 1479523a7..a65b0874f 100644 --- a/proxy/common/constants.py +++ b/proxy/common/constants.py @@ -86,3 +86,7 @@ PLUGIN_DEVTOOLS_PROTOCOL = 'proxy.http.inspector.DevtoolsProtocolPlugin' PLUGIN_DASHBOARD = 'proxy.dashboard.dashboard.ProxyDashboard' PLUGIN_INSPECT_TRAFFIC = 'proxy.dashboard.inspect_traffic.InspectTrafficPlugin' + +PY2_DEPRECATION_MESSAGE = '''DEPRECATION: proxy.py no longer supports Python 2.7. Kindly upgrade to Python 3+. ' + 'If for some reasons you cannot upgrade, use' + '"pip install proxy.py==0.3".''' diff --git a/proxy/common/flag.py b/proxy/common/flag.py new file mode 100644 index 000000000..13ae8e6f3 --- /dev/null +++ b/proxy/common/flag.py @@ -0,0 +1,55 @@ +# -*- 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 Optional, List, Any + +from .version import __version__ + +__homepage__ = 'https://github.com/abhinavsingh/proxy.py' + + +class FlagParser: + """Wrapper around argparse module. + + proxy.py core and plugin classes must import `flag.flags` and + use `add_argument` to define their own flags within respective + class files. + + Best Practice: + 1. Define flags at the top of your class files. + 2. DO NOT add flags within your class `__init__` method OR + within class methods. It MAY result into runtime exception, + especially if your class is initialized multiple times or if + class method registering the flag gets invoked multiple times. + """ + + def __init__(self) -> None: + self.args: Optional[argparse.Namespace] = None + self.actions: List[str] = [] + self.parser = argparse.ArgumentParser( + description='proxy.py v%s' % __version__, + epilog='Proxy.py not working? Report at: %s/issues/new' % __homepage__ + ) + + def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action: + """Register a flag.""" + action = self.parser.add_argument(*args, **kwargs) + self.actions.append(action.dest) + return action + + def parse_args( + self, input_args: Optional[List[str]]) -> argparse.Namespace: + """Parse flags from input arguments.""" + self.args = self.parser.parse_args(input_args) + return self.args + + +flags = FlagParser() diff --git a/proxy/common/flags.py b/proxy/common/flags.py index 12318c97f..93acc9ae8 100644 --- a/proxy/common/flags.py +++ b/proxy/common/flags.py @@ -8,44 +8,21 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -import abc -import logging -import importlib -import collections -import argparse -import base64 -import ipaddress import os import socket import multiprocessing -import sys -import inspect -from typing import Optional, Dict, List, TypeVar, Type, cast, Any, Tuple, Union +from typing import Optional, Dict, List from .types import IpAddress -from .utils import text_, bytes_ -from .constants import DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH -from .constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_DISABLE_HEADERS -from .constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_EVENTS, DEFAULT_ENABLE_DEVTOOLS -from .constants import DEFAULT_ENABLE_WEB_SERVER, DEFAULT_THREADLESS, DEFAULT_CERT_FILE, DEFAULT_KEY_FILE, DEFAULT_CA_FILE -from .constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE -from .constants import DEFAULT_PAC_FILE_URL_PATH, DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT -from .constants import DEFAULT_NUM_WORKERS, DEFAULT_VERSION, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME +from .constants import DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH +from .constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS +from .constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_EVENTS +from .constants import DEFAULT_THREADLESS +from .constants import DEFAULT_PAC_FILE_URL_PATH, DEFAULT_PAC_FILE, DEFAULT_PID_FILE, DEFAULT_PORT +from .constants import DEFAULT_IPV6_HOSTNAME from .constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_STATIC_SERVER_DIR -from .constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_DATA_DIRECTORY_PATH, COMMA, DOT -from .constants import PLUGIN_HTTP_PROXY, PLUGIN_WEB_SERVER, PLUGIN_PAC_FILE -from .constants import PLUGIN_DEVTOOLS_PROTOCOL, PLUGIN_DASHBOARD, PLUGIN_INSPECT_TRAFFIC -from .version import __version__ - -__homepage__ = 'https://github.com/abhinavsingh/proxy.py' - -if os.name != 'nt': - import resource - -logger = logging.getLogger(__name__) - -T = TypeVar('T', bound='Flags') +from .constants import DEFAULT_DATA_DIRECTORY_PATH class Flags: @@ -117,452 +94,3 @@ def __init__( self.ca_cert_dir = os.path.join( self.proxy_py_data_dir, 'certificates') os.makedirs(self.ca_cert_dir, exist_ok=True) - - def tls_interception_enabled(self) -> bool: - return self.ca_key_file is not None and \ - self.ca_cert_dir is not None and \ - self.ca_signing_key_file is not None and \ - self.ca_cert_file is not None - - def encryption_enabled(self) -> bool: - return self.keyfile is not None and \ - self.certfile is not None - - @classmethod - def initialize( - cls: Type[T], - input_args: Optional[List[str]], - **opts: Any) -> T: - if not Flags.is_py3(): - print( - 'DEPRECATION: "develop" branch no longer supports Python 2.7. Kindly upgrade to Python 3+. ' - 'If for some reasons you cannot upgrade, consider using "master" branch or simply ' - '"pip install proxy.py==0.3".' - '\n\n' - 'DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. ' - 'Please upgrade your Python as Python 2.7 won\'t be maintained after that date. ' - '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 - if args.version: - print(__version__) - sys.exit(0) - - # Setup logging module - Flags.setup_logger(args.log_file, args.log_level, args.log_format) - - # 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)) - args.enable_static_server = True - default_plugins.append((PLUGIN_DASHBOARD, True)) - default_plugins.append((PLUGIN_INSPECT_TRAFFIC, True)) - args.enable_events = True - args.enable_devtools = True - if args.enable_devtools: - default_plugins.append((PLUGIN_DEVTOOLS_PROTOCOL, True)) - default_plugins.append((PLUGIN_WEB_SERVER, True)) - if not args.disable_http_proxy: - default_plugins.append((PLUGIN_HTTP_PROXY, True)) - if args.enable_web_server or \ - args.pac_file is not None or \ - args.enable_static_server: - default_plugins.append((PLUGIN_WEB_SERVER, True)) - 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_(p) for p in collections.OrderedDict(default_plugins).keys()] + - [p if isinstance(p, type) else bytes_(p) for p in opts.get('plugins', args.plugins.split(text_(COMMA)))] - ) - - # proxy.py currently cannot serve over HTTPS and perform TLS interception - # at the same time. Check if user is trying to enable both feature - # at the same time. - if (args.cert_file and args.key_file) and \ - (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file): - print('You can either enable end-to-end encryption OR TLS interception,' - 'not both together.') - sys.exit(1) - - # Generate auth_code required for basic authentication if enabled - auth_code = None - if args.basic_auth: - auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth)) - - return cls( - auth_code=cast(Optional[bytes], opts.get('auth_code', auth_code)), - server_recvbuf_size=cast( - int, - opts.get( - 'server_recvbuf_size', - args.server_recvbuf_size)), - client_recvbuf_size=cast( - int, - opts.get( - 'client_recvbuf_size', - args.client_recvbuf_size)), - pac_file=cast( - Optional[str], opts.get( - 'pac_file', bytes_( - args.pac_file))), - pac_file_url_path=cast( - Optional[bytes], opts.get( - 'pac_file_url_path', bytes_( - args.pac_file_url_path))), - disable_headers=cast(Optional[List[bytes]], opts.get('disable_headers', [ - header.lower() for header in bytes_( - args.disable_headers).split(COMMA) if header.strip() != b''])), - certfile=cast( - Optional[str], opts.get( - 'cert_file', args.cert_file)), - keyfile=cast(Optional[str], opts.get('key_file', args.key_file)), - ca_cert_dir=cast( - Optional[str], opts.get( - 'ca_cert_dir', args.ca_cert_dir)), - ca_key_file=cast( - Optional[str], opts.get( - 'ca_key_file', args.ca_key_file)), - ca_cert_file=cast( - Optional[str], opts.get( - 'ca_cert_file', args.ca_cert_file)), - ca_signing_key_file=cast( - Optional[str], - opts.get( - 'ca_signing_key_file', - args.ca_signing_key_file)), - ca_file=cast( - Optional[str], - opts.get( - 'ca_file', - args.ca_file)), - hostname=cast(IpAddress, - opts.get('hostname', ipaddress.ip_address(args.hostname))), - port=cast(int, opts.get('port', args.port)), - backlog=cast(int, opts.get('backlog', args.backlog)), - num_workers=cast(int, opts.get('num_workers', args.num_workers)), - static_server_dir=cast( - str, - opts.get( - 'static_server_dir', - args.static_server_dir)), - enable_static_server=cast( - bool, - opts.get( - 'enable_static_server', - args.enable_static_server)), - devtools_ws_path=cast( - bytes, - opts.get( - 'devtools_ws_path', - args.devtools_ws_path)), - timeout=cast(int, opts.get('timeout', args.timeout)), - threadless=cast(bool, opts.get('threadless', args.threadless)), - enable_events=cast( - bool, - opts.get( - 'enable_events', - args.enable_events)), - plugins=plugins, - pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file))) - - @staticmethod - def init_parser() -> argparse.ArgumentParser: - """Initializes and returns argument parser.""" - parser = argparse.ArgumentParser( - description='proxy.py v%s' % __version__, - epilog='Proxy.py not working? Report at: %s/issues/new' % __homepage__ - ) - # Argument names are ordered alphabetically. - parser.add_argument( - '--backlog', - type=int, - default=DEFAULT_BACKLOG, - help='Default: 100. Maximum number of pending connections to proxy server') - parser.add_argument( - '--basic-auth', - type=str, - default=DEFAULT_BASIC_AUTH, - help='Default: No authentication. Specify colon separated user:password ' - 'to enable basic authentication.') - parser.add_argument( - '--ca-key-file', - type=str, - default=DEFAULT_CA_KEY_FILE, - help='Default: None. CA key to use for signing dynamically generated ' - 'HTTPS certificates. If used, must also pass --ca-cert-file and --ca-signing-key-file' - ) - parser.add_argument( - '--ca-cert-dir', - type=str, - default=DEFAULT_CA_CERT_DIR, - help='Default: ~/.proxy.py. Directory to store dynamically generated certificates. ' - 'Also see --ca-key-file, --ca-cert-file and --ca-signing-key-file' - ) - parser.add_argument( - '--ca-cert-file', - type=str, - default=DEFAULT_CA_CERT_FILE, - help='Default: None. Signing certificate to use for signing dynamically generated ' - 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-signing-key-file' - ) - parser.add_argument( - '--ca-file', - type=str, - default=DEFAULT_CA_FILE, - help='Default: None. Provide path to custom CA file for peer certificate validation. ' - 'Specially useful on MacOS.' - ) - parser.add_argument( - '--ca-signing-key-file', - type=str, - default=DEFAULT_CA_SIGNING_KEY_FILE, - help='Default: None. CA signing key to use for dynamic generation of ' - 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-cert-file' - ) - parser.add_argument( - '--cert-file', - type=str, - default=DEFAULT_CERT_FILE, - help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. ' - 'If used, must also pass --key-file.' - ) - parser.add_argument( - '--client-recvbuf-size', - type=int, - default=DEFAULT_CLIENT_RECVBUF_SIZE, - help='Default: 1 MB. Maximum amount of data received from the ' - 'client in a single recv() operation. Bump this ' - 'value for faster uploads at the expense of ' - 'increased RAM.') - parser.add_argument( - '--devtools-ws-path', - type=str, - default=DEFAULT_DEVTOOLS_WS_PATH, - help='Default: /devtools. Only applicable ' - 'if --enable-devtools is used.' - ) - parser.add_argument( - '--disable-headers', - type=str, - default=COMMA.join(DEFAULT_DISABLE_HEADERS), - help='Default: None. Comma separated list of headers to remove before ' - 'dispatching client request to upstream server.') - parser.add_argument( - '--disable-http-proxy', - action='store_true', - default=DEFAULT_DISABLE_HTTP_PROXY, - help='Default: False. Whether to disable proxy.HttpProxyPlugin.') - parser.add_argument( - '--enable-dashboard', - action='store_true', - default=DEFAULT_ENABLE_DASHBOARD, - help='Default: False. Enables proxy.py dashboard.' - ) - parser.add_argument( - '--enable-devtools', - action='store_true', - default=DEFAULT_ENABLE_DEVTOOLS, - help='Default: False. Enables integration with Chrome Devtool Frontend. Also see --devtools-ws-path.' - ) - parser.add_argument( - '--enable-events', - action='store_true', - default=DEFAULT_ENABLE_EVENTS, - help='Default: False. Enables core to dispatch lifecycle events. ' - 'Plugins can be used to subscribe for core events.' - ) - parser.add_argument( - '--enable-static-server', - action='store_true', - default=DEFAULT_ENABLE_STATIC_SERVER, - help='Default: False. Enable inbuilt static file server. ' - 'Optionally, also use --static-server-dir to serve static content ' - 'from custom directory. By default, static file server serves ' - 'out of installed proxy.py python module folder.' - ) - parser.add_argument( - '--enable-web-server', - action='store_true', - default=DEFAULT_ENABLE_WEB_SERVER, - help='Default: False. Whether to enable proxy.HttpWebServerPlugin.') - parser.add_argument( - '--hostname', - type=str, - default=str(DEFAULT_IPV6_HOSTNAME), - help='Default: ::1. Server IP address.') - parser.add_argument( - '--key-file', - type=str, - default=DEFAULT_KEY_FILE, - help='Default: None. Server key file to enable end-to-end TLS encryption with clients. ' - 'If used, must also pass --cert-file.' - ) - parser.add_argument( - '--log-level', - type=str, - default=DEFAULT_LOG_LEVEL, - help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. ' - 'Both upper and lowercase values are allowed. ' - 'You may also simply use the leading character e.g. --log-level d') - parser.add_argument('--log-file', type=str, default=DEFAULT_LOG_FILE, - help='Default: sys.stdout. Log file destination.') - parser.add_argument('--log-format', type=str, default=DEFAULT_LOG_FORMAT, - help='Log format for Python logger.') - parser.add_argument('--num-workers', type=int, default=DEFAULT_NUM_WORKERS, - help='Defaults to number of CPU cores.') - parser.add_argument( - '--open-file-limit', - type=int, - default=DEFAULT_OPEN_FILE_LIMIT, - help='Default: 1024. Maximum number of files (TCP connections) ' - 'that proxy.py can open concurrently.') - parser.add_argument( - '--pac-file', - type=str, - default=DEFAULT_PAC_FILE, - help='A file (Proxy Auto Configuration) or string to serve when ' - 'the server receives a direct file request. ' - 'Using this option enables proxy.HttpWebServerPlugin.') - parser.add_argument( - '--pac-file-url-path', - type=str, - default=text_(DEFAULT_PAC_FILE_URL_PATH), - help='Default: %s. Web server path to serve the PAC file.' % - text_(DEFAULT_PAC_FILE_URL_PATH)) - parser.add_argument( - '--pid-file', - type=str, - default=DEFAULT_PID_FILE, - help='Default: None. Save parent process ID to a file.') - parser.add_argument( - '--plugins', - type=str, - default=DEFAULT_PLUGINS, - help='Comma separated plugins') - parser.add_argument('--port', type=int, default=DEFAULT_PORT, - help='Default: 8899. Server port.') - parser.add_argument( - '--server-recvbuf-size', - type=int, - default=DEFAULT_SERVER_RECVBUF_SIZE, - help='Default: 1 MB. Maximum amount of data received from the ' - 'server in a single recv() operation. Bump this ' - 'value for faster downloads at the expense of ' - 'increased RAM.') - parser.add_argument( - '--static-server-dir', - type=str, - default=DEFAULT_STATIC_SERVER_DIR, - help='Default: "public" folder in directory where proxy.py is placed. ' - 'This option is only applicable when static server is also enabled. ' - 'See --enable-static-server.' - ) - parser.add_argument( - '--threadless', - action='store_true', - default=DEFAULT_THREADLESS, - help='Default: False. When disabled a new thread is spawned ' - 'to handle each client connection.' - ) - parser.add_argument( - '--timeout', - type=int, - default=DEFAULT_TIMEOUT, - help='Default: ' + str(DEFAULT_TIMEOUT) + - '. Number of seconds after which ' - 'an inactive connection must be dropped. Inactivity is defined by no ' - 'data sent or received by the client.' - ) - parser.add_argument( - '--version', - '-v', - action='store_true', - default=DEFAULT_VERSION, - help='Prints proxy.py version.') - return parser - - @staticmethod - def set_open_file_limit(soft_limit: int) -> None: - """Configure open file description soft limit on supported OS.""" - if os.name != 'nt': # resource module not available on Windows OS - curr_soft_limit, curr_hard_limit = resource.getrlimit( - resource.RLIMIT_NOFILE) - if curr_soft_limit < soft_limit < curr_hard_limit: - resource.setrlimit( - resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit)) - logger.debug( - 'Open file soft limit set to %d', soft_limit) - - @staticmethod - def load_plugins(plugins: List[Union[bytes, type]]) -> Dict[bytes, List[type]]: - """Accepts a comma separated list of Python modules and returns - a list of respective Python classes.""" - p: Dict[bytes, List[type]] = { - b'HttpProtocolHandlerPlugin': [], - b'HttpProxyBasePlugin': [], - b'HttpWebServerBasePlugin': [], - b'ProxyDashboardWebsocketPlugin': [] - } - for plugin_ in plugins: - if isinstance(plugin_, type): - module_name = '__main__' - klass = plugin_ - else: - plugin = text_(plugin_.strip()) - if plugin == '': - continue - module_name, klass_name = plugin.rsplit(text_(DOT), 1) - klass = getattr( - importlib.import_module( - module_name.replace( - os.path.sep, text_(DOT))), - klass_name) - mro = list(inspect.getmro(klass)) - mro.reverse() - iterator = iter(mro) - while next(iterator) is not abc.ABC: - pass - base_klass = next(iterator) - if klass not in p[bytes_(base_klass.__name__)]: - p[bytes_(base_klass.__name__)].append(klass) - logger.info('Loaded plugin %s.%s', module_name, klass.__name__) - return p - - @staticmethod - def setup_logger( - log_file: Optional[str] = DEFAULT_LOG_FILE, - log_level: str = DEFAULT_LOG_LEVEL, - log_format: str = DEFAULT_LOG_FORMAT) -> None: - ll = getattr( - logging, - {'D': 'DEBUG', - 'I': 'INFO', - 'W': 'WARNING', - 'E': 'ERROR', - 'C': 'CRITICAL'}[log_level.upper()[0]]) - if log_file: - logging.basicConfig( - filename=log_file, - filemode='a', - level=ll, - format=log_format) - else: - logging.basicConfig(level=ll, format=log_format) - - @staticmethod - def is_py3() -> bool: - """Exists only to avoid mocking sys.version_info in tests.""" - return sys.version_info[0] == 3 diff --git a/proxy/core/acceptor/acceptor.py b/proxy/core/acceptor/acceptor.py index d5134b4e1..9cfa6e035 100644 --- a/proxy/core/acceptor/acceptor.py +++ b/proxy/core/acceptor/acceptor.py @@ -24,18 +24,36 @@ from ..connection import TcpClientConnection from ..event import EventQueue, eventNames +from ...common.constants import DEFAULT_THREADLESS from ...common.flags import Flags +from ...common.flag import flags logger = logging.getLogger(__name__) +flags.add_argument( + '--threadless', + action='store_true', + default=DEFAULT_THREADLESS, + help='Default: False. When disabled a new thread is spawned ' + 'to handle each client connection.' +) + + class Acceptor(multiprocessing.Process): """Socket server acceptor process. - Accepts client connection over received server socket handle at startup. Spawns a separate - thread to handle each client request. However, when `--threadless` is enabled, Acceptor also - pre-spawns a `Threadless` process at startup. Accepted client connections are passed to - `Threadless` process which internally uses asyncio event loop to handle client connections. + Accepts a server socket fd over `work_queue` and start listening for client + connections over the passed server socket. By default, it spawns a separate thread + to handle each client request. + + However, if `--threadless` option is enabled, Acceptor process will also pre-spawns a `Threadless` + process at startup. Accepted client connections are then passed to the `Threadless` process + which internally uses asyncio event loop to handle client connections. + + TODO(abhinavsingh): Instead of starting `Threadless` process, can we work with a `Threadless` thread? + What are the performance implications of sharing fds between threads vs processes? How much performance + degradation happen when processes are running on separate CPU cores? """ def __init__( diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py index 259c5d304..f330ea033 100644 --- a/proxy/core/acceptor/pool.py +++ b/proxy/core/acceptor/pool.py @@ -12,7 +12,6 @@ import multiprocessing import socket import threading -# import time from multiprocessing import connection from multiprocessing.reduction import send_handle from typing import List, Optional, Type @@ -22,27 +21,69 @@ from ..event import EventQueue, EventDispatcher from ...common.flags import Flags +from ...common.flag import flags +from ...common.constants import DEFAULT_BACKLOG, DEFAULT_ENABLE_EVENTS +from ...common.constants import DEFAULT_IPV6_HOSTNAME, DEFAULT_NUM_WORKERS, DEFAULT_PORT logger = logging.getLogger(__name__) +# Lock shared by worker processes LOCK = multiprocessing.Lock() +flags.add_argument( + '--backlog', + type=int, + default=DEFAULT_BACKLOG, + help='Default: 100. Maximum number of pending connections to proxy server') + +flags.add_argument( + '--enable-events', + action='store_true', + default=DEFAULT_ENABLE_EVENTS, + help='Default: False. Enables core to dispatch lifecycle events. ' + 'Plugins can be used to subscribe for core events.' +) + +flags.add_argument( + '--hostname', + type=str, + default=str(DEFAULT_IPV6_HOSTNAME), + help='Default: ::1. Server IP address.') + +flags.add_argument( + '--port', type=int, default=DEFAULT_PORT, + help='Default: 8899. Server port.') + +flags.add_argument( + '--num-workers', + type=int, + default=DEFAULT_NUM_WORKERS, + help='Defaults to number of CPU cores.') + + class AcceptorPool: """AcceptorPool. - Pre-spawns worker processes to utilize all cores available on the system. Server socket connection is - dispatched over a pipe to workers. Each Acceptor instance accepts for new client connection. + Pre-spawns worker processes to utilize all cores available on the system. + A server socket is initialized and dispatched over a pipe to these workers. + Each worker process then accepts new client connection. Example usage: - pool = AcceptorPool(flags=..., work_klass=...) - try: - pool.setup() - while True: - time.sleep(1) - finally: - pool.shutdown() + pool = AcceptorPool(flags=..., work_klass=...) + try: + pool.setup() + while True: + time.sleep(1) + finally: + pool.shutdown() + + `work_klass` must implement `work.Work` class. + + Optionally, AcceptorPool also initialize a global event queue. + It is a multiprocess safe queue which can be used to build pubsub patterns + for message sharing or signaling within proxy.py. """ def __init__(self, flags: Flags, work_klass: Type[Work]) -> None: diff --git a/proxy/http/handler.py b/proxy/http/handler.py index c93cae5f6..ba4ccba45 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -29,10 +29,39 @@ from ..core.acceptor.work import Work from ..core.event import EventQueue from ..core.connection import TcpClientConnection +from ..common.flag import flags +from ..common.constants import DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_KEY_FILE, DEFAULT_TIMEOUT + logger = logging.getLogger(__name__) +flags.add_argument( + '--client-recvbuf-size', + type=int, + default=DEFAULT_CLIENT_RECVBUF_SIZE, + help='Default: 1 MB. Maximum amount of data received from the ' + 'client in a single recv() operation. Bump this ' + 'value for faster uploads at the expense of ' + 'increased RAM.') +flags.add_argument( + '--key-file', + type=str, + default=DEFAULT_KEY_FILE, + help='Default: None. Server key file to enable end-to-end TLS encryption with clients. ' + 'If used, must also pass --cert-file.' +) +flags.add_argument( + '--timeout', + type=int, + default=DEFAULT_TIMEOUT, + help='Default: ' + str(DEFAULT_TIMEOUT) + + '. Number of seconds after which ' + 'an inactive connection must be dropped. Inactivity is defined by no ' + 'data sent or received by the client.' +) + + class HttpProtocolHandler(Work): """HTTP, HTTPS, HTTP2, WebSockets protocol handler. @@ -53,11 +82,15 @@ def __init__(self, client: TcpClientConnection, self.client: TcpClientConnection = client self.plugins: Dict[str, HttpProtocolHandlerPlugin] = {} + def encryption_enabled(self) -> bool: + return self.flags.keyfile is not None and \ + self.flags.certfile is not None + def initialize(self) -> None: """Optionally upgrades connection to HTTPS, set conn in non-blocking mode and initializes plugins.""" conn = self.optionally_wrap_socket(self.client.connection) conn.setblocking(False) - if self.flags.encryption_enabled(): + if self.encryption_enabled(): self.client = TcpClientConnection(conn=conn, addr=self.client.addr) if b'HttpProtocolHandlerPlugin' in self.flags.plugins: for klass in self.flags.plugins[b'HttpProtocolHandlerPlugin']: @@ -144,7 +177,7 @@ def shutdown(self) -> None: conn = self.client.connection # Unwrap if wrapped before shutdown. - if self.flags.encryption_enabled() and \ + if self.encryption_enabled() and \ isinstance(self.client.connection, ssl.SSLSocket): conn = self.client.connection.unwrap() conn.shutdown(socket.SHUT_WR) @@ -162,7 +195,7 @@ def optionally_wrap_socket( Shutdown and closes client connection upon error. """ - if self.flags.encryption_enabled(): + if self.encryption_enabled(): assert self.flags.keyfile and self.flags.certfile conn = wrap_socket(conn, self.flags.keyfile, self.flags.certfile) return conn diff --git a/proxy/http/inspector/devtools.py b/proxy/http/inspector/devtools.py index 2e9a983bf..50b34192a 100644 --- a/proxy/http/inspector/devtools.py +++ b/proxy/http/inspector/devtools.py @@ -19,10 +19,21 @@ from ...common.utils import bytes_, text_ from ...core.event import EventSubscriber +from ...common.flag import flags +from ...common.constants import DEFAULT_DEVTOOLS_WS_PATH logger = logging.getLogger(__name__) +flags.add_argument( + '--devtools-ws-path', + type=str, + default=DEFAULT_DEVTOOLS_WS_PATH, + help='Default: /devtools. Only applicable ' + 'if --enable-devtools is used.' +) + + class DevtoolsProtocolPlugin(HttpWebServerBasePlugin): """Speaks DevTools protocol with client over websocket. diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 1c95c7896..ef8f5ab1d 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -26,16 +26,77 @@ from ..methods import httpMethods from ...common.types import Readables, Writables -from ...common.constants import PROXY_AGENT_HEADER_VALUE +from ...common.constants import COMMA, DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_FILE, DEFAULT_SERVER_RECVBUF_SIZE +from ...common.constants import DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE, DEFAULT_CERT_FILE +from ...common.constants import PROXY_AGENT_HEADER_VALUE, DEFAULT_DISABLE_HEADERS from ...common.utils import build_http_response, text_ from ...common.pki import gen_public_key, gen_csr, sign_csr from ...core.event import eventNames from ...core.connection import TcpServerConnection, TcpConnectionUninitializedException +from ...common.flag import flags logger = logging.getLogger(__name__) +flags.add_argument( + '--ca-key-file', + type=str, + default=DEFAULT_CA_KEY_FILE, + help='Default: None. CA key to use for signing dynamically generated ' + 'HTTPS certificates. If used, must also pass --ca-cert-file and --ca-signing-key-file' +) +flags.add_argument( + '--ca-cert-dir', + type=str, + default=DEFAULT_CA_CERT_DIR, + help='Default: ~/.proxy.py. Directory to store dynamically generated certificates. ' + 'Also see --ca-key-file, --ca-cert-file and --ca-signing-key-file' +) +flags.add_argument( + '--ca-cert-file', + type=str, + default=DEFAULT_CA_CERT_FILE, + help='Default: None. Signing certificate to use for signing dynamically generated ' + 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-signing-key-file' +) +flags.add_argument( + '--ca-file', + type=str, + default=DEFAULT_CA_FILE, + help='Default: None. Provide path to custom CA file for peer certificate validation. ' + 'Specially useful on MacOS.' +) +flags.add_argument( + '--ca-signing-key-file', + type=str, + default=DEFAULT_CA_SIGNING_KEY_FILE, + help='Default: None. CA signing key to use for dynamic generation of ' + 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-cert-file' +) +flags.add_argument( + '--cert-file', + type=str, + default=DEFAULT_CERT_FILE, + help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. ' + 'If used, must also pass --key-file.' +) +flags.add_argument( + '--disable-headers', + type=str, + default=COMMA.join(DEFAULT_DISABLE_HEADERS), + help='Default: None. Comma separated list of headers to remove before ' + 'dispatching client request to upstream server.') +flags.add_argument( + '--server-recvbuf-size', + type=int, + default=DEFAULT_SERVER_RECVBUF_SIZE, + help='Default: 1 MB. Maximum amount of data received from the ' + 'server in a single recv() operation. Bump this ' + 'value for faster downloads at the expense of ' + 'increased RAM.') + + class HttpProxyPlugin(HttpProtocolHandlerPlugin): """HttpProtocolHandler plugin which implements HttpProxy specifications.""" @@ -68,6 +129,12 @@ def __init__( self.event_queue) self.plugins[instance.name()] = instance + 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 + def get_descriptors( self) -> Tuple[List[socket.socket], List[socket.socket]]: if not self.request.has_upstream_server(): @@ -149,7 +216,7 @@ def read_from_descriptors(self, r: Readables) -> bool: # See https://github.com/abhinavsingh/proxy.py/issues/127 for why # currently response parsing is disabled when TLS interception is enabled. # - # or self.config.tls_interception_enabled(): + # or self.tls_interception_enabled(): if self.response.state == httpParserStates.COMPLETE: self.handle_pipeline_response(raw) else: @@ -212,7 +279,7 @@ def on_client_data(self, raw: memoryview) -> Optional[memoryview]: if self.server and not self.server.closed: if self.request.state == httpParserStates.COMPLETE and ( self.request.method != httpMethods.CONNECT or - self.flags.tls_interception_enabled()): + self.tls_interception_enabled()): if self.pipeline_request is not None and \ self.pipeline_request.is_connection_upgrade(): # Previous pipelined request was a WebSocket @@ -282,7 +349,7 @@ def on_request_complete(self) -> Union[socket.socket, bool]: self.client.queue( HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) # If interception is enabled - if self.flags.tls_interception_enabled(): + if self.tls_interception_enabled(): # Perform SSL/TLS handshake with upstream self.wrap_server() # Generate certificate and perform handshake with client diff --git a/proxy/http/server/pac_plugin.py b/proxy/http/server/pac_plugin.py index 0dfa0b490..20f131ddb 100644 --- a/proxy/http/server/pac_plugin.py +++ b/proxy/http/server/pac_plugin.py @@ -16,6 +16,23 @@ from ..websocket import WebsocketFrame from ..parser import HttpParser from ...common.utils import bytes_, text_, build_http_response +from ...common.flag import flags +from ...common.constants import DEFAULT_PAC_FILE, DEFAULT_PAC_FILE_URL_PATH + + +flags.add_argument( + '--pac-file', + type=str, + default=DEFAULT_PAC_FILE, + help='A file (Proxy Auto Configuration) or string to serve when ' + 'the server receives a direct file request. ' + 'Using this option enables proxy.HttpWebServerPlugin.') +flags.add_argument( + '--pac-file-url-path', + type=str, + default=text_(DEFAULT_PAC_FILE_URL_PATH), + help='Default: %s. Web server path to serve the PAC file.' % + text_(DEFAULT_PAC_FILE_URL_PATH)) class HttpWebServerPacFilePlugin(HttpWebServerBasePlugin): diff --git a/proxy/http/server/web.py b/proxy/http/server/web.py index 54be0ab65..d46e01102 100644 --- a/proxy/http/server/web.py +++ b/proxy/http/server/web.py @@ -26,12 +26,23 @@ 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 +from ...common.constants import DEFAULT_STATIC_SERVER_DIR, PROXY_AGENT_HEADER_VALUE from ...common.types import Readables, Writables +from ...common.flag import flags logger = logging.getLogger(__name__) +flags.add_argument( + '--static-server-dir', + type=str, + default=DEFAULT_STATIC_SERVER_DIR, + help='Default: "public" folder in directory where proxy.py is placed. ' + 'This option is only applicable when static server is also enabled. ' + 'See --enable-static-server.' +) + + class HttpWebServerPlugin(HttpProtocolHandlerPlugin): """HttpProtocolHandler plugin which handles incoming requests to local web server.""" @@ -73,6 +84,10 @@ def __init__( for (protocol, route) in instance.routes(): self.routes[protocol][re.compile(route)] = instance + def encryption_enabled(self) -> bool: + return self.flags.keyfile is not None and \ + self.flags.certfile is not None + @staticmethod def read_and_build_static_file_response(path: str) -> memoryview: with open(path, 'rb') as f: @@ -146,7 +161,7 @@ def on_request_complete(self) -> Union[socket.socket, bool]: # Routing for Http(s) requests protocol = httpProtocolTypes.HTTPS \ - if self.flags.encryption_enabled() else \ + if self.encryption_enabled() else \ httpProtocolTypes.HTTP for route in self.routes[protocol]: match = route.match(text_(self.request.path)) diff --git a/proxy/proxy.py b/proxy/proxy.py index 643e85dcb..f807ba74e 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -8,23 +8,122 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import abc +import argparse +import base64 +import collections import contextlib +import ipaddress import os import sys import time import logging +import importlib +import inspect from types import TracebackType -from typing import List, Optional, Generator, Any, Type +from typing import Dict, List, Optional, Generator, Any, Tuple, Type, Union, cast -from .common.utils import bytes_ +from .common.utils import bytes_, text_ from .common.flags import Flags +from .common.types import IpAddress +from .common.version import __version__ from .core.acceptor import AcceptorPool from .http.handler import HttpProtocolHandler +from .common.flag import flags +from .common.constants import COMMA, DEFAULT_BASIC_AUTH, DEFAULT_DISABLE_HTTP_PROXY, PY2_DEPRECATION_MESSAGE +from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_DEVTOOLS +from .common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER +from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL +from .common.constants import DEFAULT_OPEN_FILE_LIMIT, DEFAULT_PID_FILE, DEFAULT_PLUGINS +from .common.constants import DEFAULT_VERSION, DOT, PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL +from .common.constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE +from .common.constants import PLUGIN_WEB_SERVER + +if os.name != 'nt': + import resource logger = logging.getLogger(__name__) +flags.add_argument( + '--pid-file', + type=str, + default=DEFAULT_PID_FILE, + help='Default: None. Save parent process ID to a file.') +flags.add_argument( + '--basic-auth', + type=str, + default=DEFAULT_BASIC_AUTH, + help='Default: No authentication. Specify colon separated user:password ' + 'to enable basic authentication.') +flags.add_argument( + '--version', + '-v', + action='store_true', + default=DEFAULT_VERSION, + help='Prints proxy.py version.') +flags.add_argument( + '--disable-http-proxy', + action='store_true', + default=DEFAULT_DISABLE_HTTP_PROXY, + help='Default: False. Whether to disable proxy.HttpProxyPlugin.') +flags.add_argument( + '--enable-dashboard', + action='store_true', + default=DEFAULT_ENABLE_DASHBOARD, + help='Default: False. Enables proxy.py dashboard.' +) +flags.add_argument( + '--enable-devtools', + action='store_true', + default=DEFAULT_ENABLE_DEVTOOLS, + help='Default: False. Enables integration with Chrome Devtool Frontend. Also see --devtools-ws-path.' +) +flags.add_argument( + '--enable-static-server', + action='store_true', + default=DEFAULT_ENABLE_STATIC_SERVER, + help='Default: False. Enable inbuilt static file server. ' + 'Optionally, also use --static-server-dir to serve static content ' + 'from custom directory. By default, static file server serves ' + 'out of installed proxy.py python module folder.' +) +flags.add_argument( + '--enable-web-server', + action='store_true', + default=DEFAULT_ENABLE_WEB_SERVER, + help='Default: False. Whether to enable proxy.HttpWebServerPlugin.') +flags.add_argument( + '--log-level', + type=str, + default=DEFAULT_LOG_LEVEL, + help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. ' + 'Both upper and lowercase values are allowed. ' + 'You may also simply use the leading character e.g. --log-level d') +flags.add_argument( + '--log-file', + type=str, + default=DEFAULT_LOG_FILE, + help='Default: sys.stdout. Log file destination.') +flags.add_argument( + '--log-format', + type=str, + default=DEFAULT_LOG_FORMAT, + help='Log format for Python logger.') +flags.add_argument( + '--open-file-limit', + type=int, + default=DEFAULT_OPEN_FILE_LIMIT, + help='Default: 1024. Maximum number of files (TCP connections) ' + 'that proxy.py can open concurrently.') +flags.add_argument( + '--plugins', + type=str, + default=DEFAULT_PLUGINS, + help='Comma separated plugins') + + class Proxy: """Context manager for controlling core AcceptorPool server lifecycle. @@ -33,7 +132,7 @@ class Proxy: """ def __init__(self, input_args: Optional[List[str]], **opts: Any) -> None: - self.flags = Flags.initialize(input_args, **opts) + self.flags = Proxy.initialize(input_args, **opts) self.acceptors: Optional[AcceptorPool] = None def write_pid_file(self) -> None: @@ -63,6 +162,226 @@ def __exit__( self.acceptors.shutdown() self.delete_pid_file() + @staticmethod + def initialize(input_args: Optional[List[str]], **opts: Any) -> Flags: + if not Proxy.is_py3(): + print(PY2_DEPRECATION_MESSAGE) + sys.exit(1) + + args = flags.parse_args(input_args) + + # Print version and exit + if args.version: + print(__version__) + sys.exit(0) + + # Setup logging module + Proxy.setup_logger(args.log_file, args.log_level, args.log_format) + + # Setup limits + Proxy.set_open_file_limit(args.open_file_limit) + + # Load plugins + default_plugins = Proxy.get_default_plugins(args) + + # Load default plugins along with user provided --plugins + plugins = Proxy.load_plugins( + [bytes_(p) for p in collections.OrderedDict(default_plugins).keys()] + + [p if isinstance(p, type) else bytes_(p) for p in opts.get( + 'plugins', args.plugins.split(text_(COMMA)))] + ) + + # proxy.py currently cannot serve over HTTPS and perform TLS interception + # at the same time. Check if user is trying to enable both feature + # at the same time. + if (args.cert_file and args.key_file) and \ + (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file): + print('You can either enable end-to-end encryption OR TLS interception,' + 'not both together.') + sys.exit(1) + + # Generate auth_code required for basic authentication if enabled + auth_code = None + if args.basic_auth: + auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth)) + + return Flags( + plugins=plugins, + auth_code=cast(Optional[bytes], opts.get('auth_code', auth_code)), + server_recvbuf_size=cast( + int, + opts.get( + 'server_recvbuf_size', + args.server_recvbuf_size)), + client_recvbuf_size=cast( + int, + opts.get( + 'client_recvbuf_size', + args.client_recvbuf_size)), + pac_file=cast( + Optional[str], opts.get( + 'pac_file', bytes_( + args.pac_file))), + pac_file_url_path=cast( + Optional[bytes], opts.get( + 'pac_file_url_path', bytes_( + args.pac_file_url_path))), + disable_headers=cast(Optional[List[bytes]], opts.get('disable_headers', [ + header.lower() for header in bytes_( + args.disable_headers).split(COMMA) if header.strip() != b''])), + certfile=cast( + Optional[str], opts.get( + 'cert_file', args.cert_file)), + keyfile=cast(Optional[str], opts.get('key_file', args.key_file)), + ca_cert_dir=cast( + Optional[str], opts.get( + 'ca_cert_dir', args.ca_cert_dir)), + ca_key_file=cast( + Optional[str], opts.get( + 'ca_key_file', args.ca_key_file)), + ca_cert_file=cast( + Optional[str], opts.get( + 'ca_cert_file', args.ca_cert_file)), + ca_signing_key_file=cast( + Optional[str], + opts.get( + 'ca_signing_key_file', + args.ca_signing_key_file)), + ca_file=cast( + Optional[str], + opts.get( + 'ca_file', + args.ca_file)), + hostname=cast(IpAddress, + opts.get('hostname', ipaddress.ip_address(args.hostname))), + port=cast(int, opts.get('port', args.port)), + backlog=cast(int, opts.get('backlog', args.backlog)), + num_workers=cast(int, opts.get('num_workers', args.num_workers)), + static_server_dir=cast( + str, + opts.get( + 'static_server_dir', + args.static_server_dir)), + enable_static_server=cast( + bool, + opts.get( + 'enable_static_server', + args.enable_static_server)), + devtools_ws_path=cast( + bytes, + opts.get( + 'devtools_ws_path', + getattr(args, 'devtools_ws_path', None))), + timeout=cast(int, opts.get('timeout', args.timeout)), + threadless=cast(bool, opts.get('threadless', args.threadless)), + enable_events=cast( + bool, + opts.get( + 'enable_events', + args.enable_events)), + pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file)) + ) + + @staticmethod + def load_plugins(plugins: List[Union[bytes, type]] + ) -> Dict[bytes, List[type]]: + """Accepts a comma separated list of Python modules and returns + a list of respective Python classes.""" + p: Dict[bytes, List[type]] = { + b'HttpProtocolHandlerPlugin': [], + b'HttpProxyBasePlugin': [], + b'HttpWebServerBasePlugin': [], + b'ProxyDashboardWebsocketPlugin': [] + } + for plugin_ in plugins: + if isinstance(plugin_, type): + module_name = '__main__' + klass = plugin_ + else: + plugin = text_(plugin_.strip()) + if plugin == '': + continue + module_name, klass_name = plugin.rsplit(text_(DOT), 1) + klass = getattr( + importlib.import_module( + module_name.replace( + os.path.sep, text_(DOT))), + klass_name) + mro = list(inspect.getmro(klass)) + mro.reverse() + iterator = iter(mro) + while next(iterator) is not abc.ABC: + pass + base_klass = next(iterator) + if klass not in p[bytes_(base_klass.__name__)]: + p[bytes_(base_klass.__name__)].append(klass) + logger.info('Loaded plugin %s.%s', module_name, klass.__name__) + return p + + @staticmethod + def get_default_plugins( + args: argparse.Namespace) -> List[Tuple[str, bool]]: + # 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)) + args.enable_static_server = True + default_plugins.append((PLUGIN_DASHBOARD, True)) + default_plugins.append((PLUGIN_INSPECT_TRAFFIC, True)) + args.enable_events = True + args.enable_devtools = True + if args.enable_devtools: + default_plugins.append((PLUGIN_DEVTOOLS_PROTOCOL, True)) + default_plugins.append((PLUGIN_WEB_SERVER, True)) + if not args.disable_http_proxy: + default_plugins.append((PLUGIN_HTTP_PROXY, True)) + if args.enable_web_server or \ + args.pac_file is not None or \ + args.enable_static_server: + default_plugins.append((PLUGIN_WEB_SERVER, True)) + if args.pac_file is not None: + default_plugins.append((PLUGIN_PAC_FILE, True)) + return default_plugins + + @staticmethod + def is_py3() -> bool: + """Exists only to avoid mocking sys.version_info in tests.""" + return sys.version_info[0] == 3 + + @staticmethod + def setup_logger( + log_file: Optional[str] = DEFAULT_LOG_FILE, + log_level: str = DEFAULT_LOG_LEVEL, + log_format: str = DEFAULT_LOG_FORMAT) -> None: + ll = getattr( + logging, + {'D': 'DEBUG', + 'I': 'INFO', + 'W': 'WARNING', + 'E': 'ERROR', + 'C': 'CRITICAL'}[log_level.upper()[0]]) + if log_file: + logging.basicConfig( + filename=log_file, + filemode='a', + level=ll, + format=log_format) + else: + logging.basicConfig(level=ll, format=log_format) + + @staticmethod + def set_open_file_limit(soft_limit: int) -> None: + """Configure open file description soft limit on supported OS.""" + if os.name != 'nt': # resource module not available on Windows OS + curr_soft_limit, curr_hard_limit = resource.getrlimit( + resource.RLIMIT_NOFILE) + if curr_soft_limit < soft_limit < curr_hard_limit: + resource.setrlimit( + resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit)) + logger.debug( + 'Open file soft limit set to %d', soft_limit) + @contextlib.contextmanager def start( diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py index 93774fc36..50adb831e 100644 --- a/tests/common/test_flags.py +++ b/tests/common/test_flags.py @@ -12,7 +12,7 @@ from typing import List, Dict -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.http.proxy import HttpProxyPlugin from proxy.plugin import CacheResponsesPlugin from proxy.plugin import FilterByUpstreamHostPlugin @@ -24,16 +24,17 @@ def assert_plugins(self, expected: Dict[str, List[type]]) -> None: self.assertIn(k.encode(), self.flags.plugins) for p in expected[k]: self.assertIn(p, self.flags.plugins[k.encode()]) - self.assertEqual(len([o for o in self.flags.plugins[k.encode()] if o == p]), 1) + self.assertEqual( + len([o for o in self.flags.plugins[k.encode()] if o == p]), 1) def test_load_plugin_from_bytes(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ b'proxy.plugin.CacheResponsesPlugin', ]) self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) def test_load_plugins_from_bytes(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ b'proxy.plugin.CacheResponsesPlugin', b'proxy.plugin.FilterByUpstreamHostPlugin', ]) @@ -43,13 +44,13 @@ def test_load_plugins_from_bytes(self) -> None: ]}) def test_load_plugin_from_args(self) -> None: - self.flags = Flags.initialize([ + self.flags = Proxy.initialize([ '--plugins', 'proxy.plugin.CacheResponsesPlugin', ]) self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) def test_load_plugins_from_args(self) -> None: - self.flags = Flags.initialize([ + self.flags = Proxy.initialize([ '--plugins', 'proxy.plugin.CacheResponsesPlugin,proxy.plugin.FilterByUpstreamHostPlugin', ]) self.assert_plugins({'HttpProxyBasePlugin': [ @@ -58,13 +59,13 @@ def test_load_plugins_from_args(self) -> None: ]}) def test_load_plugin_from_class(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ CacheResponsesPlugin, ]) self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) def test_load_plugins_from_class(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ CacheResponsesPlugin, FilterByUpstreamHostPlugin, ]) @@ -74,7 +75,7 @@ def test_load_plugins_from_class(self) -> None: ]}) def test_load_plugins_from_bytes_and_class(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ CacheResponsesPlugin, b'proxy.plugin.FilterByUpstreamHostPlugin', ]) @@ -84,7 +85,7 @@ def test_load_plugins_from_bytes_and_class(self) -> None: ]}) def test_unique_plugin_from_bytes(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ b'proxy.http.proxy.HttpProxyPlugin', ]) self.assert_plugins({'HttpProtocolHandlerPlugin': [ @@ -92,7 +93,7 @@ def test_unique_plugin_from_bytes(self) -> None: ]}) def test_unique_plugin_from_args(self) -> None: - self.flags = Flags.initialize([ + self.flags = Proxy.initialize([ '--plugins', 'proxy.http.proxy.HttpProxyPlugin', ]) self.assert_plugins({'HttpProtocolHandlerPlugin': [ @@ -100,7 +101,7 @@ def test_unique_plugin_from_args(self) -> None: ]}) def test_unique_plugin_from_class(self) -> None: - self.flags = Flags.initialize([], plugins=[ + self.flags = Proxy.initialize([], plugins=[ HttpProxyPlugin, ]) self.assert_plugins({'HttpProtocolHandlerPlugin': [ diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index c0b9688f2..8b7b3295c 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -15,6 +15,7 @@ from typing import cast from unittest import mock +from proxy.proxy import Proxy from proxy.common.version import __version__ from proxy.common.flags import Flags from proxy.common.utils import bytes_ @@ -40,7 +41,7 @@ def setUp(self, self.http_server_port = 65535 self.flags = Flags() - self.flags.plugins = Flags.load_plugins([ + self.flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -175,7 +176,7 @@ def test_proxy_authentication_failed( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -209,7 +210,7 @@ def test_authenticated_proxy_http_get( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -259,7 +260,7 @@ def test_authenticated_proxy_http_tunnel( flags = Flags( auth_code=b'Basic %s' % base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin' ]) diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 6ab15a966..8c69b3940 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -15,6 +15,7 @@ import selectors from unittest import mock +from proxy.proxy import Proxy from proxy.common.flags import Flags from proxy.core.connection import TcpClientConnection from proxy.http.handler import HttpProtocolHandler @@ -34,7 +35,7 @@ def setUp(self, mock_fromfd: mock.Mock, mock_selector: mock.Mock) -> None: self._conn = mock_fromfd.return_value self.mock_selector = mock_selector self.flags = Flags() - self.flags.plugins = Flags.load_plugins([ + self.flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -97,7 +98,7 @@ def test_default_web_server_returns_404( events=selectors.EVENT_READ, data=None), selectors.EVENT_READ), ] flags = Flags() - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -150,7 +151,7 @@ def test_static_web_server_serves( flags = Flags( enable_static_server=True, static_server_dir=static_server_dir) - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -200,7 +201,7 @@ def test_static_web_server_serves_404( data=None), selectors.EVENT_WRITE)], ] flags = Flags(enable_static_server=True) - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', ]) @@ -238,7 +239,7 @@ def test_on_client_connection_called_on_teardown( def init_and_make_pac_file_request(self, pac_file: str) -> None: flags = Flags(pac_file=pac_file) - flags.plugins = Flags.load_plugins([ + flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', b'proxy.http.server.HttpWebServerPacFilePlugin', diff --git a/tests/test_main.py b/tests/test_main.py index c927a2a12..b9b5651aa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -16,8 +16,7 @@ from unittest import mock from typing import List -from proxy.proxy import main -from proxy.common.flags import Flags +from proxy.proxy import main, Proxy from proxy.common.utils import bytes_ from proxy.http.handler import HttpProtocolHandler @@ -28,7 +27,7 @@ from proxy.common.constants import DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE from proxy.common.constants import DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT from proxy.common.constants import DEFAULT_NUM_WORKERS, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME -from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE +from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, PY2_DEPRECATION_MESSAGE from proxy.common.version import __version__ @@ -82,17 +81,14 @@ def test_init_with_no_arguments( mock_sleep.side_effect = KeyboardInterrupt() input_args: List[str] = [] - flags = Flags.initialize(input_args=input_args) - mock_flags.initialize = lambda *args, **kwargs: flags - - main() + main(input_args) mock_logging_config.assert_called_with( level=logging.INFO, format=DEFAULT_LOG_FORMAT ) mock_acceptor_pool.assert_called_with( - flags=flags, + flags=mock_flags.return_value, work_klass=HttpProtocolHandler, ) mock_acceptor_pool.return_value.setup.assert_called() @@ -103,23 +99,22 @@ def test_init_with_no_arguments( @mock.patch('os.remove') @mock.patch('os.path.exists') @mock.patch('builtins.open') - @mock.patch('proxy.proxy.Flags.init_parser') @mock.patch('proxy.proxy.AcceptorPool') + @mock.patch('proxy.common.flag.FlagParser.parse_args') def test_pid_file_is_written_and_removed( self, + mock_parse_args: mock.Mock, mock_acceptor_pool: mock.Mock, - mock_init_parser: mock.Mock, mock_open: mock.Mock, mock_exists: mock.Mock, mock_remove: mock.Mock, mock_sleep: mock.Mock) -> None: pid_file = get_temp_file('pid') mock_sleep.side_effect = KeyboardInterrupt() - mock_args = mock_init_parser.return_value.parse_args.return_value + mock_args = mock_parse_args.return_value self.mock_default_args(mock_args) mock_args.pid_file = pid_file main(['--pid-file', pid_file]) - mock_init_parser.assert_called() mock_acceptor_pool.assert_called() mock_acceptor_pool.return_value.setup.assert_called() mock_open.assert_called_with(pid_file, 'wb') @@ -129,54 +124,46 @@ def test_pid_file_is_written_and_removed( mock_remove.assert_called_with(pid_file) @mock.patch('time.sleep') - @mock.patch('proxy.proxy.Flags') @mock.patch('proxy.proxy.AcceptorPool') def test_basic_auth( self, mock_acceptor_pool: mock.Mock, - mock_flags: mock.Mock, mock_sleep: mock.Mock) -> None: mock_sleep.side_effect = KeyboardInterrupt() input_args = ['--basic-auth', 'user:pass'] - flags = Flags.initialize(input_args=input_args) - mock_flags.initialize = lambda *args, **kwargs: flags + flgs = Proxy.initialize(input_args) main(input_args=input_args) - mock_acceptor_pool.assert_called_with( - flags=flags, - work_klass=HttpProtocolHandler) + mock_acceptor_pool.assert_called_once() self.assertEqual( - flags.auth_code, + flgs.auth_code, b'Basic dXNlcjpwYXNz') @mock.patch('time.sleep') @mock.patch('builtins.print') - @mock.patch('proxy.proxy.Flags') @mock.patch('proxy.proxy.AcceptorPool') - @mock.patch('proxy.proxy.Flags.is_py3') + @mock.patch('proxy.proxy.Proxy.is_py3') def test_main_py3_runs( self, mock_is_py3: mock.Mock, mock_acceptor_pool: mock.Mock, - mock_flags: mock.Mock, mock_print: mock.Mock, mock_sleep: mock.Mock) -> None: mock_sleep.side_effect = KeyboardInterrupt() input_args = ['--basic-auth', 'user:pass'] - flags = Flags.initialize(input_args=input_args) - mock_flags.initialize = lambda *args, **kwargs: flags - mock_is_py3.return_value = True - main(num_workers=1) + + main(input_args, num_workers=1) + mock_is_py3.assert_called() mock_print.assert_not_called() - mock_acceptor_pool.assert_called() + mock_acceptor_pool.assert_called_once() mock_acceptor_pool.return_value.setup.assert_called() @mock.patch('builtins.print') - @mock.patch('proxy.proxy.Flags.is_py3') + @mock.patch('proxy.proxy.Proxy.is_py3') def test_main_py2_exit( self, mock_is_py3: mock.Mock, @@ -184,15 +171,7 @@ def test_main_py2_exit( mock_is_py3.return_value = False with self.assertRaises(SystemExit) as e: main(num_workers=1) - mock_print.assert_called_with( - 'DEPRECATION: "develop" branch no longer supports Python 2.7. Kindly upgrade to Python 3+. ' - 'If for some reasons you cannot upgrade, consider using "master" branch or simply ' - '"pip install proxy.py==0.3".' - '\n\n' - 'DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. ' - 'Please upgrade your Python as Python 2.7 won\'t be maintained after that date. ' - 'A future version of pip will drop support for Python 2.7.' - ) + mock_print.assert_called_with(PY2_DEPRECATION_MESSAGE) self.assertEqual(e.exception.code, 1) mock_is_py3.assert_called() diff --git a/tests/test_set_open_file_limit.py b/tests/test_set_open_file_limit.py index 7eddcf0ff..3bae38cfe 100644 --- a/tests/test_set_open_file_limit.py +++ b/tests/test_set_open_file_limit.py @@ -12,7 +12,7 @@ import unittest from unittest import mock -from proxy.common.flags import Flags +from proxy.proxy import Proxy if os.name != 'nt': import resource @@ -29,7 +29,7 @@ def test_set_open_file_limit( self, mock_set_rlimit: mock.Mock, mock_get_rlimit: mock.Mock) -> None: - Flags.set_open_file_limit(256) + Proxy.set_open_file_limit(256) mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE) mock_set_rlimit.assert_called_with(resource.RLIMIT_NOFILE, (256, 1024)) @@ -39,7 +39,7 @@ def test_set_open_file_limit_not_called( self, mock_set_rlimit: mock.Mock, mock_get_rlimit: mock.Mock) -> None: - Flags.set_open_file_limit(256) + Proxy.set_open_file_limit(256) mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE) mock_set_rlimit.assert_not_called() @@ -49,6 +49,6 @@ def test_set_open_file_limit_not_called_coz_upper_bound_check( self, mock_set_rlimit: mock.Mock, mock_get_rlimit: mock.Mock) -> None: - Flags.set_open_file_limit(1024) + Proxy.set_open_file_limit(1024) mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE) mock_set_rlimit.assert_not_called() From 29e2a3509183ceaf8fe71bf64a16cbf7e966577e Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 4 Oct 2020 03:42:09 +0200 Subject: [PATCH 47/79] Update pytest from 6.1.0 to 6.1.1 (#440) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 06412d638..f5189cf6c 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.3 flake8==3.8.4 -pytest==6.1.0 +pytest==6.1.1 pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.782 From 1038bb841d83a042f653ef3fdc01d402032a7f01 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Tue, 6 Oct 2020 22:27:19 +0530 Subject: [PATCH 48/79] More examples (#444) * Refactor into BaseServerHandler and BaseEchoServerHandler classes * Add connect tunnel example --- .../{base_echo_server.py => base_server.py} | 25 ++-- examples/connect_tunnel.py | 133 ++++++++++++++++++ examples/ssl_echo_server.py | 10 +- examples/tcp_echo_server.py | 10 +- proxy/http/parser.py | 2 +- proxy/http/proxy/server.py | 2 +- tests/http/test_http_parser.py | 2 +- 7 files changed, 166 insertions(+), 18 deletions(-) rename examples/{base_echo_server.py => base_server.py} (77%) create mode 100644 examples/connect_tunnel.py diff --git a/examples/base_echo_server.py b/examples/base_server.py similarity index 77% rename from examples/base_echo_server.py rename to examples/base_server.py index 70e875bd6..19b3c7b6a 100644 --- a/examples/base_echo_server.py +++ b/examples/base_server.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +from abc import abstractmethod import socket import selectors @@ -17,22 +18,25 @@ from proxy.common.types import Readables, Writables -class BaseEchoServerHandler(Work): - """BaseEchoServerHandler implements Work interface. +class BaseServerHandler(Work): + """BaseServerHandler implements Work interface. - An instance of EchoServerHandler is created for each client - connection. EchoServerHandler lifecycle is controlled by - Threadless core using asyncio. Implementation must provide - get_events and handle_events method. Optionally, also implement - intialize, is_inactive and shutdown method. + An instance of BaseServerHandler is created for each client + connection. BaseServerHandler lifecycle is controlled by + Threadless core using asyncio. + + Implementation must provide: + a) handle_data(data: memoryview) + c) (optionally) intialize, is_inactive and shutdown methods """ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) print('Connection accepted from {0}'.format(self.client.addr)) - def initialize(self) -> None: - pass + @abstractmethod + def handle_data(self, data: memoryview) -> None: + pass # pragma: no cover def get_events(self) -> Dict[socket.socket, int]: # We always want to read from client @@ -58,8 +62,7 @@ def handle_events( 'Connection closed by client {0}'.format( self.client.addr)) return True - # Echo data back to client - self.client.queue(data) + self.handle_data(data) except ConnectionResetError: print( 'Connection reset by client {0}'.format( diff --git a/examples/connect_tunnel.py b/examples/connect_tunnel.py new file mode 100644 index 000000000..2b1aeefad --- /dev/null +++ b/examples/connect_tunnel.py @@ -0,0 +1,133 @@ +# -*- 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 time +import socket +import selectors +from typing import Any, Optional, Dict + +from proxy.core.acceptor import AcceptorPool +from proxy.core.connection import TcpServerConnection +from proxy.http.parser import HttpParser, httpParserTypes, httpParserStates +from proxy.http.codes import httpStatusCodes +from proxy.http.methods import httpMethods +from proxy.common.types import Readables, Writables +from proxy.common.utils import build_http_response, text_ +from proxy.common.flags import Flags + +from examples.base_server import BaseServerHandler + + +class ConnectTunnelHandler(BaseServerHandler): # type: ignore + """A http CONNECT tunnel server.""" + + PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(build_http_response( + httpStatusCodes.OK, + reason=b'Connection established' + )) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.request = HttpParser(httpParserTypes.REQUEST_PARSER) + self.upstream: Optional[TcpServerConnection] = None + + def initialize(self) -> None: + self.client.connection.setblocking(False) + + def shutdown(self) -> None: + if self.upstream: + print('Connection closed with upstream {0}:{1}'.format( + text_(self.request.host), self.request.port)) + self.upstream.close() + super().shutdown() + + def handle_data(self, data: memoryview) -> None: + # Queue for upstream if connection has been established + if self.upstream and self.upstream._conn is not None: + self.upstream.queue(data) + return + + # Parse client request + self.request.parse(data) + + # Drop the request if not a CONNECT request + if self.request.method != httpMethods.CONNECT: + pass + + # CONNECT requests are short and we need not worry about + # receiving partial request bodies here. + assert self.request.state == httpParserStates.COMPLETE + + # Establish connection with upstream + assert self.request.host and self.request.port + self.upstream = TcpServerConnection( + text_(self.request.host), self.request.port) + self.upstream.connect() + print('Connection established with upstream {0}:{1}'.format( + text_(self.request.host), self.request.port)) + + # Queue tunnel established response to client + self.client.queue( + ConnectTunnelHandler.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) + + def get_events(self) -> Dict[socket.socket, int]: + # Get default client events + ev: Dict[socket.socket, int] = super().get_events() + # Read from server if we are connected + if self.upstream and self.upstream._conn is not None: + ev[self.upstream.connection] = selectors.EVENT_READ + # If there is pending buffer for server + # also register for EVENT_WRITE events + if self.upstream and self.upstream.has_buffer(): + if self.upstream.connection in ev: + ev[self.upstream.connection] |= selectors.EVENT_WRITE + else: + ev[self.upstream.connection] = selectors.EVENT_WRITE + return ev + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + # Handle client events + do_shutdown: bool = super().handle_events(readables, writables) + if do_shutdown: + return do_shutdown + # Handle server events + if self.upstream and self.upstream.connection in readables: + data = self.upstream.recv() + if data is None: + # Server closed connection + print('Connection closed by server') + return True + # tunnel data to client + self.client.queue(data) + if self.upstream and self.upstream.connection in writables: + self.upstream.flush() + return False + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Flags(port=12345, num_workers=1, threadless=True), + work_klass=ConnectTunnelHandler) + try: + pool.setup() + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py index 1bb401bfc..5e660dfae 100644 --- a/examples/ssl_echo_server.py +++ b/examples/ssl_echo_server.py @@ -15,10 +15,10 @@ from proxy.common.flags import Flags from proxy.common.utils import wrap_socket -from examples.base_echo_server import BaseEchoServerHandler +from examples.base_server import BaseServerHandler -class EchoSSLServerHandler(BaseEchoServerHandler): # type: ignore +class EchoSSLServerHandler(BaseServerHandler): # type: ignore """Wraps client socket during initialization.""" def initialize(self) -> None: @@ -34,6 +34,10 @@ def initialize(self) -> None: self.client = TcpClientConnection( conn=conn, addr=self.client.addr) # type: ignore + def handle_data(self, data: memoryview) -> None: + # echo back to client + self.client.queue(data) + def main() -> None: # This example requires `threadless=True` @@ -49,6 +53,8 @@ def main() -> None: pool.setup() while True: time.sleep(1) + except KeyboardInterrupt: + pass finally: pool.shutdown() diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py index fa5da58af..f5b0aa50b 100644 --- a/examples/tcp_echo_server.py +++ b/examples/tcp_echo_server.py @@ -13,15 +13,19 @@ from proxy.core.acceptor import AcceptorPool from proxy.common.flags import Flags -from examples.base_echo_server import BaseEchoServerHandler +from examples.base_server import BaseServerHandler -class EchoServerHandler(BaseEchoServerHandler): # type: ignore +class EchoServerHandler(BaseServerHandler): # type: ignore """Sets client socket to non-blocking during initialization.""" def initialize(self) -> None: self.client.connection.setblocking(False) + def handle_data(self, data: memoryview) -> None: + # echo back to client + self.client.queue(data) + def main() -> None: # This example requires `threadless=True` @@ -32,6 +36,8 @@ def main() -> None: pool.setup() while True: time.sleep(1) + except KeyboardInterrupt: + pass finally: pool.shutdown() diff --git a/proxy/http/parser.py b/proxy/http/parser.py index d40885ca7..638959ac9 100644 --- a/proxy/http/parser.py +++ b/proxy/http/parser.py @@ -110,7 +110,7 @@ def set_url(self, url: bytes) -> None: # For CONNECT requests, request line contains # upstream_host:upstream_port which is not complaint # with urlsplit, which expects a fully qualified url. - if self.method == b'CONNECT': + if self.method == httpMethods.CONNECT: url = b'https://' + url self.url = urlparse.urlsplit(url) self.set_line_attributes() diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index ef8f5ab1d..d0297ea83 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -408,7 +408,7 @@ def access_log(self) -> None: server_host, server_port = self.server.addr if self.server else ( None, None) connection_time_ms = (time.time() - self.start_time) * 1000 - if self.request.method == b'CONNECT': + if self.request.method == httpMethods.CONNECT: logger.info( '%s:%s - %s %s:%s - %s bytes - %.2f ms' % (self.client.addr[0], diff --git a/tests/http/test_http_parser.py b/tests/http/test_http_parser.py index e8d78cbea..253f2b7f7 100644 --- a/tests/http/test_http_parser.py +++ b/tests/http/test_http_parser.py @@ -308,7 +308,7 @@ def test_connect_request_without_host_header_request_parse(self) -> None: See https://github.com/abhinavsingh/py/issues/5 for details. """ self.parser.parse(b'CONNECT pypi.org:443 HTTP/1.0\r\n\r\n') - self.assertEqual(self.parser.method, b'CONNECT') + self.assertEqual(self.parser.method, httpMethods.CONNECT) self.assertEqual(self.parser.version, b'HTTP/1.0') self.assertEqual(self.parser.state, httpParserStates.COMPLETE) From 02dcb1dfffbeca815e63faa8fc77107394d6fdb6 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 7 Oct 2020 18:30:01 +0200 Subject: [PATCH 49/79] Update rope from 0.17.0 to 0.18.0 (#445) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index f5189cf6c..d79daebb7 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -10,4 +10,4 @@ codecov==2.1.9 tox==3.20.0 mccabe==0.6.1 pylint==2.6.0 -rope==0.17.0 +rope==0.18.0 From 66762beb6f75ce705a8aba3e1f658915bda39592 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 9 Oct 2020 10:25:46 +0200 Subject: [PATCH 50/79] Update tox from 3.20.0 to 3.20.1 (#446) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index d79daebb7..2d99c1e8a 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.4 mypy==0.782 py-spy==0.3.3 codecov==2.1.9 -tox==3.20.0 +tox==3.20.1 mccabe==0.6.1 pylint==2.6.0 rope==0.18.0 From b5ff5c924f84e2a42398653d73d3743290acbeff Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 9 Oct 2020 18:07:13 +0200 Subject: [PATCH 51/79] Update codecov from 2.1.9 to 2.1.10 (#447) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 2d99c1e8a..4a52f7d58 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -6,7 +6,7 @@ pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.782 py-spy==0.3.3 -codecov==2.1.9 +codecov==2.1.10 tox==3.20.1 mccabe==0.6.1 pylint==2.6.0 From 4804c534f82d34d70a8ee7d797caefcf39d2d3d2 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sat, 10 Oct 2020 16:23:29 +0530 Subject: [PATCH 52/79] Update mypy (#449) --- examples/README.md | 21 +++++++++++++++++++++ examples/connect_tunnel.py | 15 +++++++++------ proxy/common/utils.py | 3 +-- requirements-testing.txt | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/examples/README.md b/examples/README.md index 69e3084c5..04b38a006 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,7 @@ Table of Contents * [SSL Echo Server](#ssl-echo-server) * [SSL Echo Client](#ssl-echo-client) * [PubSub Eventing](#pubsub-eventing) +* [Connect Tunnel](#connect-tunnel) ## WebSocket Client @@ -115,3 +116,23 @@ bye!!! DEBUG:proxy.core.event.subscriber:Un-subscribed relay sub id 5eb22010764f4d44900f41e2fb408ca6 from core events Received 52724 events from main thread, 60172 events from another process, in 21.50117802619934 seconds ``` + +## Connect Tunnel + +A simple HTTP proxy server supporting only CONNECT (https) requests. + +1. Uses `HttpParser` for request parsing. +2. Uses `TcpServerConnection` to establish upstream connection. +3. Overrides `BaseServer` methods to also register read/write events for upstream connection. + +Start `connect_tunnel.py` as: + +``` +❯ PYTHONPATH=. python examples/connect_tunnel.py +``` + +Send https requests via tunnel as: + +``` +❯ curl -x localhost:12345 https://httpbin.org/get +``` diff --git a/examples/connect_tunnel.py b/examples/connect_tunnel.py index 2b1aeefad..2ebac0fb7 100644 --- a/examples/connect_tunnel.py +++ b/examples/connect_tunnel.py @@ -66,12 +66,7 @@ def handle_data(self, data: memoryview) -> None: assert self.request.state == httpParserStates.COMPLETE # Establish connection with upstream - assert self.request.host and self.request.port - self.upstream = TcpServerConnection( - text_(self.request.host), self.request.port) - self.upstream.connect() - print('Connection established with upstream {0}:{1}'.format( - text_(self.request.host), self.request.port)) + self.connect_upstream() # Queue tunnel established response to client self.client.queue( @@ -113,6 +108,14 @@ def handle_events( self.upstream.flush() return False + def connect_upstream(self) -> None: + assert self.request.host and self.request.port + self.upstream = TcpServerConnection( + text_(self.request.host), self.request.port) + self.upstream.connect() + print('Connection established with upstream {0}:{1}'.format( + text_(self.request.host), self.request.port)) + def main() -> None: # This example requires `threadless=True` diff --git a/proxy/common/utils.py b/proxy/common/utils.py index 3edc63274..e125a3d4a 100644 --- a/proxy/common/utils.py +++ b/proxy/common/utils.py @@ -211,8 +211,7 @@ def __exit__( if self.conn: self.conn.close() - def __call__(self, func: Callable[..., Any] - ) -> Callable[[Tuple[Any, ...], Dict[str, Any]], Any]: + def __call__(self, func: Callable[..., Any]) -> Callable[[Tuple[Any, ...], Dict[str, Any]], Any]: # type: ignore @functools.wraps(func) def decorated(*args: Any, **kwargs: Any) -> Any: with self as conn: diff --git a/requirements-testing.txt b/requirements-testing.txt index 4a52f7d58..93a89606a 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -4,7 +4,7 @@ flake8==3.8.4 pytest==6.1.1 pytest-cov==2.10.1 autopep8==1.5.4 -mypy==0.782 +mypy==0.790 py-spy==0.3.3 codecov==2.1.10 tox==3.20.1 From 969990464c4223b044e68daecf511d3f55b37d1a Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 11 Oct 2020 10:26:36 +0530 Subject: [PATCH 53/79] Fix path to devtools websocket endpoint, broken after refactoring (#450) --- proxy/http/inspector/transformer.py | 4 +++- proxy/proxy.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/proxy/http/inspector/transformer.py b/proxy/http/inspector/transformer.py index 32d6b65ec..450ce7bba 100644 --- a/proxy/http/inspector/transformer.py +++ b/proxy/http/inspector/transformer.py @@ -61,8 +61,10 @@ def request_complete(event: Dict[str, Any]) -> Dict[str, Any]: 'wallTime': now, 'hasUserGesture': False, 'type': event['event_payload']['headers']['content-type'] - if event['event_payload']['headers'].has_header('content-type') + if 'content-type' in event['event_payload']['headers'] else 'Other', + # TODO(abhinavsingh): Bring this inline with devtools protocol + 'method': 'Network.requestWillBeSent', 'request': { 'url': event['event_payload']['url'], 'method': event['event_payload']['method'], diff --git a/proxy/proxy.py b/proxy/proxy.py index f807ba74e..996cccbb7 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -31,14 +31,14 @@ from .core.acceptor import AcceptorPool from .http.handler import HttpProtocolHandler from .common.flag import flags -from .common.constants import COMMA, DEFAULT_BASIC_AUTH, DEFAULT_DISABLE_HTTP_PROXY, PY2_DEPRECATION_MESSAGE +from .common.constants import COMMA, DEFAULT_BASIC_AUTH, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_DEVTOOLS from .common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL from .common.constants import DEFAULT_OPEN_FILE_LIMIT, DEFAULT_PID_FILE, DEFAULT_PLUGINS from .common.constants import DEFAULT_VERSION, DOT, PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL from .common.constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE -from .common.constants import PLUGIN_WEB_SERVER +from .common.constants import PLUGIN_WEB_SERVER, PY2_DEPRECATION_MESSAGE if os.name != 'nt': import resource @@ -271,7 +271,7 @@ def initialize(input_args: Optional[List[str]], **opts: Any) -> Flags: bytes, opts.get( 'devtools_ws_path', - getattr(args, 'devtools_ws_path', None))), + getattr(args, 'devtools_ws_path', DEFAULT_DEVTOOLS_WS_PATH))), timeout=cast(int, opts.get('timeout', args.timeout)), threadless=cast(bool, opts.get('threadless', args.threadless)), enable_events=cast( From a48319e32d3c60cb919ef70706b3a3750406f837 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Tue, 13 Oct 2020 20:56:23 +0530 Subject: [PATCH 54/79] Relax proxy auth requirement to allow mixed case for the auth type e.g. "basic", "Basic", "BaSiC" are all allowed (#451) --- proxy/http/proxy/server.py | 6 ++++-- proxy/proxy.py | 2 +- tests/http/test_protocol_handler.py | 9 +++------ tests/test_main.py | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index d0297ea83..168de7e1a 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -535,8 +535,10 @@ def wrap_client(self) -> None: def authenticate(self) -> None: if self.flags.auth_code: - if b'proxy-authorization' not in self.request.headers or \ - self.request.headers[b'proxy-authorization'][1] != self.flags.auth_code: + if b'proxy-authorization' not in self.request.headers: + raise ProxyAuthenticationFailed() + parts = self.request.headers[b'proxy-authorization'][1].split() + if len(parts) != 2 and parts[0].lower() != b'basic' and parts[1] != self.flags.auth_code: raise ProxyAuthenticationFailed() def connect_upstream(self) -> None: diff --git a/proxy/proxy.py b/proxy/proxy.py index 996cccbb7..c97646a14 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -203,7 +203,7 @@ def initialize(input_args: Optional[List[str]], **opts: Any) -> Flags: # Generate auth_code required for basic authentication if enabled auth_code = None if args.basic_auth: - auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth)) + auth_code = base64.b64encode(bytes_(args.basic_auth)) return Flags( plugins=plugins, diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index 8b7b3295c..75f3c544e 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -174,8 +174,7 @@ def test_proxy_authentication_failed( self._conn = mock_fromfd.return_value self.mock_selector_for_client_read(mock_selector) flags = Flags( - auth_code=b'Basic %s' % - base64.b64encode(b'user:pass')) + auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', @@ -208,8 +207,7 @@ def test_authenticated_proxy_http_get( server.buffer_size.return_value = 0 flags = Flags( - auth_code=b'Basic %s' % - base64.b64encode(b'user:pass')) + auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', @@ -258,8 +256,7 @@ def test_authenticated_proxy_http_tunnel( mock_selector, server) flags = Flags( - auth_code=b'Basic %s' % - base64.b64encode(b'user:pass')) + auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin' diff --git a/tests/test_main.py b/tests/test_main.py index b9b5651aa..1f0d7ebbc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -138,7 +138,7 @@ def test_basic_auth( mock_acceptor_pool.assert_called_once() self.assertEqual( flgs.auth_code, - b'Basic dXNlcjpwYXNz') + b'dXNlcjpwYXNz') @mock.patch('time.sleep') @mock.patch('builtins.print') From 0744cd8e7fc6985f5ab5656685833b85a096ddf0 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 14 Oct 2020 10:51:56 +0530 Subject: [PATCH 55/79] Go flagless to allow custom user defined flags. (#452) * Go flagless to allow custom user defined flags. Fixes #301 * Add --cache-dir flag for cache plugin (when used with on-disk store) * Enable discovery of flags from external plugins, example those that reside outside of proxy.py package and loaded on demand. This also allows external flags to surface in --help section * Define --filtered-client-ips flag for FilterByClientIpPlugin --- examples/connect_tunnel.py | 4 +- examples/ssl_echo_server.py | 4 +- examples/tcp_echo_server.py | 4 +- proxy/common/flags.py | 96 -------- proxy/common/utils.py | 3 +- proxy/common/version.py | 2 +- proxy/core/acceptor/acceptor.py | 4 +- proxy/core/acceptor/pool.py | 5 +- proxy/core/acceptor/threadless.py | 4 +- proxy/core/acceptor/work.py | 6 +- proxy/core/event/dispatcher.py | 2 +- proxy/dashboard/plugin.py | 4 +- proxy/http/handler.py | 4 +- proxy/http/plugin.py | 6 +- proxy/http/proxy/plugin.py | 4 +- proxy/http/proxy/server.py | 3 +- proxy/http/server/plugin.py | 4 +- proxy/plugin/cache/cache_responses.py | 3 +- proxy/plugin/cache/store/disk.py | 10 + proxy/plugin/filter_by_client_ip.py | 13 +- proxy/plugin/filter_by_url_regex.py | 8 +- proxy/proxy.py | 223 ++++++++++-------- setup.py | 2 +- tests/core/test_acceptor.py | 4 +- tests/core/test_acceptor_pool.py | 4 +- tests/http/test_http_proxy.py | 4 +- .../http/test_http_proxy_tls_interception.py | 4 +- tests/http/test_protocol_handler.py | 9 +- tests/http/test_web_server.py | 13 +- tests/plugin/test_http_proxy_plugins.py | 4 +- ...ttp_proxy_plugins_with_tls_interception.py | 4 +- tests/test_main.py | 12 +- version-check.py | 1 + 33 files changed, 216 insertions(+), 261 deletions(-) delete mode 100644 proxy/common/flags.py diff --git a/examples/connect_tunnel.py b/examples/connect_tunnel.py index 2ebac0fb7..ad230ece4 100644 --- a/examples/connect_tunnel.py +++ b/examples/connect_tunnel.py @@ -13,6 +13,7 @@ import selectors from typing import Any, Optional, Dict +from proxy.proxy import Proxy from proxy.core.acceptor import AcceptorPool from proxy.core.connection import TcpServerConnection from proxy.http.parser import HttpParser, httpParserTypes, httpParserStates @@ -20,7 +21,6 @@ from proxy.http.methods import httpMethods from proxy.common.types import Readables, Writables from proxy.common.utils import build_http_response, text_ -from proxy.common.flags import Flags from examples.base_server import BaseServerHandler @@ -120,7 +120,7 @@ def connect_upstream(self) -> None: def main() -> None: # This example requires `threadless=True` pool = AcceptorPool( - flags=Flags(port=12345, num_workers=1, threadless=True), + flags=Proxy.initialize(port=12345, num_workers=1, threadless=True), work_klass=ConnectTunnelHandler) try: pool.setup() diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py index 5e660dfae..dfd26dcfb 100644 --- a/examples/ssl_echo_server.py +++ b/examples/ssl_echo_server.py @@ -10,9 +10,9 @@ """ import time +from proxy.proxy import Proxy from proxy.core.acceptor import AcceptorPool from proxy.core.connection import TcpClientConnection -from proxy.common.flags import Flags from proxy.common.utils import wrap_socket from examples.base_server import BaseServerHandler @@ -42,7 +42,7 @@ def handle_data(self, data: memoryview) -> None: def main() -> None: # This example requires `threadless=True` pool = AcceptorPool( - flags=Flags( + flags=Proxy.initialize( port=12345, num_workers=1, threadless=True, diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py index f5b0aa50b..bb904323e 100644 --- a/examples/tcp_echo_server.py +++ b/examples/tcp_echo_server.py @@ -11,7 +11,7 @@ import time from proxy.core.acceptor import AcceptorPool -from proxy.common.flags import Flags +from proxy.proxy import Proxy from examples.base_server import BaseServerHandler @@ -30,7 +30,7 @@ def handle_data(self, data: memoryview) -> None: def main() -> None: # This example requires `threadless=True` pool = AcceptorPool( - flags=Flags(port=12345, num_workers=1, threadless=True), + flags=Proxy.initialize(port=12345, num_workers=1, threadless=True), work_klass=EchoServerHandler) try: pool.setup() diff --git a/proxy/common/flags.py b/proxy/common/flags.py deleted file mode 100644 index 93acc9ae8..000000000 --- a/proxy/common/flags.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- 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 os -import socket -import multiprocessing - -from typing import Optional, Dict, List - -from .types import IpAddress -from .constants import DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH -from .constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS -from .constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_EVENTS -from .constants import DEFAULT_THREADLESS -from .constants import DEFAULT_PAC_FILE_URL_PATH, DEFAULT_PAC_FILE, DEFAULT_PID_FILE, DEFAULT_PORT -from .constants import DEFAULT_IPV6_HOSTNAME -from .constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_STATIC_SERVER_DIR -from .constants import DEFAULT_DATA_DIRECTORY_PATH - - -class Flags: - """Contains all input flags and inferred input parameters.""" - - def __init__( - self, - auth_code: Optional[bytes] = DEFAULT_BASIC_AUTH, - server_recvbuf_size: int = DEFAULT_SERVER_RECVBUF_SIZE, - client_recvbuf_size: int = DEFAULT_CLIENT_RECVBUF_SIZE, - pac_file: Optional[str] = DEFAULT_PAC_FILE, - pac_file_url_path: Optional[bytes] = DEFAULT_PAC_FILE_URL_PATH, - plugins: Optional[Dict[bytes, List[type]]] = None, - disable_headers: Optional[List[bytes]] = None, - certfile: Optional[str] = None, - keyfile: Optional[str] = None, - ca_cert_dir: Optional[str] = None, - ca_key_file: Optional[str] = None, - ca_cert_file: Optional[str] = None, - ca_signing_key_file: Optional[str] = None, - ca_file: Optional[str] = None, - num_workers: int = 0, - hostname: IpAddress = DEFAULT_IPV6_HOSTNAME, - port: int = DEFAULT_PORT, - backlog: int = DEFAULT_BACKLOG, - static_server_dir: str = DEFAULT_STATIC_SERVER_DIR, - enable_static_server: bool = DEFAULT_ENABLE_STATIC_SERVER, - devtools_ws_path: bytes = DEFAULT_DEVTOOLS_WS_PATH, - timeout: int = DEFAULT_TIMEOUT, - threadless: bool = DEFAULT_THREADLESS, - enable_events: bool = DEFAULT_ENABLE_EVENTS, - pid_file: Optional[str] = DEFAULT_PID_FILE) -> None: - self.pid_file = pid_file - self.threadless = threadless - self.timeout = timeout - self.auth_code = auth_code - self.server_recvbuf_size = server_recvbuf_size - self.client_recvbuf_size = client_recvbuf_size - self.pac_file = pac_file - self.pac_file_url_path = pac_file_url_path - if plugins is None: - plugins = {} - self.plugins: Dict[bytes, List[type]] = plugins - if disable_headers is None: - disable_headers = DEFAULT_DISABLE_HEADERS - self.disable_headers = disable_headers - self.certfile: Optional[str] = certfile - self.keyfile: Optional[str] = keyfile - self.ca_key_file: Optional[str] = ca_key_file - self.ca_cert_file: Optional[str] = ca_cert_file - self.ca_signing_key_file: Optional[str] = ca_signing_key_file - self.ca_file = ca_file - self.num_workers: int = num_workers if num_workers > 0 else multiprocessing.cpu_count() - self.hostname: IpAddress = hostname - self.family: socket.AddressFamily = socket.AF_INET6 if hostname.version == 6 else socket.AF_INET - self.port: int = port - self.backlog: int = backlog - - self.enable_static_server: bool = enable_static_server - self.static_server_dir: str = static_server_dir - self.devtools_ws_path: bytes = devtools_ws_path - self.enable_events: bool = enable_events - - self.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH - os.makedirs(self.proxy_py_data_dir, exist_ok=True) - - self.ca_cert_dir: Optional[str] = ca_cert_dir - if self.ca_cert_dir is None: - self.ca_cert_dir = os.path.join( - self.proxy_py_data_dir, 'certificates') - os.makedirs(self.ca_cert_dir, exist_ok=True) diff --git a/proxy/common/utils.py b/proxy/common/utils.py index e125a3d4a..203a17aee 100644 --- a/proxy/common/utils.py +++ b/proxy/common/utils.py @@ -211,7 +211,8 @@ def __exit__( if self.conn: self.conn.close() - def __call__(self, func: Callable[..., Any]) -> Callable[[Tuple[Any, ...], Dict[str, Any]], Any]: # type: ignore + def __call__( # type: ignore + self, func: Callable[..., Any]) -> Callable[[Tuple[Any, ...], Dict[str, Any]], Any]: @functools.wraps(func) def decorated(*args: Any, **kwargs: Any) -> Any: with self as conn: diff --git a/proxy/common/version.py b/proxy/common/version.py index d164acb27..d7dc8e484 100644 --- a/proxy/common/version.py +++ b/proxy/common/version.py @@ -8,5 +8,5 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -VERSION = (2, 2, 1) +VERSION = (2, 3, 0) __version__ = '.'.join(map(str, VERSION[0:3])) diff --git a/proxy/core/acceptor/acceptor.py b/proxy/core/acceptor/acceptor.py index 9cfa6e035..3a9ca6182 100644 --- a/proxy/core/acceptor/acceptor.py +++ b/proxy/core/acceptor/acceptor.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import logging import multiprocessing import multiprocessing.synchronize @@ -25,7 +26,6 @@ from ..connection import TcpClientConnection from ..event import EventQueue, eventNames from ...common.constants import DEFAULT_THREADLESS -from ...common.flags import Flags from ...common.flag import flags logger = logging.getLogger(__name__) @@ -60,7 +60,7 @@ def __init__( self, idd: int, work_queue: connection.Connection, - flags: Flags, + flags: argparse.Namespace, work_klass: Type[Work], lock: multiprocessing.synchronize.Lock, event_queue: Optional[EventQueue] = None) -> None: diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py index f330ea033..65b98c72a 100644 --- a/proxy/core/acceptor/pool.py +++ b/proxy/core/acceptor/pool.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import logging import multiprocessing import socket @@ -20,7 +21,6 @@ from .work import Work from ..event import EventQueue, EventDispatcher -from ...common.flags import Flags from ...common.flag import flags from ...common.constants import DEFAULT_BACKLOG, DEFAULT_ENABLE_EVENTS from ...common.constants import DEFAULT_IPV6_HOSTNAME, DEFAULT_NUM_WORKERS, DEFAULT_PORT @@ -86,7 +86,8 @@ class AcceptorPool: for message sharing or signaling within proxy.py. """ - def __init__(self, flags: Flags, work_klass: Type[Work]) -> None: + def __init__(self, flags: argparse.Namespace, + work_klass: Type[Work]) -> None: self.flags = flags self.socket: Optional[socket.socket] = None self.acceptors: List[Acceptor] = [] diff --git a/proxy/core/acceptor/threadless.py b/proxy/core/acceptor/threadless.py index 78a59cc9f..87ef4aba7 100644 --- a/proxy/core/acceptor/threadless.py +++ b/proxy/core/acceptor/threadless.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import os import socket import logging @@ -25,7 +26,6 @@ from ..connection import TcpClientConnection from ..event import EventQueue, eventNames -from ...common.flags import Flags from ...common.types import Readables, Writables from ...common.constants import DEFAULT_TIMEOUT @@ -47,7 +47,7 @@ class Threadless(multiprocessing.Process): def __init__( self, client_queue: connection.Connection, - flags: Flags, + flags: argparse.Namespace, work_klass: Type[Work], event_queue: Optional[EventQueue] = None) -> None: super().__init__() diff --git a/proxy/core/acceptor/work.py b/proxy/core/acceptor/work.py index bcd251f7b..6bf3880ec 100644 --- a/proxy/core/acceptor/work.py +++ b/proxy/core/acceptor/work.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import socket from abc import ABC, abstractmethod @@ -16,7 +17,6 @@ from ..event import eventNames, EventQueue from ..connection import TcpClientConnection -from ...common.flags import Flags from ...common.types import Readables, Writables @@ -26,11 +26,11 @@ class Work(ABC): def __init__( self, client: TcpClientConnection, - flags: Optional[Flags], + flags: argparse.Namespace, event_queue: Optional[EventQueue] = None, uid: Optional[UUID] = None) -> None: self.client = client - self.flags = flags if flags else Flags() + self.flags = flags self.event_queue = event_queue self.uid: UUID = uid if uid is not None else uuid4() diff --git a/proxy/core/event/dispatcher.py b/proxy/core/event/dispatcher.py index d978bebe0..fb7c52753 100644 --- a/proxy/core/event/dispatcher.py +++ b/proxy/core/event/dispatcher.py @@ -38,7 +38,7 @@ class EventDispatcher: event at a time. When --enable-events is used, a multiprocessing.Queue is created and - attached to global Flags. This queue can then be used for + attached to global argparse. This queue can then be used for dispatching an Event dict object into the queue. When --enable-events is used, dispatcher module is automatically diff --git a/proxy/dashboard/plugin.py b/proxy/dashboard/plugin.py index 7c5b9a3b9..b3787ac2d 100644 --- a/proxy/dashboard/plugin.py +++ b/proxy/dashboard/plugin.py @@ -8,12 +8,12 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import json from abc import ABC, abstractmethod from typing import List, Dict, Any from ..common.utils import bytes_ -from ..common.flags import Flags from ..http.websocket import WebsocketFrame from ..core.connection import TcpClientConnection from ..core.event import EventQueue @@ -24,7 +24,7 @@ class ProxyDashboardWebsocketPlugin(ABC): def __init__( self, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, event_queue: EventQueue) -> None: self.flags = flags diff --git a/proxy/http/handler.py b/proxy/http/handler.py index ba4ccba45..474ef6226 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import socket import selectors import ssl @@ -23,7 +24,6 @@ from .parser import HttpParser, httpParserStates, httpParserTypes from .exception import HttpProtocolException -from ..common.flags import Flags from ..common.types import Readables, Writables from ..common.utils import wrap_socket from ..core.acceptor.work import Work @@ -69,7 +69,7 @@ class HttpProtocolHandler(Work): """ def __init__(self, client: TcpClientConnection, - flags: Optional[Flags] = None, + flags: argparse.Namespace, event_queue: Optional[EventQueue] = None, uid: Optional[UUID] = None): super().__init__(client, flags, event_queue, uid) diff --git a/proxy/http/plugin.py b/proxy/http/plugin.py index b8d95b286..2e7fed454 100644 --- a/proxy/http/plugin.py +++ b/proxy/http/plugin.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import socket from abc import ABC, abstractmethod @@ -16,7 +17,6 @@ from .parser import HttpParser -from ..common.flags import Flags from ..common.types import Readables, Writables from ..core.event import EventQueue from ..core.connection import TcpClientConnection @@ -47,12 +47,12 @@ class HttpProtocolHandlerPlugin(ABC): def __init__( self, uid: UUID, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, request: HttpParser, event_queue: EventQueue): self.uid: UUID = uid - self.flags: Flags = flags + self.flags: argparse.Namespace = flags self.client: TcpClientConnection = client self.request: HttpParser = request self.event_queue = event_queue diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py index 44129ed86..7e5c1b0ba 100644 --- a/proxy/http/proxy/plugin.py +++ b/proxy/http/proxy/plugin.py @@ -9,11 +9,11 @@ :license: BSD, see LICENSE for more details. """ from abc import ABC, abstractmethod +import argparse from typing import Optional from uuid import UUID from ..parser import HttpParser -from ...common.flags import Flags from ...core.event import EventQueue from ...core.connection import TcpClientConnection @@ -27,7 +27,7 @@ class HttpProxyBasePlugin(ABC): def __init__( self, uid: UUID, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, event_queue: EventQueue) -> None: self.uid = uid # pragma: no cover diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 168de7e1a..966b21008 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -538,7 +538,8 @@ def authenticate(self) -> None: if b'proxy-authorization' not in self.request.headers: raise ProxyAuthenticationFailed() parts = self.request.headers[b'proxy-authorization'][1].split() - if len(parts) != 2 and parts[0].lower() != b'basic' and parts[1] != self.flags.auth_code: + if len(parts) != 2 and parts[0].lower( + ) != b'basic' and parts[1] != self.flags.auth_code: raise ProxyAuthenticationFailed() def connect_upstream(self) -> None: diff --git a/proxy/http/server/plugin.py b/proxy/http/server/plugin.py index a1e17c1e6..3cb737ea7 100644 --- a/proxy/http/server/plugin.py +++ b/proxy/http/server/plugin.py @@ -9,12 +9,12 @@ :license: BSD, see LICENSE for more details. """ from abc import ABC, abstractmethod +import argparse from typing import List, Tuple from uuid import UUID from ..websocket import WebsocketFrame from ..parser import HttpParser -from ...common.flags import Flags from ...core.connection import TcpClientConnection from ...core.event import EventQueue @@ -25,7 +25,7 @@ class HttpWebServerBasePlugin(ABC): def __init__( self, uid: UUID, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, event_queue: EventQueue): self.uid = uid diff --git a/proxy/plugin/cache/cache_responses.py b/proxy/plugin/cache/cache_responses.py index 91f290790..f6da087e5 100644 --- a/proxy/plugin/cache/cache_responses.py +++ b/proxy/plugin/cache/cache_responses.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for more details. """ import multiprocessing -import tempfile from typing import Any from .store.disk import OnDiskCacheStore @@ -25,5 +24,5 @@ class CacheResponsesPlugin(BaseCacheResponsesPlugin): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.disk_store = OnDiskCacheStore( - uid=self.uid, cache_dir=tempfile.gettempdir()) + uid=self.uid, cache_dir=self.flags.cache_dir) self.set_store(self.disk_store) diff --git a/proxy/plugin/cache/store/disk.py b/proxy/plugin/cache/store/disk.py index 91eb4f295..1f472a12c 100644 --- a/proxy/plugin/cache/store/disk.py +++ b/proxy/plugin/cache/store/disk.py @@ -10,9 +10,11 @@ """ import logging import os +import tempfile from typing import Optional, BinaryIO from uuid import UUID +from ....common.flag import flags from ....common.utils import text_ from ....http.parser import HttpParser @@ -21,6 +23,14 @@ logger = logging.getLogger(__name__) +flags.add_argument( + '--cache-dir', + type=str, + default=tempfile.gettempdir(), + help='Default: A temporary directory. Flag only applicable when cache plugin is used with on-disk storage.' +) + + class OnDiskCacheStore(CacheStore): def __init__(self, uid: UUID, cache_dir: str) -> None: diff --git a/proxy/plugin/filter_by_client_ip.py b/proxy/plugin/filter_by_client_ip.py index 41ce7da2f..95169b488 100644 --- a/proxy/plugin/filter_by_client_ip.py +++ b/proxy/plugin/filter_by_client_ip.py @@ -10,20 +10,27 @@ """ from typing import Optional +from ..common.flag import flags from ..http.exception import HttpRequestRejected from ..http.parser import HttpParser from ..http.codes import httpStatusCodes from ..http.proxy import HttpProxyBasePlugin +flags.add_argument( + '--filtered-client-ips', + type=str, + default='127.0.0.1,::1', + help='Default: 127.0.0.1,::1. Comma separated list of IPv4 and IPv6 addresses.' +) + + class FilterByClientIpPlugin(HttpProxyBasePlugin): """Drop traffic by inspecting incoming client IP address.""" - FILTERED_IPS = ['127.0.0.1', '::1'] - def before_upstream_connection( self, request: HttpParser) -> Optional[HttpParser]: - if self.client.addr[0] in self.FILTERED_IPS: + if self.client.addr[0] in self.flags.filtered_client_ips.split(','): raise HttpRequestRejected( status_code=httpStatusCodes.I_AM_A_TEAPOT, reason=b'I\'m a tea pot', headers={ diff --git a/proxy/plugin/filter_by_url_regex.py b/proxy/plugin/filter_by_url_regex.py index a4106c94f..1e9d5efbc 100644 --- a/proxy/plugin/filter_by_url_regex.py +++ b/proxy/plugin/filter_by_url_regex.py @@ -25,10 +25,10 @@ class FilterByURLRegexPlugin(HttpProxyBasePlugin): - """ - Drop traffic by inspecting request URL, - checking against a list of regular expressions, - then returning a HTTP status code. + """Drops traffic by inspecting request URL and checking + against a list of regular expressions. Example, default + filter list below can be used as a starting point for + filtering ads. """ FILTER_LIST: List[Dict[str, Any]] = [ diff --git a/proxy/proxy.py b/proxy/proxy.py index c97646a14..8a6440882 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -14,7 +14,9 @@ import collections import contextlib import ipaddress +import multiprocessing import os +import socket import sys import time import logging @@ -25,13 +27,14 @@ from typing import Dict, List, Optional, Generator, Any, Tuple, Type, Union, cast from .common.utils import bytes_, text_ -from .common.flags import Flags from .common.types import IpAddress from .common.version import __version__ from .core.acceptor import AcceptorPool from .http.handler import HttpProtocolHandler from .common.flag import flags -from .common.constants import COMMA, DEFAULT_BASIC_AUTH, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY +from .common.constants import COMMA, DEFAULT_BASIC_AUTH, DEFAULT_DATA_DIRECTORY_PATH +from .common.constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS +from .common.constants import DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_NUM_WORKERS from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_DEVTOOLS from .common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL @@ -163,11 +166,22 @@ def __exit__( self.delete_pid_file() @staticmethod - def initialize(input_args: Optional[List[str]], **opts: Any) -> Flags: + def initialize(input_args: Optional[List[str]] + = None, **opts: Any) -> argparse.Namespace: + if input_args is None: + input_args = [] + if not Proxy.is_py3(): print(PY2_DEPRECATION_MESSAGE) sys.exit(1) + # Discover flags from requested plugin. + # This also surface external plugin flags under --help + for i, f in enumerate(input_args): + if f == '--plugin': + Proxy.import_plugin(bytes_(input_args[i + 1])) + + # Parse flags args = flags.parse_args(input_args) # Print version and exit @@ -205,82 +219,100 @@ def initialize(input_args: Optional[List[str]], **opts: Any) -> Flags: if args.basic_auth: auth_code = base64.b64encode(bytes_(args.basic_auth)) - return Flags( - plugins=plugins, - auth_code=cast(Optional[bytes], opts.get('auth_code', auth_code)), - server_recvbuf_size=cast( - int, - opts.get( - 'server_recvbuf_size', - args.server_recvbuf_size)), - client_recvbuf_size=cast( - int, - opts.get( - 'client_recvbuf_size', - args.client_recvbuf_size)), - pac_file=cast( - Optional[str], opts.get( - 'pac_file', bytes_( - args.pac_file))), - pac_file_url_path=cast( - Optional[bytes], opts.get( - 'pac_file_url_path', bytes_( - args.pac_file_url_path))), - disable_headers=cast(Optional[List[bytes]], opts.get('disable_headers', [ - header.lower() for header in bytes_( - args.disable_headers).split(COMMA) if header.strip() != b''])), - certfile=cast( - Optional[str], opts.get( - 'cert_file', args.cert_file)), - keyfile=cast(Optional[str], opts.get('key_file', args.key_file)), - ca_cert_dir=cast( - Optional[str], opts.get( - 'ca_cert_dir', args.ca_cert_dir)), - ca_key_file=cast( - Optional[str], opts.get( - 'ca_key_file', args.ca_key_file)), - ca_cert_file=cast( - Optional[str], opts.get( - 'ca_cert_file', args.ca_cert_file)), - ca_signing_key_file=cast( - Optional[str], - opts.get( - 'ca_signing_key_file', - args.ca_signing_key_file)), - ca_file=cast( - Optional[str], - opts.get( - 'ca_file', - args.ca_file)), - hostname=cast(IpAddress, - opts.get('hostname', ipaddress.ip_address(args.hostname))), - port=cast(int, opts.get('port', args.port)), - backlog=cast(int, opts.get('backlog', args.backlog)), - num_workers=cast(int, opts.get('num_workers', args.num_workers)), - static_server_dir=cast( - str, - opts.get( - 'static_server_dir', - args.static_server_dir)), - enable_static_server=cast( - bool, - opts.get( - 'enable_static_server', - args.enable_static_server)), - devtools_ws_path=cast( - bytes, - opts.get( - 'devtools_ws_path', - getattr(args, 'devtools_ws_path', DEFAULT_DEVTOOLS_WS_PATH))), - timeout=cast(int, opts.get('timeout', args.timeout)), - threadless=cast(bool, opts.get('threadless', args.threadless)), - enable_events=cast( - bool, - opts.get( - 'enable_events', - args.enable_events)), - pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file)) - ) + args.plugins = plugins + args.auth_code = cast( + Optional[bytes], + opts.get( + 'auth_code', + auth_code)) + args.server_recvbuf_size = cast( + int, + opts.get( + 'server_recvbuf_size', + args.server_recvbuf_size)) + args.client_recvbuf_size = cast( + int, + opts.get( + 'client_recvbuf_size', + args.client_recvbuf_size)) + args.pac_file = cast( + Optional[str], opts.get( + 'pac_file', bytes_( + args.pac_file))) + args.pac_file_url_path = cast( + Optional[bytes], opts.get( + 'pac_file_url_path', bytes_( + args.pac_file_url_path))) + disabled_headers = cast(Optional[List[bytes]], opts.get('disable_headers', [ + header.lower() for header in bytes_( + args.disable_headers).split(COMMA) if header.strip() != b''])) + args.disable_headers = disabled_headers if disabled_headers is not None else DEFAULT_DISABLE_HEADERS + args.certfile = cast( + Optional[str], opts.get( + 'cert_file', args.cert_file)) + args.keyfile = cast(Optional[str], opts.get('key_file', args.key_file)) + args.ca_key_file = cast( + Optional[str], opts.get( + 'ca_key_file', args.ca_key_file)) + args.ca_cert_file = cast( + Optional[str], opts.get( + 'ca_cert_file', args.ca_cert_file)) + args.ca_signing_key_file = cast( + Optional[str], + opts.get( + 'ca_signing_key_file', + args.ca_signing_key_file)) + args.ca_file = cast( + Optional[str], + opts.get( + 'ca_file', + args.ca_file)) + args.hostname = cast(IpAddress, + opts.get('hostname', ipaddress.ip_address(args.hostname))) + args.family = socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET + args.port = cast(int, opts.get('port', args.port)) + args.backlog = cast(int, opts.get('backlog', args.backlog)) + num_workers = opts.get('num_workers', args.num_workers) + num_workers = num_workers if num_workers is not None else DEFAULT_NUM_WORKERS + args.num_workers = cast( + int, num_workers if num_workers > 0 else multiprocessing.cpu_count()) + args.static_server_dir = cast( + str, + opts.get( + 'static_server_dir', + args.static_server_dir)) + args.enable_static_server = cast( + bool, + opts.get( + 'enable_static_server', + args.enable_static_server)) + args.devtools_ws_path = cast( + bytes, + opts.get( + 'devtools_ws_path', + getattr(args, 'devtools_ws_path', DEFAULT_DEVTOOLS_WS_PATH))) + args.timeout = cast(int, opts.get('timeout', args.timeout)) + args.threadless = cast(bool, opts.get('threadless', args.threadless)) + args.enable_events = cast( + bool, + opts.get( + 'enable_events', + args.enable_events)) + args.pid_file = cast( + Optional[str], opts.get( + 'pid_file', args.pid_file)) + + args.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH + os.makedirs(args.proxy_py_data_dir, exist_ok=True) + + ca_cert_dir = opts.get('ca_cert_dir', args.ca_cert_dir) + args.ca_cert_dir = cast(Optional[str], ca_cert_dir) + if args.ca_cert_dir is None: + args.ca_cert_dir = os.path.join( + args.proxy_py_data_dir, 'certificates') + os.makedirs(args.ca_cert_dir, exist_ok=True) + + return args @staticmethod def load_plugins(plugins: List[Union[bytes, type]] @@ -294,19 +326,9 @@ def load_plugins(plugins: List[Union[bytes, type]] b'ProxyDashboardWebsocketPlugin': [] } for plugin_ in plugins: - if isinstance(plugin_, type): - module_name = '__main__' - klass = plugin_ - else: - plugin = text_(plugin_.strip()) - if plugin == '': - continue - module_name, klass_name = plugin.rsplit(text_(DOT), 1) - klass = getattr( - importlib.import_module( - module_name.replace( - os.path.sep, text_(DOT))), - klass_name) + klass, module_name = Proxy.import_plugin(plugin_) + if klass is None and module_name is None: + continue mro = list(inspect.getmro(klass)) mro.reverse() iterator = iter(mro) @@ -318,6 +340,23 @@ def load_plugins(plugins: List[Union[bytes, type]] logger.info('Loaded plugin %s.%s', module_name, klass.__name__) return p + @staticmethod + def import_plugin(plugin: Union[bytes, type]) -> Any: + if isinstance(plugin, type): + module_name = '__main__' + klass = plugin + else: + plugin_ = text_(plugin.strip()) + if plugin_ == '': + return (None, None) + module_name, klass_name = plugin_.rsplit(text_(DOT), 1) + klass = getattr( + importlib.import_module( + module_name.replace( + os.path.sep, text_(DOT))), + klass_name) + return (klass, module_name) + @staticmethod def get_default_plugins( args: argparse.Namespace) -> List[Tuple[str, bool]]: diff --git a/setup.py b/setup.py index 63df325b7..b09bc8009 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ """ from setuptools import setup, find_packages -VERSION = (2, 2, 1) +VERSION = (2, 3, 0) __version__ = '.'.join(map(str, VERSION[0:3])) __description__ = '''⚡⚡⚡Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on Network monitoring, controls & Application development, testing, debugging.''' diff --git a/tests/core/test_acceptor.py b/tests/core/test_acceptor.py index 2b4dbd9bc..aae3de5ca 100644 --- a/tests/core/test_acceptor.py +++ b/tests/core/test_acceptor.py @@ -14,8 +14,8 @@ import multiprocessing from unittest import mock -from proxy.common.flags import Flags from proxy.core.acceptor import Acceptor +from proxy.proxy import Proxy class TestAcceptor(unittest.TestCase): @@ -24,7 +24,7 @@ def setUp(self) -> None: self.acceptor_id = 1 self.mock_protocol_handler = mock.MagicMock() self.pipe = multiprocessing.Pipe() - self.flags = Flags() + self.flags = Proxy.initialize() self.acceptor = Acceptor( idd=self.acceptor_id, work_queue=self.pipe[1], diff --git a/tests/core/test_acceptor_pool.py b/tests/core/test_acceptor_pool.py index e8192495e..3142e39bf 100644 --- a/tests/core/test_acceptor_pool.py +++ b/tests/core/test_acceptor_pool.py @@ -12,7 +12,7 @@ import socket from unittest import mock -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.acceptor import AcceptorPool @@ -35,7 +35,7 @@ def test_setup_and_shutdown( num_workers = 2 sock = mock_socket.return_value work_klass = mock.MagicMock() - flags = Flags(num_workers=2) + flags = Proxy.initialize(num_workers=2) pool = AcceptorPool(flags=flags, work_klass=work_klass) pool.setup() diff --git a/tests/http/test_http_proxy.py b/tests/http/test_http_proxy.py index 60024d3f0..1be5af965 100644 --- a/tests/http/test_http_proxy.py +++ b/tests/http/test_http_proxy.py @@ -13,7 +13,7 @@ from unittest import mock from proxy.common.constants import DEFAULT_HTTP_PORT -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.connection import TcpClientConnection from proxy.http.proxy import HttpProxyPlugin from proxy.http.handler import HttpProtocolHandler @@ -33,7 +33,7 @@ def setUp(self, self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags() + self.flags = Proxy.initialize() self.plugin = mock.MagicMock() self.flags.plugins = { b'HttpProtocolHandlerPlugin': [HttpProxyPlugin], diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py index c904e69c8..ddb798701 100644 --- a/tests/http/test_http_proxy_tls_interception.py +++ b/tests/http/test_http_proxy_tls_interception.py @@ -22,7 +22,7 @@ from proxy.http.proxy import HttpProxyPlugin from proxy.http.methods import httpMethods from proxy.common.utils import build_http_request, bytes_ -from proxy.common.flags import Flags +from proxy.proxy import Proxy class TestHttpProxyTlsInterception(unittest.TestCase): @@ -81,7 +81,7 @@ def mock_connection() -> Any: self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags( + self.flags = Proxy.initialize( ca_cert_file='ca-cert.pem', ca_key_file='ca-key.pem', ca_signing_key_file='ca-signing-key.pem' diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index 75f3c544e..a96d43f37 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -17,7 +17,6 @@ from proxy.proxy import Proxy from proxy.common.version import __version__ -from proxy.common.flags import Flags from proxy.common.utils import bytes_ from proxy.common.constants import CRLF from proxy.core.connection import TcpClientConnection @@ -40,7 +39,7 @@ def setUp(self, self._conn = mock_fromfd.return_value self.http_server_port = 65535 - self.flags = Flags() + self.flags = Proxy.initialize() self.flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', @@ -173,7 +172,7 @@ def test_proxy_authentication_failed( mock_selector: mock.Mock) -> None: self._conn = mock_fromfd.return_value self.mock_selector_for_client_read(mock_selector) - flags = Flags( + flags = Proxy.initialize( auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', @@ -206,7 +205,7 @@ def test_authenticated_proxy_http_get( server.connect.return_value = True server.buffer_size.return_value = 0 - flags = Flags( + flags = Proxy.initialize( auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', @@ -255,7 +254,7 @@ def test_authenticated_proxy_http_tunnel( self.mock_selector_for_client_read_read_server_write( mock_selector, server) - flags = Flags( + flags = Proxy.initialize( auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 8c69b3940..43ff86c32 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -16,7 +16,6 @@ from unittest import mock from proxy.proxy import Proxy -from proxy.common.flags import Flags from proxy.core.connection import TcpClientConnection from proxy.http.handler import HttpProtocolHandler from proxy.http.parser import httpParserStates @@ -34,7 +33,7 @@ def setUp(self, mock_fromfd: mock.Mock, mock_selector: mock.Mock) -> None: self._addr = ('127.0.0.1', 54382) self._conn = mock_fromfd.return_value self.mock_selector = mock_selector - self.flags = Flags() + self.flags = Proxy.initialize() self.flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', @@ -97,7 +96,7 @@ def test_default_web_server_returns_404( fd=self._conn.fileno, events=selectors.EVENT_READ, data=None), selectors.EVENT_READ), ] - flags = Flags() + flags = Proxy.initialize() flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', @@ -148,7 +147,7 @@ def test_static_web_server_serves( events=selectors.EVENT_WRITE, data=None), selectors.EVENT_WRITE)], ] - flags = Flags( + flags = Proxy.initialize( enable_static_server=True, static_server_dir=static_server_dir) flags.plugins = Proxy.load_plugins([ @@ -200,7 +199,7 @@ def test_static_web_server_serves_404( events=selectors.EVENT_WRITE, data=None), selectors.EVENT_WRITE)], ] - flags = Flags(enable_static_server=True) + flags = Proxy.initialize(enable_static_server=True) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', @@ -222,7 +221,7 @@ def test_static_web_server_serves_404( @mock.patch('socket.fromfd') def test_on_client_connection_called_on_teardown( self, mock_fromfd: mock.Mock) -> None: - flags = Flags() + flags = Proxy.initialize() plugin = mock.MagicMock() flags.plugins = {b'HttpProtocolHandlerPlugin': [plugin]} self._conn = mock_fromfd.return_value @@ -238,7 +237,7 @@ def test_on_client_connection_called_on_teardown( plugin.return_value.on_client_connection_close.assert_called() def init_and_make_pac_file_request(self, pac_file: str) -> None: - flags = Flags(pac_file=pac_file) + flags = Proxy.initialize(pac_file=pac_file) flags.plugins = Proxy.load_plugins([ b'proxy.http.proxy.HttpProxyPlugin', b'proxy.http.server.HttpWebServerPlugin', diff --git a/tests/plugin/test_http_proxy_plugins.py b/tests/plugin/test_http_proxy_plugins.py index 51b3cf607..28e85896e 100644 --- a/tests/plugin/test_http_proxy_plugins.py +++ b/tests/plugin/test_http_proxy_plugins.py @@ -16,7 +16,7 @@ from unittest import mock from typing import cast -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.connection import TcpClientConnection from proxy.http.handler import HttpProtocolHandler from proxy.http.proxy import HttpProxyPlugin @@ -38,7 +38,7 @@ def setUp(self, mock_selector: mock.Mock) -> None: self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags() + self.flags = Proxy.initialize() self.plugin = mock.MagicMock() self.mock_fromfd = mock_fromfd diff --git a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py index f3289e0dd..39311b22a 100644 --- a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py +++ b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py @@ -16,8 +16,8 @@ from unittest import mock from typing import Any, cast +from proxy.proxy import Proxy from proxy.common.utils import bytes_ -from proxy.common.flags import Flags from proxy.common.utils import build_http_request, build_http_response from proxy.core.connection import TcpClientConnection, TcpServerConnection from proxy.http.codes import httpStatusCodes @@ -62,7 +62,7 @@ def setUp(self, self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags( + self.flags = Proxy.initialize( ca_cert_file='ca-cert.pem', ca_key_file='ca-key.pem', ca_signing_key_file='ca-signing-key.pem',) diff --git a/tests/test_main.py b/tests/test_main.py index 1f0d7ebbc..0e171eb04 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for more details. """ import unittest -import logging import tempfile import os @@ -69,26 +68,21 @@ def mock_default_args(mock_args: mock.Mock) -> None: mock_args.enable_events = DEFAULT_ENABLE_EVENTS @mock.patch('time.sleep') - @mock.patch('proxy.proxy.Flags') + @mock.patch('proxy.proxy.Proxy.initialize') @mock.patch('proxy.proxy.AcceptorPool') @mock.patch('logging.basicConfig') def test_init_with_no_arguments( self, mock_logging_config: mock.Mock, mock_acceptor_pool: mock.Mock, - mock_flags: mock.Mock, + mock_initialize: mock.Mock, mock_sleep: mock.Mock) -> None: mock_sleep.side_effect = KeyboardInterrupt() input_args: List[str] = [] main(input_args) - - mock_logging_config.assert_called_with( - level=logging.INFO, - format=DEFAULT_LOG_FORMAT - ) mock_acceptor_pool.assert_called_with( - flags=mock_flags.return_value, + flags=mock_initialize.return_value, work_klass=HttpProtocolHandler, ) mock_acceptor_pool.return_value.setup.assert_called() diff --git a/version-check.py b/version-check.py index c6f88b52c..b46222500 100644 --- a/version-check.py +++ b/version-check.py @@ -21,4 +21,5 @@ # 3. TODO: Version is also hardcoded in README.md flags # section if lib_version != pkg_version: + print('Version mismatch found. {0} (lib) vs {1} (pkg).'.format(lib_version, pkg_version)) sys.exit(1) From 137ce457bba9d4f3146ae49e28762534028b81cf Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Wed, 14 Oct 2020 20:00:29 +0530 Subject: [PATCH 56/79] Separate basic auth plugin outside of core server (#453) * Separate basic auth plugin outside of core * Put basic auth plugin at top --- Makefile | 2 +- proxy/common/constants.py | 1 + proxy/core/connection/connection.py | 1 + proxy/http/proxy/__init__.py | 2 ++ proxy/http/proxy/auth.py | 50 +++++++++++++++++++++++++++++ proxy/http/proxy/server.py | 16 ++------- proxy/proxy.py | 14 +++----- tests/common/test_flags.py | 6 ++-- tests/http/test_protocol_handler.py | 19 +++++------ tests/http/test_web_server.py | 24 +++++++------- 10 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 proxy/http/proxy/auth.py diff --git a/Makefile b/Makefile index 92ae5d0e6..e0331a5ec 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ lib-coverage: open htmlcov/index.html lib-profile: - sudo py-spy -F -f profile.svg -d 3600 proxy.py + sudo py-spy record -o profile.svg -t -F -s -- python -m proxy dashboard: pushd dashboard && npm run build && popd diff --git a/proxy/common/constants.py b/proxy/common/constants.py index a65b0874f..21e395508 100644 --- a/proxy/common/constants.py +++ b/proxy/common/constants.py @@ -86,6 +86,7 @@ PLUGIN_DEVTOOLS_PROTOCOL = 'proxy.http.inspector.DevtoolsProtocolPlugin' PLUGIN_DASHBOARD = 'proxy.dashboard.dashboard.ProxyDashboard' PLUGIN_INSPECT_TRAFFIC = 'proxy.dashboard.inspect_traffic.InspectTrafficPlugin' +PLUGIN_PROXY_AUTH = 'proxy.http.proxy.AuthPlugin' PY2_DEPRECATION_MESSAGE = '''DEPRECATION: proxy.py no longer supports Python 2.7. Kindly upgrade to Python 3+. ' 'If for some reasons you cannot upgrade, use' diff --git a/proxy/core/connection/connection.py b/proxy/core/connection/connection.py index 3aa72eebc..73eabe208 100644 --- a/proxy/core/connection/connection.py +++ b/proxy/core/connection/connection.py @@ -88,5 +88,6 @@ def flush(self) -> int: self.buffer.pop(0) else: self.buffer[0] = memoryview(mv[sent:]) + del mv logger.debug('flushed %d bytes to %s' % (sent, self.tag)) return sent diff --git a/proxy/http/proxy/__init__.py b/proxy/http/proxy/__init__.py index afd352711..4e18002c0 100644 --- a/proxy/http/proxy/__init__.py +++ b/proxy/http/proxy/__init__.py @@ -10,8 +10,10 @@ """ from .plugin import HttpProxyBasePlugin from .server import HttpProxyPlugin +from .auth import AuthPlugin __all__ = [ 'HttpProxyBasePlugin', 'HttpProxyPlugin', + 'AuthPlugin', ] diff --git a/proxy/http/proxy/auth.py b/proxy/http/proxy/auth.py new file mode 100644 index 000000000..caa7cf4ee --- /dev/null +++ b/proxy/http/proxy/auth.py @@ -0,0 +1,50 @@ +# -*- 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. +""" +from typing import Optional + +from ..exception import ProxyAuthenticationFailed +from ...common.flag import flags +from ...common.constants import DEFAULT_BASIC_AUTH +from ...http.parser import HttpParser +from ...http.proxy import HttpProxyBasePlugin + + +flags.add_argument( + '--basic-auth', + type=str, + default=DEFAULT_BASIC_AUTH, + help='Default: No authentication. Specify colon separated user:password ' + 'to enable basic authentication.') + + +class AuthPlugin(HttpProxyBasePlugin): + """Performs proxy authentication.""" + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + if self.flags.auth_code: + if b'proxy-authorization' not in request.headers: + raise ProxyAuthenticationFailed() + parts = request.headers[b'proxy-authorization'][1].split() + if len(parts) != 2 and parts[0].lower( + ) != b'basic' and parts[1] != self.flags.auth_code: + raise ProxyAuthenticationFailed() + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 966b21008..f60ff5436 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -20,7 +20,7 @@ from .plugin import HttpProxyBasePlugin from ..plugin import HttpProtocolHandlerPlugin -from ..exception import HttpProtocolException, ProxyConnectionFailed, ProxyAuthenticationFailed +from ..exception import HttpProtocolException, ProxyConnectionFailed from ..codes import httpStatusCodes from ..parser import HttpParser, httpParserStates, httpParserTypes from ..methods import httpMethods @@ -105,8 +105,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): reason=b'Connection established' )) - # Used to synchronize with other HttpProxyPlugin instances while - # generating certificates + # Used to synchronization during certificate generation. lock = threading.Lock() def __init__( @@ -322,8 +321,6 @@ def on_request_complete(self) -> Union[socket.socket, bool]: self.emit_request_complete() - self.authenticate() - # Note: can raise HttpRequestRejected exception # Invoke plugin.before_upstream_connection do_connect = True @@ -533,15 +530,6 @@ def wrap_client(self) -> None: logger.debug( 'TLS interception using %s', generated_cert) - def authenticate(self) -> None: - if self.flags.auth_code: - if b'proxy-authorization' not in self.request.headers: - raise ProxyAuthenticationFailed() - parts = self.request.headers[b'proxy-authorization'][1].split() - if len(parts) != 2 and parts[0].lower( - ) != b'basic' and parts[1] != self.flags.auth_code: - raise ProxyAuthenticationFailed() - def connect_upstream(self) -> None: host, port = self.request.host, self.request.port if host and port: diff --git a/proxy/proxy.py b/proxy/proxy.py index 8a6440882..023c43cb4 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -32,7 +32,7 @@ from .core.acceptor import AcceptorPool from .http.handler import HttpProtocolHandler from .common.flag import flags -from .common.constants import COMMA, DEFAULT_BASIC_AUTH, DEFAULT_DATA_DIRECTORY_PATH +from .common.constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, PLUGIN_PROXY_AUTH from .common.constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS from .common.constants import DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_NUM_WORKERS from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_DEVTOOLS @@ -54,12 +54,6 @@ type=str, default=DEFAULT_PID_FILE, help='Default: None. Save parent process ID to a file.') -flags.add_argument( - '--basic-auth', - type=str, - default=DEFAULT_BASIC_AUTH, - help='Default: No authentication. Specify colon separated user:password ' - 'to enable basic authentication.') flags.add_argument( '--version', '-v', @@ -360,9 +354,11 @@ def import_plugin(plugin: Union[bytes, type]) -> Any: @staticmethod def get_default_plugins( args: argparse.Namespace) -> List[Tuple[str, bool]]: - # Prepare list of plugins to load based upon --enable-* and --disable-* - # flags + # Prepare list of plugins to load based upon + # --enable-*, --disable-* and --basic-auth flags. default_plugins: List[Tuple[str, bool]] = [] + if args.basic_auth is not None: + default_plugins.append((PLUGIN_PROXY_AUTH, True)) if args.enable_dashboard: default_plugins.append((PLUGIN_WEB_SERVER, True)) args.enable_static_server = True diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py index 50adb831e..c36f8e635 100644 --- a/tests/common/test_flags.py +++ b/tests/common/test_flags.py @@ -8,6 +8,8 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +from proxy.common.utils import bytes_ +from proxy.common.constants import PLUGIN_HTTP_PROXY import unittest from typing import List, Dict @@ -86,7 +88,7 @@ def test_load_plugins_from_bytes_and_class(self) -> None: def test_unique_plugin_from_bytes(self) -> None: self.flags = Proxy.initialize([], plugins=[ - b'proxy.http.proxy.HttpProxyPlugin', + bytes_(PLUGIN_HTTP_PROXY), ]) self.assert_plugins({'HttpProtocolHandlerPlugin': [ HttpProxyPlugin, @@ -94,7 +96,7 @@ def test_unique_plugin_from_bytes(self) -> None: def test_unique_plugin_from_args(self) -> None: self.flags = Proxy.initialize([ - '--plugins', 'proxy.http.proxy.HttpProxyPlugin', + '--plugins', PLUGIN_HTTP_PROXY, ]) self.assert_plugins({'HttpProtocolHandlerPlugin': [ HttpProxyPlugin, diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index a96d43f37..a79e6b4ad 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -18,7 +18,7 @@ from proxy.proxy import Proxy from proxy.common.version import __version__ from proxy.common.utils import bytes_ -from proxy.common.constants import CRLF +from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PROXY_AUTH, PLUGIN_WEB_SERVER from proxy.core.connection import TcpClientConnection from proxy.http.parser import HttpParser from proxy.http.proxy import HttpProxyPlugin @@ -41,8 +41,8 @@ def setUp(self, self.http_server_port = 65535 self.flags = Proxy.initialize() self.flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), ]) self.mock_selector = mock_selector @@ -175,8 +175,9 @@ def test_proxy_authentication_failed( flags = Proxy.initialize( auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + bytes_(PLUGIN_PROXY_AUTH), ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -208,8 +209,8 @@ def test_authenticated_proxy_http_get( flags = Proxy.initialize( auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), ]) self.protocol_handler = HttpProtocolHandler( @@ -257,8 +258,8 @@ def test_authenticated_proxy_http_tunnel( flags = Proxy.initialize( auth_code=base64.b64encode(b'user:pass')) flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin' + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER) ]) self.protocol_handler = HttpProtocolHandler( diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 43ff86c32..cdb359263 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -20,7 +20,7 @@ from proxy.http.handler import HttpProtocolHandler from proxy.http.parser import httpParserStates from proxy.common.utils import build_http_response, build_http_request, bytes_, text_ -from proxy.common.constants import CRLF, PROXY_PY_DIR +from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PAC_FILE, PLUGIN_WEB_SERVER, PROXY_PY_DIR from proxy.http.server import HttpWebServerPlugin @@ -35,8 +35,8 @@ def setUp(self, mock_fromfd: mock.Mock, mock_selector: mock.Mock) -> None: self.mock_selector = mock_selector self.flags = Proxy.initialize() self.flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -98,8 +98,8 @@ def test_default_web_server_returns_404( data=None), selectors.EVENT_READ), ] flags = Proxy.initialize() flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -151,8 +151,8 @@ def test_static_web_server_serves( enable_static_server=True, static_server_dir=static_server_dir) flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), ]) self.protocol_handler = HttpProtocolHandler( @@ -201,8 +201,8 @@ def test_static_web_server_serves_404( flags = Proxy.initialize(enable_static_server=True) flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), ]) self.protocol_handler = HttpProtocolHandler( @@ -239,9 +239,9 @@ def test_on_client_connection_called_on_teardown( def init_and_make_pac_file_request(self, pac_file: str) -> None: flags = Proxy.initialize(pac_file=pac_file) flags.plugins = Proxy.load_plugins([ - b'proxy.http.proxy.HttpProxyPlugin', - b'proxy.http.server.HttpWebServerPlugin', - b'proxy.http.server.HttpWebServerPacFilePlugin', + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + bytes_(PLUGIN_PAC_FILE), ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), From 12b9184e81ba1bae78b4c55e275387b08bcea362 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Thu, 15 Oct 2020 10:30:04 +0530 Subject: [PATCH 57/79] Create codeql-analysis.yml (#454) --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..2dd16165d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [develop, master] + pull_request: + # The branches below must be a subset of the branches above + branches: [develop] + schedule: + - cron: '0 14 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['python', 'javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 0c0f2221be2cb07863dadb2df398fdb7af1b22fb Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Thu, 15 Oct 2020 10:54:26 +0530 Subject: [PATCH 58/79] Create SECURITY.md (#455) --- SECURITY.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..606fe61b6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.x | :white_check_mark: | +| < 2.x | :x: | + +## Reporting a Vulnerability + +Follow these steps: + +1. Start by [emailing developers](mailto:mailsforabhinav+proxy@gmail.com) +2. If unresponsive, [create a public issue](https://github.com/abhinavsingh/proxy.py/issues/new/choose) +3. [Pull requests](https://github.com/abhinavsingh/proxy.py/pulls) are always welcome From f04845cd645e642b92a40ea5650fd805f4f9ad04 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Thu, 15 Oct 2020 19:05:37 +0530 Subject: [PATCH 59/79] Refactor (#456) --- proxy/http/proxy/auth.py | 5 +- proxy/http/proxy/server.py | 108 +++++++++++++++++++++---------------- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/proxy/http/proxy/auth.py b/proxy/http/proxy/auth.py index caa7cf4ee..263ec9bd0 100644 --- a/proxy/http/proxy/auth.py +++ b/proxy/http/proxy/auth.py @@ -34,8 +34,9 @@ def before_upstream_connection( if b'proxy-authorization' not in request.headers: raise ProxyAuthenticationFailed() parts = request.headers[b'proxy-authorization'][1].split() - if len(parts) != 2 and parts[0].lower( - ) != b'basic' and parts[1] != self.flags.auth_code: + if len(parts) != 2 \ + and parts[0].lower() != b'basic' \ + and parts[1] != self.flags.auth_code: raise ProxyAuthenticationFailed() return request diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index f60ff5436..01c5e0644 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -26,8 +26,9 @@ from ..methods import httpMethods from ...common.types import Readables, Writables -from ...common.constants import COMMA, DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_FILE, DEFAULT_SERVER_RECVBUF_SIZE -from ...common.constants import DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE, DEFAULT_CERT_FILE +from ...common.constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_FILE +from ...common.constants import DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE +from ...common.constants import COMMA, DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CERT_FILE from ...common.constants import PROXY_AGENT_HEADER_VALUE, DEFAULT_DISABLE_HEADERS from ...common.utils import build_http_response, text_ from ...common.pki import gen_public_key, gen_csr, sign_csr @@ -171,8 +172,10 @@ def write_to_descriptors(self, w: Writables) -> bool: return False def read_from_descriptors(self, r: Readables) -> bool: - if self.request.has_upstream_server( - ) and self.server and not self.server.closed and self.server.connection in r: + if self.request.has_upstream_server() \ + and self.server \ + and not self.server.closed \ + and self.server.connection in r: logger.debug('Server is ready for reads, reading...') try: raw = self.server.recv(self.flags.server_recvbuf_size) @@ -345,31 +348,8 @@ def on_request_complete(self) -> Union[socket.socket, bool]: if self.request.method == httpMethods.CONNECT: self.client.queue( HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) - # If interception is enabled if self.tls_interception_enabled(): - # Perform SSL/TLS handshake with upstream - self.wrap_server() - # Generate certificate and perform handshake with client - try: - # 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') - return True - except OSError as e: - logger.exception( - 'OSError when wrapping client', exc_info=e) - return True - # Update all plugin connection reference - for plugin in self.plugins.values(): - plugin.client._conn = self.client.connection - return self.client.connection + return self.intercept() elif self.server: # - proxy-connection header is a mistake, it doesn't seem to be # officially documented in any specification, drop it. @@ -427,6 +407,30 @@ def access_log(self) -> None: self.response.total_size, connection_time_ms)) + def connect_upstream(self) -> None: + host, port = self.request.host, self.request.port + if host and port: + self.server = TcpServerConnection(text_(host), port) + try: + logger.debug( + 'Connecting to upstream %s:%s' % + (text_(host), port)) + self.server.connect() + self.server.connection.setblocking(False) + logger.debug( + 'Connected to upstream %s:%s' % + (text_(host), port)) + except Exception as e: # TimeoutError, socket.gaierror + self.server.closed = True + raise ProxyConnectionFailed(text_(host), port, repr(e)) from e + else: + logger.exception('Both host and port must exist') + raise HttpProtocolException() + + # + # Interceptor related methods + # + def gen_ca_signed_certificate( self, cert_file_path: str, certificate: Dict[str, Any]) -> None: '''CA signing key (default) is used for generating a public key @@ -515,6 +519,32 @@ def generate_upstream_certificate( self.gen_ca_signed_certificate(cert_file_path, certificate) return cert_file_path + def intercept(self) -> Union[socket.socket, bool]: + # Perform SSL/TLS handshake with upstream + self.wrap_server() + # Generate certificate and perform handshake with client + try: + # 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') + return True + except OSError as e: + logger.exception( + 'OSError when wrapping client', exc_info=e) + return True + # Update all plugin connection reference + # TODO(abhinavsingh): Is this required? + for plugin in self.plugins.values(): + plugin.client._conn = self.client.connection + return self.client.connection + def wrap_server(self) -> None: assert self.server is not None assert isinstance(self.server.connection, socket.socket) @@ -530,25 +560,9 @@ def wrap_client(self) -> None: logger.debug( 'TLS interception using %s', generated_cert) - def connect_upstream(self) -> None: - host, port = self.request.host, self.request.port - if host and port: - self.server = TcpServerConnection(text_(host), port) - try: - logger.debug( - 'Connecting to upstream %s:%s' % - (text_(host), port)) - self.server.connect() - self.server.connection.setblocking(False) - logger.debug( - 'Connected to upstream %s:%s' % - (text_(host), port)) - except Exception as e: # TimeoutError, socket.gaierror - self.server.closed = True - raise ProxyConnectionFailed(text_(host), port, repr(e)) from e - else: - logger.exception('Both host and port must exist') - raise HttpProtocolException() + # + # Event emitter callbacks + # def emit_request_complete(self) -> None: if not self.flags.enable_events: From 4ba9e5836dce84d630d019a4da1e84e6b449466a Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Thu, 29 Oct 2020 13:29:42 +0200 Subject: [PATCH 60/79] Update pytest from 6.1.1 to 6.1.2 (#457) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 93a89606a..740da8910 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.3 flake8==3.8.4 -pytest==6.1.1 +pytest==6.1.2 pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.790 From da23ae03bcaad51f35f92812e81b9f7044907ec8 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 15 Nov 2020 21:48:28 +0530 Subject: [PATCH 61/79] npm update (#460) --- dashboard/package-lock.json | 1282 ++++------------------------------- dashboard/package.json | 14 +- 2 files changed, 134 insertions(+), 1162 deletions(-) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index bde5bf1e5..84c042cb3 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -30,16 +30,6 @@ "js-tokens": "^4.0.0" } }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -58,12 +48,6 @@ } } }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, "@nodelib/fs.walk": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", @@ -112,15 +96,15 @@ } }, "@types/jasmine": { - "version": "3.5.12", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.12.tgz", - "integrity": "sha512-vJaQ58oceFao+NzpKNqLOWwHPsqA7YEhKv+mOXvYU4/qh+BfVWIxaBtL0Ck5iCS67yOkNwGkDCrzepnzIWF+7g==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.1.tgz", + "integrity": "sha512-eeSCVhBsgwHNS1FmaMu4zrLxfykCTWJMLFZv7lmyrZQjw7foUUXoPu4GukSN9v7JvUw7X+/aDH3kCaymirBSTg==", "dev": true }, "@types/jquery": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz", - "integrity": "sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.4.tgz", + "integrity": "sha512-//9CHhaUt/rurMJTxGI+I6DmsNHgYU6d8aSLFfO5dB7+10lwLnaWT0z5GY/yY82Q/M+B+0Qh3TixlJ8vmBeqIw==", "dev": true, "requires": { "@types/sizzle": "*" @@ -338,24 +322,6 @@ "sprintf-js": "~1.0.2" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, "array-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", @@ -379,12 +345,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, "array.prototype.flat": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", @@ -416,12 +376,6 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -443,12 +397,6 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -504,61 +452,6 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "basic-auth": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", @@ -584,35 +477,6 @@ "concat-map": "0.0.1" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -625,29 +489,16 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", "dev": true, "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" } }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -690,34 +541,11 @@ "dev": true }, "chrome-devtools-frontend": { - "version": "1.0.792622", - "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.792622.tgz", - "integrity": "sha512-ulWZkHn4aEJpnQU982/QMt/kNlNSQZwMJw1NHnEcPhFVO2EmcQ26Pqev5fewj0EwJ1dBo9+Rcz49KPuTiRgL2A==", + "version": "1.0.827632", + "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.827632.tgz", + "integrity": "sha512-8qtv6zxlaoZhmtPFrR/dAyXjCKCSXL8s7SeAaHyICpd7p6mfXS+y+b14E66LLbOIEMwpidmy3xUJPePx8NuWHg==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -763,16 +591,6 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -809,12 +627,6 @@ "delayed-stream": "~1.0.0" } }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -827,12 +639,6 @@ "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", "dev": true }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, "core-js": { "version": "2.6.10", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz", @@ -926,12 +732,6 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -947,47 +747,6 @@ "object-keys": "^1.0.12" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1082,20 +841,20 @@ } }, "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.0", + "object.assign": "^4.1.1", "string.prototype.trimend": "^1.0.1", "string.prototype.trimstart": "^1.0.1" } @@ -1505,11 +1264,12 @@ "dev": true }, "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "dev": true, "requires": { + "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } } @@ -1561,9 +1321,9 @@ } }, "eslint-plugin-import": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", - "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", "dev": true, "requires": { "array-includes": "^3.1.1", @@ -1571,7 +1331,7 @@ "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.3", + "eslint-import-resolver-node": "^0.3.4", "eslint-module-utils": "^2.6.0", "has": "^1.0.3", "minimatch": "^3.0.4", @@ -1607,11 +1367,12 @@ "dev": true }, "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "dev": true, "requires": { + "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } } @@ -1646,9 +1407,9 @@ "dev": true }, "eslint-plugin-standard": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", - "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz", + "integrity": "sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==", "dev": true }, "eslint-scope": { @@ -1758,83 +1519,12 @@ "strip-eof": "^1.0.0" } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, "external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", @@ -1846,71 +1536,6 @@ "tmp": "^0.0.33" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1923,43 +1548,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "dependencies": { - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - } - } - }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1999,29 +1587,6 @@ "flat-cache": "^2.0.1" } }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -2057,12 +1622,6 @@ "debug": "^3.0.0" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2080,15 +1639,6 @@ "mime-types": "^2.1.12" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2124,18 +1674,23 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2168,12 +1723,6 @@ "is-glob": "^4.0.1" } }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -2322,38 +1871,6 @@ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2522,26 +2039,6 @@ "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", "dev": true }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2555,29 +2052,18 @@ "dev": true }, "is-callable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", "dev": true, "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "has": "^1.0.3" } }, "is-date-object": { @@ -2586,31 +2072,6 @@ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2635,34 +2096,11 @@ "is-extglob": "^2.1.1" } }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true }, "is-promise": { "version": "2.1.0", @@ -2706,12 +2144,6 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -2724,12 +2156,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -2737,12 +2163,12 @@ "dev": true }, "jasmine": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.1.tgz", - "integrity": "sha512-Jqp8P6ZWkTVFGmJwBK46p+kJNrZCdqkQ4GL+PGuBXZwK1fM4ST9BizkYgIwCFqYYqnTizAy6+XG2Ej5dFrej9Q==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.3.tgz", + "integrity": "sha512-Th91zHsbsALWjDUIiU5d/W5zaYQsZFMPTdeNmi8GivZPmAaUAK8MblSG3yQI4VMGC/abF2us7ex60NH1AAIMTA==", "dev": true, "requires": { - "fast-glob": "^2.2.6", + "glob": "^7.1.6", "jasmine-core": "~3.6.0" } }, @@ -3156,12 +2582,6 @@ "verror": "1.10.0" } }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -3231,21 +2651,6 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, "md5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", @@ -3272,27 +2677,6 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3335,27 +2719,6 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -3377,25 +2740,6 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3469,37 +2813,6 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", @@ -3512,34 +2825,16 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { - "isobject": "^3.0.1" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.values": { @@ -3720,18 +3015,6 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -3824,12 +3107,6 @@ "mkdirp": "^0.5.1" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -3887,34 +3164,12 @@ "read-pkg": "^2.0.0" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -4026,12 +3281,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -4042,12 +3291,6 @@ "signal-exit": "^3.0.2" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -4159,15 +3402,6 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4201,29 +3435,6 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -4270,153 +3481,12 @@ } } }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -4427,12 +3497,6 @@ "source-map": "^0.6.0" } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -4465,15 +3529,6 @@ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4497,27 +3552,6 @@ "tweetnacl": "~0.14.0" } }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -4564,23 +3598,67 @@ } }, "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "strip-ansi": { @@ -4698,48 +3776,6 @@ "os-tmpdir": "~1.0.2" } }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -4848,64 +3884,12 @@ "qs": "^6.4.0" } }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -4915,24 +3899,12 @@ "punycode": "^2.1.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, "url-join": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", "dev": true }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -5084,9 +4056,9 @@ } }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", "dev": true }, "xml-name-validator": { diff --git a/dashboard/package.json b/dashboard/package.json index c3ee183c5..236e2d157 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -25,20 +25,20 @@ }, "homepage": "https://github.com/abhinavsingh/proxy.py#readme", "devDependencies": { - "@types/jasmine": "^3.5.12", - "@types/jquery": "^3.5.1", + "@types/jasmine": "^3.6.1", + "@types/jquery": "^3.5.4", "@types/js-cookie": "^2.2.6", "@typescript-eslint/eslint-plugin": "^2.34.0", "@typescript-eslint/parser": "^2.34.0", - "chrome-devtools-frontend": "^1.0.792622", + "chrome-devtools-frontend": "^1.0.827632", "eslint": "^6.8.0", "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.22.0", + "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^10.0.0", "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", + "eslint-plugin-standard": "^4.1.0", "http-server": "^0.12.3", - "jasmine": "^3.6.1", + "jasmine": "^3.6.3", "jasmine-ts": "^0.3.0", "jquery": "^3.5.1", "js-cookie": "^2.2.1", @@ -50,6 +50,6 @@ "rollup-plugin-typescript": "^1.0.1", "ts-node": "^7.0.1", "typescript": "^3.9.7", - "ws": "^7.3.1" + "ws": "^7.4.0" } } From 4520ae31a6119ab1ee433e27c9516159581d45da Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Tue, 24 Nov 2020 21:37:11 +0530 Subject: [PATCH 62/79] Refactor base server interfaces into core modules (#461) * Ensure pending buffers are flushed before shutting down in base_server.py Handle unsupported scheme cases within connect_tunnel.py * Move base implementations within core module * Update ssl_echo_server --- examples/README.md | 8 +- examples/base_server.py | 75 ------------- examples/https_connect_tunnel.py | 85 ++++++++++++++ examples/ssl_echo_server.py | 14 ++- examples/tcp_echo_server.py | 11 +- proxy/core/base/__init__.py | 17 +++ proxy/core/base/tcp_server.py | 106 ++++++++++++++++++ .../core/base/tcp_tunnel.py | 72 ++---------- 8 files changed, 238 insertions(+), 150 deletions(-) delete mode 100644 examples/base_server.py create mode 100644 examples/https_connect_tunnel.py create mode 100644 proxy/core/base/__init__.py create mode 100644 proxy/core/base/tcp_server.py rename examples/connect_tunnel.py => proxy/core/base/tcp_tunnel.py (58%) diff --git a/examples/README.md b/examples/README.md index 04b38a006..84bf0f445 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,7 +12,7 @@ Table of Contents * [SSL Echo Server](#ssl-echo-server) * [SSL Echo Client](#ssl-echo-client) * [PubSub Eventing](#pubsub-eventing) -* [Connect Tunnel](#connect-tunnel) +* [Https Connect Tunnel](#https-connect-tunnel) ## WebSocket Client @@ -117,7 +117,7 @@ DEBUG:proxy.core.event.subscriber:Un-subscribed relay sub id 5eb22010764f4d44900 Received 52724 events from main thread, 60172 events from another process, in 21.50117802619934 seconds ``` -## Connect Tunnel +## HTTPS Connect Tunnel A simple HTTP proxy server supporting only CONNECT (https) requests. @@ -125,10 +125,10 @@ A simple HTTP proxy server supporting only CONNECT (https) requests. 2. Uses `TcpServerConnection` to establish upstream connection. 3. Overrides `BaseServer` methods to also register read/write events for upstream connection. -Start `connect_tunnel.py` as: +Start `https_connect_tunnel.py` as: ``` -❯ PYTHONPATH=. python examples/connect_tunnel.py +❯ PYTHONPATH=. python examples/https_connect_tunnel.py ``` Send https requests via tunnel as: diff --git a/examples/base_server.py b/examples/base_server.py deleted file mode 100644 index 19b3c7b6a..000000000 --- a/examples/base_server.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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. -""" -from abc import abstractmethod -import socket -import selectors - -from typing import Dict, Any - -from proxy.core.acceptor import Work -from proxy.common.types import Readables, Writables - - -class BaseServerHandler(Work): - """BaseServerHandler implements Work interface. - - An instance of BaseServerHandler is created for each client - connection. BaseServerHandler lifecycle is controlled by - Threadless core using asyncio. - - Implementation must provide: - a) handle_data(data: memoryview) - c) (optionally) intialize, is_inactive and shutdown methods - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - print('Connection accepted from {0}'.format(self.client.addr)) - - @abstractmethod - def handle_data(self, data: memoryview) -> None: - pass # pragma: no cover - - def get_events(self) -> Dict[socket.socket, int]: - # We always want to read from client - # Register for EVENT_READ events - events = {self.client.connection: selectors.EVENT_READ} - # If there is pending buffer for client - # also register for EVENT_WRITE events - if self.client.has_buffer(): - events[self.client.connection] |= selectors.EVENT_WRITE - return events - - def handle_events( - self, - readables: Readables, - writables: Writables) -> bool: - """Return True to shutdown work.""" - if self.client.connection in readables: - try: - data = self.client.recv() - if data is None: - # Client closed connection, signal shutdown - print( - 'Connection closed by client {0}'.format( - self.client.addr)) - return True - self.handle_data(data) - except ConnectionResetError: - print( - 'Connection reset by client {0}'.format( - self.client.addr)) - return True - - if self.client.connection in writables: - self.client.flush() - - return False diff --git a/examples/https_connect_tunnel.py b/examples/https_connect_tunnel.py new file mode 100644 index 000000000..950186468 --- /dev/null +++ b/examples/https_connect_tunnel.py @@ -0,0 +1,85 @@ +# -*- 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 time +from typing import Any, Optional + +from proxy.proxy import Proxy +from proxy.common.utils import build_http_response +from proxy.http.codes import httpStatusCodes +from proxy.http.parser import httpParserStates +from proxy.http.methods import httpMethods +from proxy.core.acceptor import AcceptorPool +from proxy.core.base import BaseTcpTunnelHandler + + +class HttpsConnectTunnelHandler(BaseTcpTunnelHandler): + """A https CONNECT tunnel.""" + + PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(build_http_response( + httpStatusCodes.OK, + reason=b'Connection established' + )) + + PROXY_TUNNEL_UNSUPPORTED_SCHEME = memoryview(build_http_response( + httpStatusCodes.BAD_REQUEST, + headers={b'Connection': b'close'}, + reason=b'Unsupported protocol scheme' + )) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + def handle_data(self, data: memoryview) -> Optional[bool]: + # Queue for upstream if connection has been established + if self.upstream and self.upstream._conn is not None: + self.upstream.queue(data) + return None + + # Parse client request + self.request.parse(data) + + # Drop the request if not a CONNECT request + if self.request.method != httpMethods.CONNECT: + self.client.queue( + HttpsConnectTunnelHandler.PROXY_TUNNEL_UNSUPPORTED_SCHEME) + return True + + # CONNECT requests are short and we need not worry about + # receiving partial request bodies here. + assert self.request.state == httpParserStates.COMPLETE + + # Establish connection with upstream + self.connect_upstream() + + # Queue tunnel established response to client + self.client.queue( + HttpsConnectTunnelHandler.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) + + return None + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Proxy.initialize(port=12345, num_workers=1, threadless=True), + work_klass=HttpsConnectTunnelHandler) + try: + pool.setup() + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py index dfd26dcfb..013bc3a5f 100644 --- a/examples/ssl_echo_server.py +++ b/examples/ssl_echo_server.py @@ -9,16 +9,17 @@ :license: BSD, see LICENSE for more details. """ import time +from typing import Optional from proxy.proxy import Proxy +from proxy.common.utils import wrap_socket from proxy.core.acceptor import AcceptorPool from proxy.core.connection import TcpClientConnection -from proxy.common.utils import wrap_socket -from examples.base_server import BaseServerHandler +from proxy.core.base import BaseTcpServerHandler -class EchoSSLServerHandler(BaseServerHandler): # type: ignore +class EchoSSLServerHandler(BaseTcpServerHandler): """Wraps client socket during initialization.""" def initialize(self) -> None: @@ -26,17 +27,18 @@ def initialize(self) -> None: # here using wrap_socket() utility. assert self.flags.keyfile is not None and self.flags.certfile is not None conn = wrap_socket( - self.client.connection, # type: ignore + self.client.connection, self.flags.keyfile, self.flags.certfile) conn.setblocking(False) # Upgrade plain TcpClientConnection to SSL connection object self.client = TcpClientConnection( - conn=conn, addr=self.client.addr) # type: ignore + conn=conn, addr=self.client.addr) - def handle_data(self, data: memoryview) -> None: + def handle_data(self, data: memoryview) -> Optional[bool]: # echo back to client self.client.queue(data) + return None def main() -> None: diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py index bb904323e..c468b7eaf 100644 --- a/examples/tcp_echo_server.py +++ b/examples/tcp_echo_server.py @@ -9,22 +9,23 @@ :license: BSD, see LICENSE for more details. """ import time +from typing import Optional -from proxy.core.acceptor import AcceptorPool from proxy.proxy import Proxy - -from examples.base_server import BaseServerHandler +from proxy.core.acceptor import AcceptorPool +from proxy.core.base import BaseTcpServerHandler -class EchoServerHandler(BaseServerHandler): # type: ignore +class EchoServerHandler(BaseTcpServerHandler): """Sets client socket to non-blocking during initialization.""" def initialize(self) -> None: self.client.connection.setblocking(False) - def handle_data(self, data: memoryview) -> None: + def handle_data(self, data: memoryview) -> Optional[bool]: # echo back to client self.client.queue(data) + return None def main() -> None: diff --git a/proxy/core/base/__init__.py b/proxy/core/base/__init__.py new file mode 100644 index 000000000..c60f14778 --- /dev/null +++ b/proxy/core/base/__init__.py @@ -0,0 +1,17 @@ +# -*- 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. +""" +from .tcp_server import BaseTcpServerHandler +from .tcp_tunnel import BaseTcpTunnelHandler + +__all__ = [ + 'BaseTcpServerHandler', + 'BaseTcpTunnelHandler', +] diff --git a/proxy/core/base/tcp_server.py b/proxy/core/base/tcp_server.py new file mode 100644 index 000000000..bff043113 --- /dev/null +++ b/proxy/core/base/tcp_server.py @@ -0,0 +1,106 @@ +# -*- 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. +""" +from abc import abstractmethod +import socket +import selectors + +from typing import Dict, Any, Optional + +from proxy.core.acceptor import Work +from proxy.common.types import Readables, Writables + + +class BaseTcpServerHandler(Work): + """BaseTcpServerHandler implements Work interface. + + An instance of BaseTcpServerHandler is created for each client + connection. BaseServerHandler lifecycle is controlled by + Threadless core using asyncio. + + BaseServerHandler ensures that pending buffers are flushed + before client connection is closed. + + Implementations must provide: + a) handle_data(data: memoryview) + c) (optionally) intialize, is_inactive and shutdown methods + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.must_flush_before_shutdown = False + print('Connection accepted from {0}'.format(self.client.addr)) + + @abstractmethod + def handle_data(self, data: memoryview) -> Optional[bool]: + """Optionally return True to close client connection.""" + pass # pragma: no cover + + def get_events(self) -> Dict[socket.socket, int]: + events = {} + # We always want to read from client + # Register for EVENT_READ events + if self.must_flush_before_shutdown is False: + events[self.client.connection] = selectors.EVENT_READ + # If there is pending buffer for client + # also register for EVENT_WRITE events + if self.client.has_buffer(): + if self.client.connection in events: + events[self.client.connection] |= selectors.EVENT_WRITE + else: + events[self.client.connection] = selectors.EVENT_WRITE + return events + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Return True to shutdown work.""" + do_shutdown = False + if self.client.connection in readables: + try: + data = self.client.recv() + if data is None: + # Client closed connection, signal shutdown + print( + 'Connection closed by client {0}'.format( + self.client.addr)) + do_shutdown = True + else: + r = self.handle_data(data) + if isinstance(r, bool) and r is True: + print( + 'Implementation signaled shutdown for client {0}'.format( + self.client.addr)) + if self.client.has_buffer(): + print( + 'Client {0} has pending buffer, will be flushed before shutting down'.format( + self.client.addr)) + self.must_flush_before_shutdown = True + else: + do_shutdown = True + except ConnectionResetError: + print( + 'Connection reset by client {0}'.format( + self.client.addr)) + do_shutdown = True + + if self.client.connection in writables: + print('Flushing buffer to client {0}'.format(self.client.addr)) + self.client.flush() + if self.must_flush_before_shutdown is True: + do_shutdown = True + self.must_flush_before_shutdown = False + + if do_shutdown: + print( + 'Shutting down client {0} connection'.format( + self.client.addr)) + return do_shutdown diff --git a/examples/connect_tunnel.py b/proxy/core/base/tcp_tunnel.py similarity index 58% rename from examples/connect_tunnel.py rename to proxy/core/base/tcp_tunnel.py index ad230ece4..d83053ae1 100644 --- a/examples/connect_tunnel.py +++ b/proxy/core/base/tcp_tunnel.py @@ -8,36 +8,31 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -import time +from abc import abstractmethod import socket import selectors from typing import Any, Optional, Dict -from proxy.proxy import Proxy -from proxy.core.acceptor import AcceptorPool -from proxy.core.connection import TcpServerConnection -from proxy.http.parser import HttpParser, httpParserTypes, httpParserStates -from proxy.http.codes import httpStatusCodes -from proxy.http.methods import httpMethods -from proxy.common.types import Readables, Writables -from proxy.common.utils import build_http_response, text_ +from ...http.parser import HttpParser, httpParserTypes +from ...common.types import Readables, Writables +from ...common.utils import text_ -from examples.base_server import BaseServerHandler +from ..connection import TcpServerConnection +from .tcp_server import BaseTcpServerHandler -class ConnectTunnelHandler(BaseServerHandler): # type: ignore - """A http CONNECT tunnel server.""" - - PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(build_http_response( - httpStatusCodes.OK, - reason=b'Connection established' - )) +class BaseTcpTunnelHandler(BaseTcpServerHandler): + """Base TCP tunnel interface.""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.request = HttpParser(httpParserTypes.REQUEST_PARSER) self.upstream: Optional[TcpServerConnection] = None + @abstractmethod + def handle_data(self, data: memoryview) -> Optional[bool]: + pass # pragma: no cover + def initialize(self) -> None: self.client.connection.setblocking(False) @@ -48,30 +43,6 @@ def shutdown(self) -> None: self.upstream.close() super().shutdown() - def handle_data(self, data: memoryview) -> None: - # Queue for upstream if connection has been established - if self.upstream and self.upstream._conn is not None: - self.upstream.queue(data) - return - - # Parse client request - self.request.parse(data) - - # Drop the request if not a CONNECT request - if self.request.method != httpMethods.CONNECT: - pass - - # CONNECT requests are short and we need not worry about - # receiving partial request bodies here. - assert self.request.state == httpParserStates.COMPLETE - - # Establish connection with upstream - self.connect_upstream() - - # Queue tunnel established response to client - self.client.queue( - ConnectTunnelHandler.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) - def get_events(self) -> Dict[socket.socket, int]: # Get default client events ev: Dict[socket.socket, int] = super().get_events() @@ -115,22 +86,3 @@ def connect_upstream(self) -> None: self.upstream.connect() print('Connection established with upstream {0}:{1}'.format( text_(self.request.host), self.request.port)) - - -def main() -> None: - # This example requires `threadless=True` - pool = AcceptorPool( - flags=Proxy.initialize(port=12345, num_workers=1, threadless=True), - work_klass=ConnectTunnelHandler) - try: - pool.setup() - while True: - time.sleep(1) - except KeyboardInterrupt: - pass - finally: - pool.shutdown() - - -if __name__ == '__main__': - main() From d593576bc838fa5a2fc28cb54c90801c4a22a757 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 1 Dec 2020 15:58:39 +0200 Subject: [PATCH 63/79] Update wheel from 0.35.1 to 0.36.0 (#462) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index a3024372b..ed129cb11 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ twine==3.2.0 -wheel==0.35.1 +wheel==0.36.0 From e3068f6d0cd7d3d095cd6fdd51e69a58edb9be6d Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sat, 5 Dec 2020 09:59:22 +0200 Subject: [PATCH 64/79] Update wheel from 0.36.0 to 0.36.1 (#463) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index ed129cb11..7f179b4fe 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ twine==3.2.0 -wheel==0.36.0 +wheel==0.36.1 From cd5b6ffb11f26b7155e87d772731e74353ab2c5c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 13 Dec 2020 09:42:29 +0200 Subject: [PATCH 65/79] Update pytest from 6.1.2 to 6.2.0 (#465) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 740da8910..ce43b7113 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.3 flake8==3.8.4 -pytest==6.1.2 +pytest==6.2.0 pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.790 From 28fe4e7242ec851c503272e74dbf89f31426bbec Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 14 Dec 2020 12:06:58 +0200 Subject: [PATCH 66/79] Update wheel from 0.36.1 to 0.36.2 (#466) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index 7f179b4fe..0c899fc43 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ twine==3.2.0 -wheel==0.36.1 +wheel==0.36.2 From ed52a0783e52e1b93e06fe9c413036bff13faf84 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 16 Dec 2020 08:36:29 +0200 Subject: [PATCH 67/79] Update pytest from 6.2.0 to 6.2.1 (#467) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index ce43b7113..ef5da6804 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.3 flake8==3.8.4 -pytest==6.2.0 +pytest==6.2.1 pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.790 From 72eea06f2da4af265990f0177def33122efded81 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 18 Dec 2020 16:46:01 +0200 Subject: [PATCH 68/79] Update codecov from 2.1.10 to 2.1.11 (#469) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index ef5da6804..0d41fcee0 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -6,7 +6,7 @@ pytest-cov==2.10.1 autopep8==1.5.4 mypy==0.790 py-spy==0.3.3 -codecov==2.1.10 +codecov==2.1.11 tox==3.20.1 mccabe==0.6.1 pylint==2.6.0 From 6d3135cab6d2897a10299eb95746f3ff70821c38 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 20 Dec 2020 13:32:32 +0530 Subject: [PATCH 69/79] Add version check for README.md (#471) --- README.md | 2 +- version-check.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 70d9b7d23..2fad226b9 100644 --- a/README.md +++ b/README.md @@ -1703,7 +1703,7 @@ usage: proxy [-h] [--backlog BACKLOG] [--basic-auth BASIC_AUTH] [--static-server-dir STATIC_SERVER_DIR] [--threadless] [--timeout TIMEOUT] [--version] -proxy.py v2.2.0 +proxy.py v2.3.0 optional arguments: -h, --help show this help message and exit diff --git a/version-check.py b/version-check.py index b46222500..e783a2a36 100644 --- a/version-check.py +++ b/version-check.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ proxy.py ~~~~~~~~ @@ -8,18 +9,30 @@ :license: BSD, see LICENSE for more details. """ import sys +import subprocess from proxy.common.version import __version__ as lib_version from setup import __version__ as pkg_version # This script ensures our versions never run out of sync. # -# 1. setup.py doesn't import proxy and hence they both use -# their own respective __version__ -# 2. TODO: Version is hardcoded in homebrew stable package +# 1. TODO: Version is hardcoded in homebrew stable package # installer file, but it only needs to match with lib # versions if current git branch is master -# 3. TODO: Version is also hardcoded in README.md flags -# section + +# setup.py doesn't import proxy and hence they both use +# their own respective __version__ if lib_version != pkg_version: - print('Version mismatch found. {0} (lib) vs {1} (pkg).'.format(lib_version, pkg_version)) + print('Version mismatch found. {0} (lib) vs {1} (pkg).'.format( + lib_version, pkg_version)) + sys.exit(1) + +# Version is also hardcoded in README.md flags section +readme_version_cmd = 'cat README.md | grep "proxy.py v" | tail -2 | head -1 | cut -d " " -f 2 | cut -c2-' +readme_version_output = subprocess.check_output( + ['bash', '-c', readme_version_cmd]) +readme_version = readme_version_output.decode().strip() + +if readme_version != lib_version: + print('Version mismatch found. {0} (readme) vs {1} (lib).'.format( + readme_version, lib_version)) sys.exit(1) From 475536b079ab3618f0f748927903772d2390e31b Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 22 Dec 2020 13:47:26 +0200 Subject: [PATCH 70/79] Update coverage from 5.3 to 5.3.1 (#472) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 0d41fcee0..a9e882abf 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,5 +1,5 @@ python-coveralls==2.9.3 -coverage==5.3 +coverage==5.3.1 flake8==3.8.4 pytest==6.2.1 pytest-cov==2.10.1 From 0f78e74705e295bbfccfba342bf9fd34a9aa9103 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 25 Dec 2020 08:34:47 +0200 Subject: [PATCH 71/79] Update twine from 3.2.0 to 3.3.0 (#474) --- requirements-release.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-release.txt b/requirements-release.txt index 0c899fc43..43e86b68f 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ -twine==3.2.0 +twine==3.3.0 wheel==0.36.2 From bff171ec26d826ae1d22d2466eaf9d8bdbf059d3 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 10 Jan 2021 22:12:56 +0530 Subject: [PATCH 72/79] Fix basic auth condition (#482) * Fix basic auth condition * Prepare for v2.3.1 --- README.md | 678 +++++++++++++++++++-------------------- proxy/common/version.py | 2 +- proxy/http/proxy/auth.py | 4 +- setup.py | 2 +- 4 files changed, 337 insertions(+), 349 deletions(-) diff --git a/README.md b/README.md index 2fad226b9..256ae2dab 100644 --- a/README.md +++ b/README.md @@ -25,151 +25,151 @@ [![Become a Backer](https://opencollective.com/proxypy/tiers/backer.svg?avatarHeight=72)](https://opencollective.com/proxypy) -Table of Contents -================= - -* [Features](#features) -* [Install](#install) - * [Using PIP](#using-pip) - * [Stable version](#stable-version-with-pip) - * [Development version](#development-version-with-pip) - * [Using Docker](#using-docker) - * [Stable version](#stable-version-from-docker-hub) - * [Development version](#build-development-version-locally) - * [Using HomeBrew](#using-homebrew) - * [Stable version](#stable-version-with-homebrew) - * [Development version](#development-version-with-homebrew) -* [Start proxy.py](#start-proxypy) - * [From command line when installed using PIP](#from-command-line-when-installed-using-pip) - * [Run it](#run-it) - * [Understanding logs](#understanding-logs) - * [Enable DEBUG logging](#enable-debug-logging) - * [From command line using repo source](#from-command-line-using-repo-source) - * [Docker Image](#docker-image) - * [Customize Startup Flags](#customize-startup-flags) -* [Plugin Examples](#plugin-examples) - * [HTTP Proxy Plugins](#http-proxy-plugins) - * [ShortLink Plugin](#shortlinkplugin) - * [Modify Post Data Plugin](#modifypostdataplugin) - * [Mock Api Plugin](#mockrestapiplugin) - * [Redirect To Custom Server Plugin](#redirecttocustomserverplugin) - * [Filter By Upstream Host Plugin](#filterbyupstreamhostplugin) - * [Cache Responses Plugin](#cacheresponsesplugin) - * [Man-In-The-Middle Plugin](#maninthemiddleplugin) - * [Proxy Pool Plugin](#proxypoolplugin) - * [FilterByClientIpPlugin](#filterbyclientipplugin) - * [ModifyChunkResponsePlugin](#modifychunkresponseplugin) - * [HTTP Web Server Plugins](#http-web-server-plugins) - * [Reverse Proxy](#reverse-proxy) - * [Web Server Route](#web-server-route) - * [Plugin Ordering](#plugin-ordering) -* [End-to-End Encryption](#end-to-end-encryption) -* [TLS Interception](#tls-interception) - * [TLS Interception With Docker](#tls-interception-with-docker) -* [Proxy Over SSH Tunnel](#proxy-over-ssh-tunnel) - * [Proxy Remote Requests Locally](#proxy-remote-requests-locally) - * [Proxy Local Requests Remotely](#proxy-local-requests-remotely) -* [Embed proxy.py](#embed-proxypy) - * [Blocking Mode](#blocking-mode) - * [Non-blocking Mode](#non-blocking-mode) - * [Loading Plugins](#loading-plugins) -* [Unit testing with proxy.py](#unit-testing-with-proxypy) - * [proxy.TestCase](#proxytestcase) - * [Override Startup Flags](#override-startup-flags) - * [With unittest.TestCase](#with-unittesttestcase) -* [Plugin Developer and Contributor Guide](#plugin-developer-and-contributor-guide) - * [Everything is a plugin](#everything-is-a-plugin) - * [Internal Architecture](#internal-architecture) - * [Internal Documentation](#internal-documentation) - * [Development Guide](#development-guide) - * [Setup Local Environment](#setup-local-environment) - * [Setup pre-commit hook](#setup-pre-commit-hook) - * [Sending a Pull Request](#sending-a-pull-request) -* [Utilities](#utilities) - * [TCP](#tcp-sockets) - * [new_socket_connection](#new_socket_connection) - * [socket_connection](#socket_connection) - * [Http](#http-client) - * [build_http_request](#build_http_request) - * [build_http_response](#build_http_response) - * [Public Key Infrastructure](#pki) - * [API Usage](#api-usage) - * [CLI Usage](#cli-usage) -* [Frequently Asked Questions](#frequently-asked-questions) - * [Threads vs Threadless](#threads-vs-threadless) - * [SyntaxError: invalid syntax](#syntaxerror-invalid-syntax) - * [Unable to load plugins](#unable-to-load-plugins) - * [Unable to connect with proxy.py from remote host](#unable-to-connect-with-proxypy-from-remote-host) - * [Basic auth not working with a browser](#basic-auth-not-working-with-a-browser) - * [Docker image not working on MacOS](#docker-image-not-working-on-macos) - * [ValueError: filedescriptor out of range in select](#valueerror-filedescriptor-out-of-range-in-select) - * [None:None in access logs](#nonenone-in-access-logs) -* [Flags](#flags) -* [Changelog](#changelog) - * [v2.x](#v2x) - * [v1.x](#v1x) - * [v0.x](#v0x) - -Features -======== +# Table of Contents + +- [Features](#features) +- [Install](#install) + - [Using PIP](#using-pip) + - [Stable version](#stable-version-with-pip) + - [Development version](#development-version-with-pip) + - [Using Docker](#using-docker) + - [Stable version](#stable-version-from-docker-hub) + - [Development version](#build-development-version-locally) + - [Using HomeBrew](#using-homebrew) + - [Stable version](#stable-version-with-homebrew) + - [Development version](#development-version-with-homebrew) +- [Start proxy.py](#start-proxypy) + - [From command line when installed using PIP](#from-command-line-when-installed-using-pip) + - [Run it](#run-it) + - [Understanding logs](#understanding-logs) + - [Enable DEBUG logging](#enable-debug-logging) + - [From command line using repo source](#from-command-line-using-repo-source) + - [Docker Image](#docker-image) + - [Customize Startup Flags](#customize-startup-flags) +- [Plugin Examples](#plugin-examples) + - [HTTP Proxy Plugins](#http-proxy-plugins) + - [ShortLink Plugin](#shortlinkplugin) + - [Modify Post Data Plugin](#modifypostdataplugin) + - [Mock Api Plugin](#mockrestapiplugin) + - [Redirect To Custom Server Plugin](#redirecttocustomserverplugin) + - [Filter By Upstream Host Plugin](#filterbyupstreamhostplugin) + - [Cache Responses Plugin](#cacheresponsesplugin) + - [Man-In-The-Middle Plugin](#maninthemiddleplugin) + - [Proxy Pool Plugin](#proxypoolplugin) + - [FilterByClientIpPlugin](#filterbyclientipplugin) + - [ModifyChunkResponsePlugin](#modifychunkresponseplugin) + - [HTTP Web Server Plugins](#http-web-server-plugins) + - [Reverse Proxy](#reverse-proxy) + - [Web Server Route](#web-server-route) + - [Plugin Ordering](#plugin-ordering) +- [End-to-End Encryption](#end-to-end-encryption) +- [TLS Interception](#tls-interception) + - [TLS Interception With Docker](#tls-interception-with-docker) +- [Proxy Over SSH Tunnel](#proxy-over-ssh-tunnel) + - [Proxy Remote Requests Locally](#proxy-remote-requests-locally) + - [Proxy Local Requests Remotely](#proxy-local-requests-remotely) +- [Embed proxy.py](#embed-proxypy) + - [Blocking Mode](#blocking-mode) + - [Non-blocking Mode](#non-blocking-mode) + - [Loading Plugins](#loading-plugins) +- [Unit testing with proxy.py](#unit-testing-with-proxypy) + - [proxy.TestCase](#proxytestcase) + - [Override Startup Flags](#override-startup-flags) + - [With unittest.TestCase](#with-unittesttestcase) +- [Plugin Developer and Contributor Guide](#plugin-developer-and-contributor-guide) + - [Everything is a plugin](#everything-is-a-plugin) + - [Internal Architecture](#internal-architecture) + - [Internal Documentation](#internal-documentation) + - [Development Guide](#development-guide) + - [Setup Local Environment](#setup-local-environment) + - [Setup pre-commit hook](#setup-pre-commit-hook) + - [Sending a Pull Request](#sending-a-pull-request) +- [Utilities](#utilities) + - [TCP](#tcp-sockets) + - [new_socket_connection](#new_socket_connection) + - [socket_connection](#socket_connection) + - [Http](#http-client) + - [build_http_request](#build_http_request) + - [build_http_response](#build_http_response) + - [Public Key Infrastructure](#pki) + - [API Usage](#api-usage) + - [CLI Usage](#cli-usage) +- [Frequently Asked Questions](#frequently-asked-questions) + - [Threads vs Threadless](#threads-vs-threadless) + - [SyntaxError: invalid syntax](#syntaxerror-invalid-syntax) + - [Unable to load plugins](#unable-to-load-plugins) + - [Unable to connect with proxy.py from remote host](#unable-to-connect-with-proxypy-from-remote-host) + - [Basic auth not working with a browser](#basic-auth-not-working-with-a-browser) + - [Docker image not working on MacOS](#docker-image-not-working-on-macos) + - [ValueError: filedescriptor out of range in select](#valueerror-filedescriptor-out-of-range-in-select) + - [None:None in access logs](#nonenone-in-access-logs) +- [Flags](#flags) +- [Changelog](#changelog) + - [v2.x](#v2x) + - [v1.x](#v1x) + - [v0.x](#v0x) + +# Features - Fast & Scalable - - Scales by using all available cores on the system - - Threadless executions using coroutine - - Made to handle `tens-of-thousands` connections / sec - ```bash - # On Macbook Pro 2015 / 2.8 GHz Intel Core i7 - ❯ hey -n 10000 -c 100 http://localhost:8899/ - - Summary: - Total: 0.6157 secs - Slowest: 0.1049 secs - Fastest: 0.0007 secs - Average: 0.0055 secs - Requests/sec: 16240.5444 - - Total data: 800000 bytes - Size/request: 80 bytes - - Response time histogram: - 0.001 [1] | - 0.011 [9565] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ - 0.022 [332] |■ - ``` + + - Scales by using all available cores on the system + - Threadless executions using coroutine + - Made to handle `tens-of-thousands` connections / sec + + ```bash + # On Macbook Pro 2015 / 2.8 GHz Intel Core i7 + ❯ hey -n 10000 -c 100 http://localhost:8899/ + + Summary: + Total: 0.6157 secs + Slowest: 0.1049 secs + Fastest: 0.0007 secs + Average: 0.0055 secs + Requests/sec: 16240.5444 + + Total data: 800000 bytes + Size/request: 80 bytes + + Response time histogram: + 0.001 [1] | + 0.011 [9565] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ + 0.022 [332] |■ + ``` + - Lightweight - - Uses only `~5-20MB` RAM - - No external dependency other than standard Python library + - Uses only `~5-20MB` RAM + - No external dependency other than standard Python library - Programmable - - Optionally enable builtin Web Server - - Customize proxy and http routing via [plugins](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) - - Enable plugin using command line option e.g. `--plugins proxy.plugin.CacheResponsesPlugin` - - Plugin API is currently in development phase, expect breaking changes. + - Optionally enable builtin Web Server + - Customize proxy and http routing via [plugins](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) + - Enable plugin using command line option e.g. `--plugins proxy.plugin.CacheResponsesPlugin` + - Plugin API is currently in development phase, expect breaking changes. - Realtime Dashboard - - Optionally enable bundled dashboard. - - Available at `http://localhost:8899/dashboard`. - - Inspect, Monitor, Control and Configure `proxy.py` at runtime. - - Extend dashboard using plugins. - - Dashboard is currently in development phase, expect breaking changes. + - Optionally enable bundled dashboard. + - Available at `http://localhost:8899/dashboard`. + - Inspect, Monitor, Control and Configure `proxy.py` at runtime. + - Extend dashboard using plugins. + - Dashboard is currently in development phase, expect breaking changes. - Secure - - Enable end-to-end encryption between clients and `proxy.py` using TLS - - See [End-to-End Encryption](#end-to-end-encryption) + - Enable end-to-end encryption between clients and `proxy.py` using TLS + - See [End-to-End Encryption](#end-to-end-encryption) - Man-In-The-Middle - - Can decrypt TLS traffic between clients and upstream servers - - See [TLS Interception](#tls-interception) + - Can decrypt TLS traffic between clients and upstream servers + - See [TLS Interception](#tls-interception) - Supported proxy protocols - - `http(s)` - - `http1` - - `http1.1` pipeline - - `http2` - - `websockets` + - `http(s)` + - `http1` + - `http1.1` pipeline + - `http2` + - `websockets` - Optimized for large file uploads and downloads - IPv4 and IPv6 support - Basic authentication support - Can serve a [PAC (Proxy Auto-configuration)](https://en.wikipedia.org/wiki/Proxy_auto-config) file - - See `--pac-file` and `--pac-file-url-path` flags + - See `--pac-file` and `--pac-file-url-path` flags -Install -======= +# Install ## Using PIP @@ -227,8 +227,7 @@ or from GitHub `master` branch ❯ brew install https://raw.githubusercontent.com/abhinavsingh/proxy.py/develop/helper/homebrew/develop/proxy.rb ``` -Start proxy.py -============== +# Start proxy.py ## From command line when installed using PIP @@ -257,7 +256,7 @@ Things to notice from above logs: By default, `proxy.py` will start as many workers as there are CPU cores on the machine. - `Started server on ::1:8899` - By default, `proxy.py` listens on IPv6 `::1`, which - is equivalent of IPv4 `127.0.0.1`. If you want to access `proxy.py` externally, + is equivalent of IPv4 `127.0.0.1`. If you want to access `proxy.py` externally, use `--hostname ::` or `--hostname 0.0.0.0` or bind to any other interface available on your machine. @@ -294,36 +293,36 @@ To start `proxy.py` from source code follow these instructions: - Clone repo - ```bash - ❯ git clone https://github.com/abhinavsingh/proxy.py.git - ❯ cd proxy.py - ``` + ```bash + ❯ git clone https://github.com/abhinavsingh/proxy.py.git + ❯ cd proxy.py + ``` - Create a Python 3 virtual env - ```bash - ❯ python3 -m venv venv - ❯ source venv/bin/activate - ``` + ```bash + ❯ python3 -m venv venv + ❯ source venv/bin/activate + ``` - Install deps - ```bash - ❯ pip install -r requirements.txt - ❯ pip install -r requirements-testing.txt - ``` + ```bash + ❯ pip install -r requirements.txt + ❯ pip install -r requirements-testing.txt + ``` - Run tests - ```bash - ❯ make - ``` + ```bash + ❯ make + ``` - Run proxy.py - ```bash - ❯ python -m proxy - ``` + ```bash + ❯ python -m proxy + ``` Also see [Plugin Developer and Contributor Guide](#plugin-developer-and-contributor-guide) if you plan to work with `proxy.py` source code. @@ -344,15 +343,14 @@ For example, to check `proxy.py` version within Docker image: --rm abhinavsingh/proxy.py:latest \ -v -Plugin Examples -=============== +# Plugin Examples - See [plugin](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) module for full code. - All the bundled plugin examples also works with `https` traffic - - Require additional flags and certificate generation - - See [TLS Interception](#tls-interception). + - Require additional flags and certificate generation + - See [TLS Interception](#tls-interception). - Plugin examples are also bundled with Docker image. - - See [Customize startup flags](#customize-startup-flags) to try plugins with Docker image. + - See [Customize startup flags](#customize-startup-flags) to try plugins with Docker image. ## HTTP Proxy Plugins @@ -370,22 +368,22 @@ Start `proxy.py` as: ``` Now you can speed up your daily browsing experience by visiting your -favorite website using single character domain names :). This works +favorite website using single character domain names :). This works across all browsers. Following short links are enabled by default: -Short Link | Destination URL -:--------: | :---------------: -a/ | amazon.com -i/ | instagram.com -l/ | linkedin.com -f/ | facebook.com -g/ | google.com -t/ | twitter.com -w/ | web.whatsapp.com -y/ | youtube.com -proxy/ | localhost:8899 +| Short Link | Destination URL | +| :--------: | :--------------: | +| a/ | amazon.com | +| i/ | instagram.com | +| l/ | linkedin.com | +| f/ | facebook.com | +| g/ | google.com | +| t/ | twitter.com | +| w/ | web.whatsapp.com | +| y/ | youtube.com | +| proxy/ | localhost:8899 | ### ModifyPostDataPlugin @@ -464,7 +462,7 @@ Verify the same by inspecting `proxy.py` logs: 2019-09-27 12:44:02,212 - INFO - pid:7077 - access_log:1210 - ::1:64792 - GET None:None/v1/users/ - None None - 0 byte ``` -Access log shows `None:None` as server `ip:port`. `None` simply means that +Access log shows `None:None` as server `ip:port`. `None` simply means that the server connection was never made, since response was returned by our plugin. Now modify `ProposedRestApiPlugin` to returns REST API mock @@ -674,7 +672,7 @@ by checking respective logs. ### FilterByClientIpPlugin -Reject traffic from specific IP addresses. By default this +Reject traffic from specific IP addresses. By default this plugin blocks traffic from `127.0.0.1` and `::1`. Start `proxy.py` as: @@ -700,7 +698,7 @@ Modify plugin to your taste e.g. Allow specific IP addresses only. ### ModifyChunkResponsePlugin -This plugin demonstrate how to modify chunked encoded responses. In able to do so, this plugin uses `proxy.py` core to parse the chunked encoded response. Then we reconstruct the response using custom hardcoded chunks, ignoring original chunks received from upstream server. +This plugin demonstrate how to modify chunked encoded responses. In able to do so, this plugin uses `proxy.py` core to parse the chunked encoded response. Then we reconstruct the response using custom hardcoded chunks, ignoring original chunks received from upstream server. Start `proxy.py` as: @@ -795,8 +793,7 @@ If we enable `RedirectToCustomServerPlugin` before `FilterByUpstreamHostPlugin`, `google` requests will also get redirected to inbuilt web server, instead of being dropped. -End-to-End Encryption -===================== +# End-to-End Encryption By default, `proxy.py` uses `http` protocol for communication with clients e.g. `curl`, `browser`. For enabling end-to-end encrypting using `tls` / `https` first generate certificates: @@ -828,7 +825,7 @@ Verify using `curl -x https://localhost:8899 --proxy-cacert https-cert.pem https } ``` -If you want to avoid passing `--proxy-cacert` flag, also consider signing generated SSL certificates. Example: +If you want to avoid passing `--proxy-cacert` flag, also consider signing generated SSL certificates. Example: First, generate CA certificates: @@ -842,10 +839,9 @@ Then, sign SSL certificate: make sign-https-certificates ``` -Now restart the server with `--cert-file https-signed-cert.pem` flag. Note that you must also trust generated `ca-cert.pem` in your system keychain. +Now restart the server with `--cert-file https-signed-cert.pem` flag. Note that you must also trust generated `ca-cert.pem` in your system keychain. -TLS Interception -================= +# TLS Interception By default, `proxy.py` will not decrypt `https` traffic between client and server. To enable TLS interception first generate root CA certificates: @@ -855,7 +851,7 @@ To enable TLS interception first generate root CA certificates: ``` Lets also enable `CacheResponsePlugin` so that we can verify decrypted -response from the server. Start `proxy.py` as: +response from the server. Start `proxy.py` as: ```bash ❯ proxy \ @@ -865,10 +861,8 @@ response from the server. Start `proxy.py` as: --ca-signing-key-file ca-signing-key.pem ``` - [![NOTE](https://img.shields.io/static/v1?label=MacOS&message=note&color=yellow)](https://github.com/abhinavsingh/proxy.py#flags) Also provide explicit CA bundle path needed for validation of peer certificates. See `--ca-file` flag. - Verify TLS interception using `curl` ```bash @@ -896,7 +890,7 @@ Verify TLS interception using `curl` The `issuer` line confirms that response was intercepted. -Also verify the contents of cached response file. Get path to the cache +Also verify the contents of cached response file. Get path to the cache file from `proxy.py` logs. `❯ cat /path/to/your/tmp/directory/httpbin.org-1569452863.924174.txt` @@ -927,7 +921,7 @@ Connection: keep-alive } ``` -Viola!!! If you remove CA flags, encrypted data will be found in the +Viola!!! If you remove CA flags, encrypted data will be found in the cached file instead of plain text. Now use CA flags with other @@ -937,8 +931,8 @@ Now use CA flags with other Important notes about TLS Interception with Docker container: -- Since `v2.2.0`, `proxy.py` docker container also ships with `openssl`. This allows `proxy.py` -to generate certificates on the fly for TLS Interception. +- Since `v2.2.0`, `proxy.py` docker container also ships with `openssl`. This allows `proxy.py` + to generate certificates on the fly for TLS Interception. - For security reasons, `proxy.py` docker container doesn't ship with CA certificates. @@ -947,77 +941,76 @@ with TLS Interception: 1. Generate CA certificates on host computer - ```bash - ❯ make ca-certificates - ``` + ```bash + ❯ make ca-certificates + ``` -2. Copy all generated certificates into a separate directory. We'll later mount this directory into our docker container +2. Copy all generated certificates into a separate directory. We'll later mount this directory into our docker container - ```bash - ❯ mkdir /tmp/ca-certificates - ❯ cp ca-cert.pem ca-key.pem ca-signing-key.pem /tmp/ca-certificates - ``` + ```bash + ❯ mkdir /tmp/ca-certificates + ❯ cp ca-cert.pem ca-key.pem ca-signing-key.pem /tmp/ca-certificates + ``` 3. Start docker container - ```bash - ❯ docker run -it --rm \ - -v /tmp/ca-certificates:/tmp/ca-certificates \ - -p 8899:8899 \ - abhinavsingh/proxy.py:latest \ - --hostname 0.0.0.0 \ - --plugins proxy.plugin.CacheResponsesPlugin \ - --ca-key-file /tmp/ca-certificates/ca-key.pem \ - --ca-cert-file /tmp/ca-certificates/ca-cert.pem \ - --ca-signing-key /tmp/ca-certificates/ca-signing-key.pem - ``` + ```bash + ❯ docker run -it --rm \ + -v /tmp/ca-certificates:/tmp/ca-certificates \ + -p 8899:8899 \ + abhinavsingh/proxy.py:latest \ + --hostname 0.0.0.0 \ + --plugins proxy.plugin.CacheResponsesPlugin \ + --ca-key-file /tmp/ca-certificates/ca-key.pem \ + --ca-cert-file /tmp/ca-certificates/ca-cert.pem \ + --ca-signing-key /tmp/ca-certificates/ca-signing-key.pem + ``` - - `-v /tmp/ca-certificates:/tmp/ca-certificates` flag mounts our CA certificate directory in container environment - - `--plugins proxy.plugin.CacheResponsesPlugin` enables `CacheResponsesPlugin` so that we can inspect intercepted traffic - - `--ca-*` flags enable TLS Interception. + - `-v /tmp/ca-certificates:/tmp/ca-certificates` flag mounts our CA certificate directory in container environment + - `--plugins proxy.plugin.CacheResponsesPlugin` enables `CacheResponsesPlugin` so that we can inspect intercepted traffic + - `--ca-*` flags enable TLS Interception. 4. From another terminal, try TLS Interception using `curl`. You can omit `--cacert` flag if CA certificate is already trusted by the system. - ```bash - ❯ curl -v \ - --cacert ca-cert.pem \ - -x 127.0.0.1:8899 \ - https://httpbin.org/get - ``` + ```bash + ❯ curl -v \ + --cacert ca-cert.pem \ + -x 127.0.0.1:8899 \ + https://httpbin.org/get + ``` 5. Verify `issuer` field from response headers. - ```bash - * Server certificate: - * subject: CN=httpbin.org; C=NA; ST=Unavailable; L=Unavailable; O=Unavailable; OU=Unavailable - * start date: Jun 17 09:26:57 2020 GMT - * expire date: Jun 17 09:26:57 2022 GMT - * subjectAltName: host "httpbin.org" matched cert's "httpbin.org" - * issuer: CN=example.com - * SSL certificate verify ok. - ``` + ```bash + * Server certificate: + * subject: CN=httpbin.org; C=NA; ST=Unavailable; L=Unavailable; O=Unavailable; OU=Unavailable + * start date: Jun 17 09:26:57 2020 GMT + * expire date: Jun 17 09:26:57 2022 GMT + * subjectAltName: host "httpbin.org" matched cert's "httpbin.org" + * issuer: CN=example.com + * SSL certificate verify ok. + ``` 6. Back on docker terminal, copy response dump path logs. - ```bash - ...[redacted]... [I] access_log:338 - 172.17.0.1:56498 - CONNECT httpbin.org:443 - 1031 bytes - 1216.70 ms - ...[redacted]... [I] close:49 - Cached response at /tmp/httpbin.org-ae1a927d064e4ab386ea319eb38fe251.txt - ``` + ```bash + ...[redacted]... [I] access_log:338 - 172.17.0.1:56498 - CONNECT httpbin.org:443 - 1031 bytes - 1216.70 ms + ...[redacted]... [I] close:49 - Cached response at /tmp/httpbin.org-ae1a927d064e4ab386ea319eb38fe251.txt + ``` 7. In another terminal, `cat` the response dump: - ```bash - ❯ docker exec -it $(docker ps | grep proxy.py | awk '{ print $1 }') cat /tmp/httpbin.org-ae1a927d064e4ab386ea319eb38fe251.txt - HTTP/1.1 200 OK - ...[redacted]... - { - ...[redacted]..., - "url": "http://httpbin.org/get" - } - ``` + ```bash + ❯ docker exec -it $(docker ps | grep proxy.py | awk '{ print $1 }') cat /tmp/httpbin.org-ae1a927d064e4ab386ea319eb38fe251.txt + HTTP/1.1 200 OK + ...[redacted]... + { + ...[redacted]..., + "url": "http://httpbin.org/get" + } + ``` -Proxy Over SSH Tunnel -===================== +# Proxy Over SSH Tunnel **This is a WIP and may not work as documented** @@ -1042,8 +1035,8 @@ running on `localhost`. ### How -* Requested `remote` port is forwarded over the SSH connection. -* `proxy.py` running on the `localhost` handles and responds to +- Requested `remote` port is forwarded over the SSH connection. +- `proxy.py` running on the `localhost` handles and responds to `remote` proxy requests. ### Requirements @@ -1104,8 +1097,7 @@ access_log:328 - remote:52067 - GET httpbin.org:80 FIREWALL (allow tcp/22) -Embed proxy.py -============== +# Embed proxy.py ## Blocking Mode @@ -1152,7 +1144,7 @@ Note that: ## Non-blocking Mode Start `proxy.py` in non-blocking embedded mode with default configuration -by using `start` method: Example: +by using `start` method: Example: ```python import proxy @@ -1168,7 +1160,7 @@ Note that: 1. `start` is a context manager. It will start `proxy.py` when called and will shut it down once scope ends. -3. Just like `main`, startup flags with `start` method +1. Just like `main`, startup flags with `start` method can be customized by either passing flags as list of input arguments e.g. `start(['--port', '8899'])` or by using passing flags as kwargs e.g. `start(port=8899)`. @@ -1200,11 +1192,11 @@ if __name__ == '__main__': ``` Note that it supports: + 1. The fully-qualified name of a class as `bytes` 2. Any `type` instance for a Proxy.py plugin class. This is espacially useful for custom plugins defined locally. -Unit testing with proxy.py -========================== +# Unit testing with proxy.py ## proxy.TestCase @@ -1279,8 +1271,7 @@ class TestProxyPyEmbedded(unittest.TestCase): or simply setup / teardown `proxy.py` within `setUpClass` and `teardownClass` class methods. -Plugin Developer and Contributor Guide -====================================== +# Plugin Developer and Contributor Guide ## Everything is a plugin @@ -1288,7 +1279,7 @@ As you might have guessed by now, in `proxy.py` everything is a plugin. - We enabled proxy server plugins using `--plugins` flag. All the [plugin examples](#plugin-examples) were implementing - `HttpProxyBasePlugin`. See documentation of + `HttpProxyBasePlugin`. See documentation of [HttpProxyBasePlugin](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L894-L938) for available lifecycle hooks. Use `HttpProxyBasePlugin` to modify behavior of http(s) proxy protocol between client and upstream server. @@ -1309,14 +1300,14 @@ As you might have guessed by now, in `proxy.py` everything is a plugin. ## Internal Architecture - [HttpProtocolHandler](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L1263-L1440) -thread is started with the accepted [TcpClientConnection](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L230-L237). -`HttpProtocolHandler` is responsible for parsing incoming client request and invoking -`HttpProtocolHandlerPlugin` lifecycle hooks. + thread is started with the accepted [TcpClientConnection](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L230-L237). + `HttpProtocolHandler` is responsible for parsing incoming client request and invoking + `HttpProtocolHandlerPlugin` lifecycle hooks. - `HttpProxyPlugin` which implements `HttpProtocolHandlerPlugin` also has its own plugin -mechanism. Its responsibility is to establish connection between client and -upstream [TcpServerConnection](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L204-L227) -and invoke `HttpProxyBasePlugin` lifecycle hooks. + mechanism. Its responsibility is to establish connection between client and + upstream [TcpServerConnection](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L204-L227) + and invoke `HttpProxyBasePlugin` lifecycle hooks. - `HttpProtocolHandler` threads are started by [Acceptor](https://github.com/abhinavsingh/proxy.py/blob/b03629fa0df1595eb4995427bc601063be7fdca9/proxy.py#L424-L472) processes. @@ -1351,8 +1342,7 @@ Every pull request is tested using GitHub actions. See [GitHub workflow](https://github.com/abhinavsingh/proxy.py/tree/develop/.github/workflows) for list of tests. -Utilities -========= +# Utilities ## TCP Sockets @@ -1502,9 +1492,9 @@ for usage examples. Use `proxy.common.pki` module for: -1) Generation of public and private keys -2) Generating CSR requests -3) Signing CSR requests using custom CA. +1. Generation of public and private keys +2. Generating CSR requests +3. Signing CSR requests using custom CA. ```bash python -m proxy.common.pki -h @@ -1548,8 +1538,7 @@ FILE /Users/abhinav/Dev/proxy.py/proxy/__init__.py ``` -Frequently Asked Questions -========================== +# Frequently Asked Questions ## Threads vs Threadless @@ -1593,7 +1582,7 @@ this has not been considered. ## Unable to load plugins -Make sure plugin modules are discoverable by adding them to `PYTHONPATH`. Example: +Make sure plugin modules are discoverable by adding them to `PYTHONPATH`. Example: `PYTHONPATH=/path/to/my/app proxy --plugins my_app.proxyPlugin` @@ -1620,7 +1609,7 @@ Most likely it's a browser integration issue with system keychain. - First verify that basic auth is working using `curl` - `curl -v -x username:password@localhost:8899 https://httpbin.org/get` + `curl -v -x username:password@localhost:8899 https://httpbin.org/get` - See [this thread](https://github.com/abhinavsingh/proxy.py/issues/89#issuecomment-534845710) for further details. @@ -1668,7 +1657,7 @@ with `requests per second` sent and output of following debug script: ## None:None in access logs -Sometimes you may see `None:None` in access logs. It simply means +Sometimes you may see `None:None` in access logs. It simply means that an upstream server connection was never established i.e. `upstream_host=None`, `upstream_port=None`. @@ -1678,82 +1667,61 @@ few obvious ones include: 1. Client established a connection but never completed the request. 2. A plugin returned a response prematurely, avoiding connection to upstream server. -Flags -===== +# Flags ```bash ❯ proxy -h -usage: proxy [-h] [--backlog BACKLOG] [--basic-auth BASIC_AUTH] - [--ca-key-file CA_KEY_FILE] [--ca-cert-dir CA_CERT_DIR] - [--ca-cert-file CA_CERT_FILE] - [--ca-signing-key-file CA_SIGNING_KEY_FILE] - [--cert-file CERT_FILE] - [--client-recvbuf-size CLIENT_RECVBUF_SIZE] - [--devtools-ws-path DEVTOOLS_WS_PATH] - [--disable-headers DISABLE_HEADERS] [--disable-http-proxy] - [--enable-dashboard] [--enable-devtools] [--enable-events] - [--enable-static-server] [--enable-web-server] - [--hostname HOSTNAME] [--key-file KEY_FILE] - [--log-level LOG_LEVEL] [--log-file LOG_FILE] - [--log-format LOG_FORMAT] [--num-workers NUM_WORKERS] - [--open-file-limit OPEN_FILE_LIMIT] [--pac-file PAC_FILE] - [--pac-file-url-path PAC_FILE_URL_PATH] - [--pid-file PID_FILE] [--plugins PLUGINS] [--port PORT] - [--server-recvbuf-size SERVER_RECVBUF_SIZE] - [--static-server-dir STATIC_SERVER_DIR] [--threadless] - [--timeout TIMEOUT] [--version] - -proxy.py v2.3.0 +usage: proxy [-h] [--threadless] [--backlog BACKLOG] [--enable-events] + [--hostname HOSTNAME] [--port PORT] [--num-workers NUM_WORKERS] + [--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--key-file KEY_FILE] + [--timeout TIMEOUT] [--pid-file PID_FILE] [--version] + [--disable-http-proxy] [--enable-dashboard] [--enable-devtools] + [--enable-static-server] [--enable-web-server] + [--log-level LOG_LEVEL] [--log-file LOG_FILE] + [--log-format LOG_FORMAT] [--open-file-limit OPEN_FILE_LIMIT] + [--plugins PLUGINS] [--ca-key-file CA_KEY_FILE] + [--ca-cert-dir CA_CERT_DIR] [--ca-cert-file CA_CERT_FILE] + [--ca-file CA_FILE] [--ca-signing-key-file CA_SIGNING_KEY_FILE] + [--cert-file CERT_FILE] [--disable-headers DISABLE_HEADERS] + [--server-recvbuf-size SERVER_RECVBUF_SIZE] + [--basic-auth BASIC_AUTH] [--cache-dir CACHE_DIR] + [--static-server-dir STATIC_SERVER_DIR] [--pac-file PAC_FILE] + [--pac-file-url-path PAC_FILE_URL_PATH] + [--filtered-client-ips FILTERED_CLIENT_IPS] + +proxy.py v2.3.1 optional arguments: -h, --help show this help message and exit + --threadless Default: False. When disabled a new thread is spawned + to handle each client connection. --backlog BACKLOG Default: 100. Maximum number of pending connections to proxy server - --basic-auth BASIC_AUTH - Default: No authentication. Specify colon separated - user:password to enable basic authentication. - --ca-key-file CA_KEY_FILE - Default: None. CA key to use for signing dynamically - generated HTTPS certificates. If used, must also pass - --ca-cert-file and --ca-signing-key-file - --ca-cert-dir CA_CERT_DIR - Default: ~/.proxy.py. Directory to store dynamically - generated certificates. Also see --ca-key-file, --ca- - cert-file and --ca-signing-key-file - --ca-cert-file CA_CERT_FILE - Default: None. Signing certificate to use for signing - dynamically generated HTTPS certificates. If used, - must also pass --ca-key-file and --ca-signing-key-file - --ca-file CA_FILE Default: None. Provide path to custom CA file for peer - certificate validation. Specially useful on MacOS. - --ca-signing-key-file CA_SIGNING_KEY_FILE - Default: None. CA signing key to use for dynamic - generation of HTTPS certificates. If used, must also - pass --ca-key-file and --ca-cert-file - --cert-file CERT_FILE - Default: None. Server certificate to enable end-to-end - TLS encryption with clients. If used, must also pass - --key-file. + --enable-events Default: False. Enables core to dispatch lifecycle + events. Plugins can be used to subscribe for core + events. + --hostname HOSTNAME Default: ::1. Server IP address. + --port PORT Default: 8899. Server port. + --num-workers NUM_WORKERS + Defaults to number of CPU cores. --client-recvbuf-size CLIENT_RECVBUF_SIZE Default: 1 MB. Maximum amount of data received from the client in a single recv() operation. Bump this value for faster uploads at the expense of increased RAM. - --devtools-ws-path DEVTOOLS_WS_PATH - Default: /devtools. Only applicable if --enable- - devtools is used. - --disable-headers DISABLE_HEADERS - Default: None. Comma separated list of headers to - remove before dispatching client request to upstream - server. + --key-file KEY_FILE Default: None. Server key file to enable end-to-end + TLS encryption with clients. If used, must also pass + --cert-file. + --timeout TIMEOUT Default: 10. Number of seconds after which an inactive + connection must be dropped. Inactivity is defined by + no data sent or received by the client. + --pid-file PID_FILE Default: None. Save parent process ID to a file. + --version, -v Prints proxy.py version. --disable-http-proxy Default: False. Whether to disable proxy.HttpProxyPlugin. --enable-dashboard Default: False. Enables proxy.py dashboard. --enable-devtools Default: False. Enables integration with Chrome Devtool Frontend. Also see --devtools-ws-path. - --enable-events Default: False. Enables core to dispatch lifecycle - events. Plugins can be used to subscribe for core - events. --enable-static-server Default: False. Enable inbuilt static file server. Optionally, also use --static-server-dir to serve @@ -1762,10 +1730,6 @@ optional arguments: python module folder. --enable-web-server Default: False. Whether to enable proxy.HttpWebServerPlugin. - --hostname HOSTNAME Default: ::1. Server IP address. - --key-file KEY_FILE Default: None. Server key file to enable end-to-end - TLS encryption with clients. If used, must also pass - --cert-file. --log-level LOG_LEVEL Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. Both upper and lowercase values are allowed. @@ -1774,41 +1738,65 @@ optional arguments: --log-file LOG_FILE Default: sys.stdout. Log file destination. --log-format LOG_FORMAT Log format for Python logger. - --num-workers NUM_WORKERS - Defaults to number of CPU cores. --open-file-limit OPEN_FILE_LIMIT Default: 1024. Maximum number of files (TCP connections) that proxy.py can open concurrently. - --pac-file PAC_FILE A file (Proxy Auto Configuration) or string to serve - when the server receives a direct file request. Using - this option enables proxy.HttpWebServerPlugin. - --pac-file-url-path PAC_FILE_URL_PATH - Default: /. Web server path to serve the PAC file. - --pid-file PID_FILE Default: None. Save parent process ID to a file. --plugins PLUGINS Comma separated plugins - --port PORT Default: 8899. Server port. + --ca-key-file CA_KEY_FILE + Default: None. CA key to use for signing dynamically + generated HTTPS certificates. If used, must also pass + --ca-cert-file and --ca-signing-key-file + --ca-cert-dir CA_CERT_DIR + Default: ~/.proxy.py. Directory to store dynamically + generated certificates. Also see --ca-key-file, --ca- + cert-file and --ca-signing-key-file + --ca-cert-file CA_CERT_FILE + Default: None. Signing certificate to use for signing + dynamically generated HTTPS certificates. If used, + must also pass --ca-key-file and --ca-signing-key-file + --ca-file CA_FILE Default: None. Provide path to custom CA file for peer + certificate validation. Specially useful on MacOS. + --ca-signing-key-file CA_SIGNING_KEY_FILE + Default: None. CA signing key to use for dynamic + generation of HTTPS certificates. If used, must also + pass --ca-key-file and --ca-cert-file + --cert-file CERT_FILE + Default: None. Server certificate to enable end-to-end + TLS encryption with clients. If used, must also pass + --key-file. + --disable-headers DISABLE_HEADERS + Default: None. Comma separated list of headers to + remove before dispatching client request to upstream + server. --server-recvbuf-size SERVER_RECVBUF_SIZE Default: 1 MB. Maximum amount of data received from the server in a single recv() operation. Bump this value for faster downloads at the expense of increased RAM. + --basic-auth BASIC_AUTH + Default: No authentication. Specify colon separated + user:password to enable basic authentication. + --cache-dir CACHE_DIR + Default: A temporary directory. Flag only applicable + when cache plugin is used with on-disk storage. --static-server-dir STATIC_SERVER_DIR Default: "public" folder in directory where proxy.py is placed. This option is only applicable when static server is also enabled. See --enable-static-server. - --threadless Default: False. When disabled a new thread is spawned - to handle each client connection. - --timeout TIMEOUT Default: 10. Number of seconds after which an inactive - connection must be dropped. Inactivity is defined by - no data sent or received by the client. - --version, -v Prints proxy.py version. + --pac-file PAC_FILE A file (Proxy Auto Configuration) or string to serve + when the server receives a direct file request. Using + this option enables proxy.HttpWebServerPlugin. + --pac-file-url-path PAC_FILE_URL_PATH + Default: /. Web server path to serve the PAC file. + --filtered-client-ips FILTERED_CLIENT_IPS + Default: 127.0.0.1,::1. Comma separated list of IPv4 + and IPv6 addresses. Proxy.py not working? Report at: https://github.com/abhinavsingh/proxy.py/issues/new ``` -Changelog -========= +# Changelog ## v2.x @@ -1820,7 +1808,7 @@ Changelog ## v1.x - `Python3` only. - - Deprecated support for ~~Python 2.x~~. + - Deprecated support for ~~Python 2.x~~. - Added support multi core accept. - Added plugin support. diff --git a/proxy/common/version.py b/proxy/common/version.py index d7dc8e484..585f15556 100644 --- a/proxy/common/version.py +++ b/proxy/common/version.py @@ -8,5 +8,5 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -VERSION = (2, 3, 0) +VERSION = (2, 3, 1) __version__ = '.'.join(map(str, VERSION[0:3])) diff --git a/proxy/http/proxy/auth.py b/proxy/http/proxy/auth.py index 263ec9bd0..d1ac9a861 100644 --- a/proxy/http/proxy/auth.py +++ b/proxy/http/proxy/auth.py @@ -35,8 +35,8 @@ def before_upstream_connection( raise ProxyAuthenticationFailed() parts = request.headers[b'proxy-authorization'][1].split() if len(parts) != 2 \ - and parts[0].lower() != b'basic' \ - and parts[1] != self.flags.auth_code: + or parts[0].lower() != b'basic' \ + or parts[1] != self.flags.auth_code: raise ProxyAuthenticationFailed() return request diff --git a/setup.py b/setup.py index b09bc8009..19f6f7b1d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ """ from setuptools import setup, find_packages -VERSION = (2, 3, 0) +VERSION = (2, 3, 1) __version__ = '.'.join(map(str, VERSION[0:3])) __description__ = '''⚡⚡⚡Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on Network monitoring, controls & Application development, testing, debugging.''' From d1681fc11cca1ef53e636d4227d850870d39a60f Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 10 Jan 2021 18:43:29 +0200 Subject: [PATCH 73/79] Update tox from 3.20.1 to 3.21.0 (#480) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index a9e882abf..c0f22dc0d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.4 mypy==0.790 py-spy==0.3.3 codecov==2.1.11 -tox==3.20.1 +tox==3.21.0 mccabe==0.6.1 pylint==2.6.0 rope==0.18.0 From 0c97ee713d0feb98bec88c548338092ff1c7ae9b Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Thu, 14 Jan 2021 08:54:24 +0200 Subject: [PATCH 74/79] Update tox from 3.21.0 to 3.21.1 (#484) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index c0f22dc0d..b4904dd6c 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.4 mypy==0.790 py-spy==0.3.3 codecov==2.1.11 -tox==3.21.0 +tox==3.21.1 mccabe==0.6.1 pylint==2.6.0 rope==0.18.0 From 07b23de8873832955cffe1fb739e6dba49dd0fa9 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 19 Jan 2021 16:59:56 +0200 Subject: [PATCH 75/79] Update pytest-cov from 2.10.1 to 2.11.0 (#486) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index b4904dd6c..1076b0100 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -2,7 +2,7 @@ python-coveralls==2.9.3 coverage==5.3.1 flake8==3.8.4 pytest==6.2.1 -pytest-cov==2.10.1 +pytest-cov==2.11.0 autopep8==1.5.4 mypy==0.790 py-spy==0.3.3 From e1f72d21a161abe0836765c2e437f01dc30a6618 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 3 Feb 2021 12:27:57 +0200 Subject: [PATCH 76/79] Update tox from 3.21.1 to 3.21.3 (#493) --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 1076b0100..b42457ebf 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -7,7 +7,7 @@ autopep8==1.5.4 mypy==0.790 py-spy==0.3.3 codecov==2.1.11 -tox==3.21.1 +tox==3.21.3 mccabe==0.6.1 pylint==2.6.0 rope==0.18.0 From 2f046092f0967dec2d078ea513af03808ea5b44c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 3 Feb 2021 12:34:17 +0200 Subject: [PATCH 77/79] Update coverage from 5.3.1 to 5.4 (#491) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index b42457ebf..03205bc9d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,5 +1,5 @@ python-coveralls==2.9.3 -coverage==5.3.1 +coverage==5.4 flake8==3.8.4 pytest==6.2.1 pytest-cov==2.11.0 From 0f8e0ab216469af5248af6cc4a54935d65743ccb Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 3 Feb 2021 12:34:45 +0200 Subject: [PATCH 78/79] Update pytest from 6.2.1 to 6.2.2 (#490) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 03205bc9d..822c2039d 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,7 +1,7 @@ python-coveralls==2.9.3 coverage==5.4 flake8==3.8.4 -pytest==6.2.1 +pytest==6.2.2 pytest-cov==2.11.0 autopep8==1.5.4 mypy==0.790 From a8075e20ec4e5b96d2c36a46b81ab2f44a60c17c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 3 Feb 2021 12:35:53 +0200 Subject: [PATCH 79/79] Update pytest-cov from 2.11.0 to 2.11.1 (#488) Co-authored-by: Abhinav Singh --- requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 822c2039d..af0e4b9dc 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -2,7 +2,7 @@ python-coveralls==2.9.3 coverage==5.4 flake8==3.8.4 pytest==6.2.2 -pytest-cov==2.11.0 +pytest-cov==2.11.1 autopep8==1.5.4 mypy==0.790 py-spy==0.3.3