Skip to content

Commit c6fceb6

Browse files
[Middleware] Capability in the core to allow custom client connection classes (#993)
* Move all TCP server related flags within `tcp_server.py` and also move the encryption functionality within TCP base server * Templatize `BaseTcpServerHandler` which now expects a client connection object bound to `TcpClientConnection`. This will allow for custom `HttpClientConnection` object in future to be used by `HttpProtocolHandler` * Pass necessary flags to allow self-signed certificates * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix https integration tests * Affected by #994 * Fix docs Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent d17dc9e commit c6fceb6

File tree

13 files changed

+180
-102
lines changed

13 files changed

+180
-102
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ sign-https-certificates:
5757
python -m proxy.common.pki sign_csr \
5858
--csr-path $(HTTPS_CSR_FILE_PATH) \
5959
--crt-path $(HTTPS_SIGNED_CERT_FILE_PATH) \
60-
--hostname example.com \
60+
--hostname localhost \
6161
--private-key-path $(CA_KEY_FILE_PATH) \
6262
--public-key-path $(CA_CERT_FILE_PATH)
6363

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,5 @@
316316
(_py_class_role, 'EventQueue'),
317317
(_py_obj_role, 'proxy.core.work.threadless.T'),
318318
(_py_obj_role, 'proxy.core.work.work.T'),
319+
(_py_obj_role, 'proxy.core.base.tcp_server.T'),
319320
]

examples/ssl_echo_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from proxy.core.connection import TcpClientConnection
1818

1919

20-
class EchoSSLServerHandler(BaseTcpServerHandler):
20+
class EchoSSLServerHandler(BaseTcpServerHandler[TcpClientConnection]):
2121
"""Wraps client socket during initialization."""
2222

2323
def initialize(self) -> None:

examples/tcp_echo_server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
from proxy import Proxy
1515
from proxy.core.base import BaseTcpServerHandler
16+
from proxy.core.connection import TcpClientConnection
1617

1718

18-
class EchoServerHandler(BaseTcpServerHandler):
19+
class EchoServerHandler(BaseTcpServerHandler[TcpClientConnection]):
1920
"""Sets client socket to non-blocking during initialization."""
2021

2122
def initialize(self) -> None:

proxy/common/pki.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ def run_openssl_command(command: List[str], timeout: int) -> bool:
268268
parser.add_argument(
269269
'--subject',
270270
type=str,
271-
default='/CN=example.com',
272-
help='Subject to use for public key generation. Default: /CN=example.com',
271+
default='/CN=localhost',
272+
help='Subject to use for public key generation. Default: /CN=localhost',
273273
)
274274
parser.add_argument(
275275
'--csr-path',

proxy/core/base/tcp_server.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,75 @@
1212
1313
tcp
1414
"""
15+
import ssl
16+
import socket
1517
import logging
1618
import selectors
1719

1820
from abc import abstractmethod
19-
from typing import Any, Optional
21+
from typing import Any, Optional, TypeVar, Union
22+
23+
from ...common.flag import flags
24+
from ...common.utils import wrap_socket
25+
from ...common.types import Readables, SelectableEvents, Writables
26+
from ...common.constants import DEFAULT_CERT_FILE, DEFAULT_CLIENT_RECVBUF_SIZE
27+
from ...common.constants import DEFAULT_KEY_FILE, DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_TIMEOUT
2028

2129
from ...core.work import Work
2230
from ...core.connection import TcpClientConnection
23-
from ...common.types import Readables, SelectableEvents, Writables
2431

2532
logger = logging.getLogger(__name__)
2633

2734

28-
class BaseTcpServerHandler(Work[TcpClientConnection]):
35+
flags.add_argument(
36+
'--key-file',
37+
type=str,
38+
default=DEFAULT_KEY_FILE,
39+
help='Default: None. Server key file to enable end-to-end TLS encryption with clients. '
40+
'If used, must also pass --cert-file.',
41+
)
42+
43+
flags.add_argument(
44+
'--cert-file',
45+
type=str,
46+
default=DEFAULT_CERT_FILE,
47+
help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. '
48+
'If used, must also pass --key-file.',
49+
)
50+
51+
flags.add_argument(
52+
'--client-recvbuf-size',
53+
type=int,
54+
default=DEFAULT_CLIENT_RECVBUF_SIZE,
55+
help='Default: ' + str(int(DEFAULT_CLIENT_RECVBUF_SIZE / 1024)) +
56+
' KB. Maximum amount of data received from the '
57+
'client in a single recv() operation.',
58+
)
59+
60+
flags.add_argument(
61+
'--server-recvbuf-size',
62+
type=int,
63+
default=DEFAULT_SERVER_RECVBUF_SIZE,
64+
help='Default: ' + str(int(DEFAULT_SERVER_RECVBUF_SIZE / 1024)) +
65+
' KB. Maximum amount of data received from the '
66+
'server in a single recv() operation.',
67+
)
68+
69+
flags.add_argument(
70+
'--timeout',
71+
type=int,
72+
default=DEFAULT_TIMEOUT,
73+
help='Default: ' + str(DEFAULT_TIMEOUT) +
74+
'. Number of seconds after which '
75+
'an inactive connection must be dropped. Inactivity is defined by no '
76+
'data sent or received by the client.',
77+
)
78+
79+
80+
T = TypeVar('T', bound=TcpClientConnection)
81+
82+
83+
class BaseTcpServerHandler(Work[T]):
2984
"""BaseTcpServerHandler implements Work interface.
3085
3186
BaseTcpServerHandler lifecycle is controlled by Threadless core
@@ -56,6 +111,14 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
56111
self.work.address,
57112
)
58113

114+
def initialize(self) -> None:
115+
"""Optionally upgrades connection to HTTPS,
116+
sets ``conn`` in non-blocking mode and initializes
117+
HTTP protocol plugins."""
118+
conn = self._optionally_wrap_socket(self.work.connection)
119+
conn.setblocking(False)
120+
logger.debug('Handling connection %s' % self.work.address)
121+
59122
@abstractmethod
60123
def handle_data(self, data: memoryview) -> Optional[bool]:
61124
"""Optionally return True to close client connection."""
@@ -139,3 +202,21 @@ async def handle_readables(self, readables: Readables) -> bool:
139202
else:
140203
teardown = True
141204
return teardown
205+
206+
def _encryption_enabled(self) -> bool:
207+
return self.flags.keyfile is not None and \
208+
self.flags.certfile is not None
209+
210+
def _optionally_wrap_socket(
211+
self, conn: socket.socket,
212+
) -> Union[ssl.SSLSocket, socket.socket]:
213+
"""Attempts to wrap accepted client connection using provided certificates.
214+
215+
Shutdown and closes client connection upon error.
216+
"""
217+
if self._encryption_enabled():
218+
assert self.flags.keyfile and self.flags.certfile
219+
# TODO(abhinavsingh): Insecure TLS versions must not be accepted by default
220+
conn = wrap_socket(conn, self.flags.keyfile, self.flags.certfile)
221+
self.work._conn = conn
222+
return conn

proxy/core/base/tcp_tunnel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
from ...common.types import Readables, SelectableEvents, Writables
1919
from ...common.utils import text_
2020

21-
from ..connection import TcpServerConnection
21+
from ..connection import TcpServerConnection, TcpClientConnection
2222
from .tcp_server import BaseTcpServerHandler
2323

2424
logger = logging.getLogger(__name__)
2525

2626

27-
class BaseTcpTunnelHandler(BaseTcpServerHandler):
27+
class BaseTcpTunnelHandler(BaseTcpServerHandler[TcpClientConnection]):
2828
"""BaseTcpTunnelHandler build on-top of BaseTcpServerHandler work class.
2929
3030
On-top of BaseTcpServerHandler implementation,

proxy/http/handler.py

Lines changed: 11 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@
1616
import logging
1717
import selectors
1818

19-
from typing import Tuple, List, Type, Union, Optional, Any
19+
from typing import Tuple, List, Type, Optional, Any
2020

21-
from ..common.flag import flags
22-
from ..common.utils import wrap_socket
2321
from ..core.base import BaseTcpServerHandler
2422
from ..core.connection import TcpClientConnection
2523
from ..common.types import Readables, SelectableEvents, Writables
26-
from ..common.constants import DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_KEY_FILE
27-
from ..common.constants import DEFAULT_SELECTOR_SELECT_TIMEOUT, DEFAULT_TIMEOUT
24+
from ..common.constants import DEFAULT_SELECTOR_SELECT_TIMEOUT
2825

2926
from .exception import HttpProtocolException
3027
from .plugin import HttpProtocolHandlerPlugin
@@ -35,33 +32,7 @@
3532
logger = logging.getLogger(__name__)
3633

3734

38-
flags.add_argument(
39-
'--client-recvbuf-size',
40-
type=int,
41-
default=DEFAULT_CLIENT_RECVBUF_SIZE,
42-
help='Default: ' + str(int(DEFAULT_CLIENT_RECVBUF_SIZE / 1024)) +
43-
' KB. Maximum amount of data received from the '
44-
'client in a single recv() operation.',
45-
)
46-
flags.add_argument(
47-
'--key-file',
48-
type=str,
49-
default=DEFAULT_KEY_FILE,
50-
help='Default: None. Server key file to enable end-to-end TLS encryption with clients. '
51-
'If used, must also pass --cert-file.',
52-
)
53-
flags.add_argument(
54-
'--timeout',
55-
type=int,
56-
default=DEFAULT_TIMEOUT,
57-
help='Default: ' + str(DEFAULT_TIMEOUT) +
58-
'. Number of seconds after which '
59-
'an inactive connection must be dropped. Inactivity is defined by no '
60-
'data sent or received by the client.',
61-
)
62-
63-
64-
class HttpProtocolHandler(BaseTcpServerHandler):
35+
class HttpProtocolHandler(BaseTcpServerHandler[TcpClientConnection]):
6536
"""HTTP, HTTPS, HTTP2, WebSockets protocol handler.
6637
6738
Accepts `Client` connection and delegates to HttpProtocolHandlerPlugin.
@@ -86,17 +57,16 @@ def __init__(self, *args: Any, **kwargs: Any):
8657
##
8758

8859
def initialize(self) -> None:
89-
"""Optionally upgrades connection to HTTPS,
90-
sets ``conn`` in non-blocking mode and initializes
91-
HTTP protocol plugins.
92-
"""
93-
conn = self._optionally_wrap_socket(self.work.connection)
94-
conn.setblocking(False)
60+
super().initialize()
9561
# Update client connection reference if connection was wrapped
62+
# This is here in `handler` and not `tcp_server` because
63+
# `tcp_server` is agnostic to constructing TcpClientConnection
64+
# objects.
9665
if self._encryption_enabled():
97-
self.work = TcpClientConnection(conn=conn, addr=self.work.addr)
98-
# self._initialize_plugins()
99-
logger.debug('Handling connection %s' % self.work.address)
66+
self.work = TcpClientConnection(
67+
conn=self.work.connection,
68+
addr=self.work.addr,
69+
)
10070

10171
def is_inactive(self) -> bool:
10272
if not self.work.has_buffer() and \
@@ -334,23 +304,6 @@ def _parse_first_request(self, data: memoryview) -> bool:
334304
self.work._conn = output
335305
return False
336306

337-
def _encryption_enabled(self) -> bool:
338-
return self.flags.keyfile is not None and \
339-
self.flags.certfile is not None
340-
341-
def _optionally_wrap_socket(
342-
self, conn: socket.socket,
343-
) -> Union[ssl.SSLSocket, socket.socket]:
344-
"""Attempts to wrap accepted client connection using provided certificates.
345-
346-
Shutdown and closes client connection upon error.
347-
"""
348-
if self._encryption_enabled():
349-
assert self.flags.keyfile and self.flags.certfile
350-
# TODO(abhinavsingh): Insecure TLS versions must not be accepted by default
351-
conn = wrap_socket(conn, self.flags.keyfile, self.flags.certfile)
352-
return conn
353-
354307
def _connection_inactive_for(self) -> float:
355308
return time.time() - self.last_activity
356309

proxy/http/proxy/server.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
from ...common.types import Readables, Writables, Descriptors
3838
from ...common.constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_FILE
3939
from ...common.constants import DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE
40-
from ...common.constants import COMMA, DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CERT_FILE
40+
from ...common.constants import COMMA, DEFAULT_HTTPS_PROXY_ACCESS_LOG_FORMAT
4141
from ...common.constants import PROXY_AGENT_HEADER_VALUE, DEFAULT_DISABLE_HEADERS
42-
from ...common.constants import DEFAULT_HTTP_PROXY_ACCESS_LOG_FORMAT, DEFAULT_HTTPS_PROXY_ACCESS_LOG_FORMAT
42+
from ...common.constants import DEFAULT_HTTP_PROXY_ACCESS_LOG_FORMAT
4343
from ...common.constants import DEFAULT_DISABLE_HTTP_PROXY, PLUGIN_PROXY_AUTH
4444
from ...common.utils import text_
4545
from ...common.pki import gen_public_key, gen_csr, sign_csr
@@ -52,15 +52,6 @@
5252
logger = logging.getLogger(__name__)
5353

5454

55-
flags.add_argument(
56-
'--server-recvbuf-size',
57-
type=int,
58-
default=DEFAULT_SERVER_RECVBUF_SIZE,
59-
help='Default: ' + str(int(DEFAULT_SERVER_RECVBUF_SIZE / 1024)) +
60-
' KB. Maximum amount of data received from the '
61-
'server in a single recv() operation.',
62-
)
63-
6455
flags.add_argument(
6556
'--disable-http-proxy',
6657
action='store_true',
@@ -116,14 +107,6 @@
116107
'HTTPS certificates. If used, must also pass --ca-key-file and --ca-cert-file',
117108
)
118109

119-
flags.add_argument(
120-
'--cert-file',
121-
type=str,
122-
default=DEFAULT_CERT_FILE,
123-
help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. '
124-
'If used, must also pass --key-file.',
125-
)
126-
127110
flags.add_argument(
128111
'--auth-plugin',
129112
type=str,

proxy/plugin/web_server_route.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
logger = logging.getLogger(__name__)
1919

2020
HTTP_RESPONSE = okResponse(content=b'HTTP route response')
21-
HTTPS_RESPONSE = okResponse(content=b'HTTP route response')
21+
HTTPS_RESPONSE = okResponse(content=b'HTTPS route response')
2222

2323

2424
class WebServerPlugin(HttpWebServerBasePlugin):

0 commit comments

Comments
 (0)