diff --git a/proxy/common/utils.py b/proxy/common/utils.py index 1eb64ec783..f65cf10b98 100644 --- a/proxy/common/utils.py +++ b/proxy/common/utils.py @@ -185,6 +185,7 @@ def wrap_socket( conn: socket.socket, keyfile: str, certfile: str, ) -> ssl.SSLSocket: + """Use this to upgrade server_side socket to TLS.""" ctx = ssl.create_default_context( ssl.Purpose.CLIENT_AUTH, ) @@ -201,7 +202,9 @@ def wrap_socket( def new_socket_connection( - addr: Tuple[str, int], timeout: float = DEFAULT_TIMEOUT, source_address: Optional[Tuple[str, int]] = None, + addr: Tuple[str, int], + timeout: float = DEFAULT_TIMEOUT, + source_address: Optional[Tuple[str, int]] = None, ) -> socket.socket: conn = None try: diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py index 5684083adb..426f51be61 100644 --- a/proxy/core/acceptor/pool.py +++ b/proxy/core/acceptor/pool.py @@ -25,7 +25,7 @@ from ..event import EventQueue -from ...common.utils import bytes_ +from ...common.utils import bytes_, is_threadless from ...common.flag import flags from ...common.constants import DEFAULT_BACKLOG, DEFAULT_IPV6_HOSTNAME from ...common.constants import DEFAULT_NUM_WORKERS, DEFAULT_PORT @@ -194,7 +194,10 @@ def _start_acceptors(self) -> None: ) self.acceptors.append(acceptor) self.work_queues.append(work_queue[0]) - logger.info('Started %d workers' % self.flags.num_workers) + mode = 'threadless' if is_threadless( + self.flags.threadless, self.flags.threaded, + ) else 'threaded' + logger.info('Started %d %s workers' % (self.flags.num_workers, mode)) def _write_pid_file(self) -> None: if self.flags.pid_file is not None: diff --git a/proxy/core/connection/server.py b/proxy/core/connection/server.py index 30692fda71..80b09f023c 100644 --- a/proxy/core/connection/server.py +++ b/proxy/core/connection/server.py @@ -44,7 +44,7 @@ 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.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 ctx.check_hostname = True self.connection.setblocking(True) self._conn = ctx.wrap_socket( diff --git a/proxy/core/eventing/__init__.py b/proxy/core/eventing/__init__.py new file mode 100644 index 0000000000..232621f0b5 --- /dev/null +++ b/proxy/core/eventing/__init__.py @@ -0,0 +1,10 @@ +# -*- 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. +""" diff --git a/proxy/core/eventing/publisher.py b/proxy/core/eventing/publisher.py new file mode 100644 index 0000000000..35bffc24eb --- /dev/null +++ b/proxy/core/eventing/publisher.py @@ -0,0 +1,32 @@ +# -*- 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. +""" + + +class EventingPublisher: + """EventingTopic provides utilities around pubsub functionalities + within a particular topic context. + + Each topic a unix socket domain file on disk. Publishers from + within proxy.py instance or from outside can publish to these + topics. + + Payload published to a topic can be consumed by multiple subscriber, + which can be across CPU processes. Subscribers can ideally also + exist outside of proxy.py instance because topics are on-disk. + + Subscribers will only receive events published after they + have completed the subscription phase. No buffering/caching/redelivery/ack + of payload is currently implemented. + """ + + def publish(self, topic: str) -> None: + """Publish payload into a topic.""" + pass diff --git a/proxy/core/eventing/subscriber.py b/proxy/core/eventing/subscriber.py new file mode 100644 index 0000000000..9b168642b0 --- /dev/null +++ b/proxy/core/eventing/subscriber.py @@ -0,0 +1,25 @@ +# -*- 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. +""" + + +class EventingSubscriber: + """EventingSubscriber provides subscription utilities for a topic.""" + + def __init__(self, topic: str) -> None: + self.topic = topic + + def subscribe(self) -> None: + """Subscribe to topic.""" + pass + + def unsubscribe(self) -> None: + """Unsubscribe from topic""" + pass diff --git a/proxy/proxy.py b/proxy/proxy.py index 5f1ff77de3..583e8d23b4 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -20,7 +20,6 @@ from .core.acceptor import AcceptorPool from .http.handler import HttpProtocolHandler from .core.event import EventManager -from .common.utils import is_threadless from .common.flag import FlagParser, flags from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL from .common.constants import DEFAULT_OPEN_FILE_LIMIT, DEFAULT_PLUGINS, DEFAULT_VERSION @@ -123,18 +122,15 @@ def __enter__(self) -> 'Proxy': ) self.pool.setup() assert self.pool is not None - mode = 'threadless' if is_threadless( - self.flags.threadless, self.flags.threaded, - ) else 'threaded' if self.flags.unix_socket_path: logger.info( - 'Listening on %s (%s)' % - (self.flags.unix_socket_path, mode), + 'Listening on %s' % + self.flags.unix_socket_path, ) else: logger.info( - 'Listening on %s:%s (%s)' % - (self.pool.flags.hostname, self.pool.flags.port, mode), + 'Listening on %s:%s' % + (self.pool.flags.hostname, self.pool.flags.port), ) return self diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 6bb85ee21d..6ef69480c5 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -19,7 +19,7 @@ from proxy.common.flag import FlagParser from proxy.core.connection import TcpClientConnection from proxy.http.handler import HttpProtocolHandler -from proxy.http.parser import httpParserStates +from proxy.http.parser import HttpParser, httpParserStates, httpParserTypes from proxy.common.utils import build_http_response, build_http_request, bytes_, text_ from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PAC_FILE, PLUGIN_WEB_SERVER, PROXY_PY_DIR from proxy.http.server import HttpWebServerPlugin @@ -137,10 +137,6 @@ def test_default_web_server_returns_404( HttpWebServerPlugin.DEFAULT_404_RESPONSE, ) - @unittest.skipIf( - os.environ.get('GITHUB_ACTIONS', 'false') == 'true', - 'Disabled on GitHub actions because this test is flaky on GitHub infrastructure.', - ) @mock.patch('selectors.DefaultSelector') @mock.patch('socket.fromfd') def test_static_web_server_serves( @@ -202,18 +198,21 @@ def test_static_web_server_serves( self.assertEqual(mock_selector.return_value.select.call_count, 2) self.assertEqual(self._conn.send.call_count, 1) encoded_html_file_content = gzip.compress(html_file_content) + + # parse response and verify + response = HttpParser(httpParserTypes.RESPONSE_PARSER) + response.parse(self._conn.send.call_args[0][0]) + self.assertEqual(response.code, b'200') + self.assertEqual(response.header(b'content-type'), b'text/html') + self.assertEqual(response.header(b'cache-control'), b'max-age=86400') + self.assertEqual(response.header(b'content-encoding'), b'gzip') + self.assertEqual(response.header(b'connection'), b'close') self.assertEqual( - self._conn.send.call_args[0][0], build_http_response( - 200, reason=b'OK', headers={ - b'Content-Type': b'text/html', - b'Cache-Control': b'max-age=86400', - b'Content-Encoding': b'gzip', - b'Connection': b'close', - b'Content-Length': bytes_(len(encoded_html_file_content)), - }, - body=encoded_html_file_content, - ), + response.header(b'content-length'), + bytes_(len(encoded_html_file_content)), ) + assert response.body + self.assertEqual(gzip.decompress(response.body), html_file_content) @mock.patch('selectors.DefaultSelector') @mock.patch('socket.fromfd')