-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for synchronous TLS-in-TLS
connections.
#732
Add support for synchronous TLS-in-TLS
connections.
#732
Conversation
We will most likely need to write these tests for each backend. without tls
with tls
with tls in tls
|
This tests are simply smoke tests that do not require any assertion; we simply want to know if it runs or not. (maybe excluding read tests) |
Now we have three failed tests, all of which are synchornous |
Could you please review this? @tomchristie It's a complicated PR that may be difficult to review, but I believe we need it in 0.18.0. |
@@ -16,6 +20,17 @@ def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes: | |||
def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None: | |||
raise NotImplementedError() # pragma: nocover | |||
|
|||
def __enter__(self) -> "Self": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need the typing_extensions.Self
here. (?)
Can we just have this return NetworkStream
.
The override point is close()
, not the __enter__
/__exit__
which will stay the same even for subclasses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we can inherit, I believe we should always use Self to avoid strange type issues, such as when the instance of SlowNetworkStream is NetworkStream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example:
import typing
from httpcore._backends.base import NetworkStream
from time import sleep
class SlowNetworkStream(NetworkStream):
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
sleep(100)
with SlowNetworkStream() as stream:
reveal_type(stream)
OUTPUT test.py:11: note: Revealed type is "httpcore._backends.base.NetworkStream"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay right.
Could we use the TypeVar
style, then?
Eg... in httpx
https://github.com/encode/httpx/blob/76c9cb65f2a159adb764c2236d139f85b46e1506/httpx/_client.py#L60
https://github.com/encode/httpx/blob/76c9cb65f2a159adb764c2236d139f85b46e1506/httpx/_client.py#L1263
Really prefer us avoiding introducing new third party packages wherever possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially, we used TypeVar, but it was too complicated.
Okay, so...
Do those two changes seem acceptable to you? Otherwise looking great. Thanks! 😊 |
If we do that, we will be able to hang out indefinitely even though a timeout has been set; do we want that? |
I'm not suggesting that we remove the timeout, I'm suggesting that we keep it nice and simple and just call |
I'm not sure how we can do that 😢 ; could you please provide an example? |
IMO the current behavior is correct. |
This... def _perform_io(
self,
func: typing.Callable[..., typing.Any],
timeout: typing.Optional[float] = None,
) -> typing.Any:
ret = None
self._sock.settimeout(timeout)
while True:
errno = None
try:
ret = func()
except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e:
errno = e.errno
self._sock.sendall(self._outgoing.read())
if errno == ssl.SSL_ERROR_WANT_READ:
buf = self._sock.recv(self.TLS_RECORD_SIZE)
if buf:
self._incoming.write(buf)
else:
self._incoming.write_eof() # pragma: no cover
if errno is None:
return ret The timeouts aren't perfect if we're in TLS-in-TLS, but we probably don't care. (vs the cost of this method being complex to understand.) |
Time problem of that solution is: |
So, according to current timeout calculation, what kind of timeout it would be? |
And other question here is if |
It has already been implemented, and it functions exactly like a standard TLS connection, with write timeouts for write operations, read timeouts for read operations, and so on. @tomchristie suggests setting the timeout only once, but I'm afraid we won't be able to do so. One TLS operation, such as If the socket timeout was only set once, it would be the same for each socket operation, so we need to decrease the timeout based on how long our socket operation takes. So, if we do this, we may have to wait 50 seconds when the timeout is set to 5 or even hang indefinitely. |
As a result, the problem I described is easily reproducible. change the def handle_tunnel_connection(client_sock: socket.socket) -> None:
from time import sleep
with client_sock, socket.create_connection(TLS_ADDRESS) as remote_socket: # type: ignore
while True:
try:
try:
client_sock.settimeout(TUNNEL_READ_WRITE_TIMEOUT)
buffer = client_sock.recv(1024)
remote_socket.sendall(buffer)
except socket.timeout: # pragma: no cover
pass
try:
remote_socket.settimeout(TUNNEL_READ_WRITE_TIMEOUT)
buffer = remote_socket.recv(1024)
while buffer:
client_sock.sendall(buffer[:1])
buffer = buffer[1:]
sleep(1)
except socket.timeout: # pragma: no cover
pass
except OSError:
break And run The But, with @tomchristie's implementation, the test will hang indefinitely.. |
@karosis88 |
Sorry for the delayed response. I don't think we'll have any problems with this because we're updating the timeout before each network operation to ensure that our network operations use the correct timeout. Please correct me if I am wrong. |
IMO we should follow implementation in other libraries, we should not apply any changes on sockets as it could have errors in other instances using the same socket. |
I'm not sure what you mean, but if there is an issue, you can describe it and write some simple reproducible code so we can work through it. |
I think in this PR the socket's timeout changes over time can cause problems in outer scopes where we set constant value for socket timeout. |
@encode/maintainers Anyone? :( |
I've been quietly reviewing this today, and I'm still of the "let's have However... I'm okay with shelving my judgement on that. I'm up for merging this and #722 once I'm able to test and verify. If anyone's able to provide a nice example of how to set up a (ideally python based, just because that'd work for me) proxy setup that we can verify TLS-in-TLS support against then that'd be helpful. Otherwise I'll dig into it myself and update the ticket when sorted. |
I've issued #786 which is a minimal-viable-product alternative to this pull request. It's a bit atypical in that I'm advocating for a bit of a relaxed approach to testing there, see the description for my motivation there. |
Resolved in #786. (Credit to @karosis88) |
Asyncio
andTrio
support TLS-in-TLS by default, but the standardssl
module does not, so this PR adds TLS-in-TLS implementation on top of the standardssl
module to fully support TLS-in-TLS backends.TLS-in-TLS implementations are required for all backends in order to support
HTTPS
proxies.Closes #721
Refs: #722 #714 #722
Todo