Connection lost before connect to HTTP Proxy #738
-
Good morning, I have the following code that makes a tunnel with a HTTP Proxy class HTTPProxyTunnel:
"""HTTP Proxy Tunner.
Attributes:
__proxy_data: Proxy Data
"""
def __init__(self, proxy_data: ProxyData) -> None:
"""HTTP Proxy Tunner.
Args:
proxy_data: Proxy Data
"""
self.__proxy_data: ProxyData = proxy_data
async def create_connection(
self,
protocol_factory: type[asyncio.BaseProtocol],
remote_host: str,
remote_port: int,
) -> tuple[asyncio.BaseTransport, asyncio.BaseProtocol]:
"""Create Connection for http proxy.
Args:
protocol_factory: Session Factory from create tunnel
remote_host: Host Address to connect
remote_port: Port to connect
Returns:
Connection with HTTP Proxy
Raises:
ConnectionError: Connection Error
"""
reader: asyncio.StreamReader
writer: asyncio.StreamWriter
print("CREATE CONNECTION - GO")
reader, writer = await asyncio.open_connection(self.__proxy_data.host, self.__proxy_data.port)
print("CREATE CONNECTION - DONE")
print("WRITE HTTP COMMAND - GO")
writer.write(
(
f"CONNECT {remote_host}:{remote_port} HTTP/1.1\r\n"
f"Host: {remote_host}:{remote_port}\r\n"
"Accept: */*\r\n"
"\r\n"
).encode("ascii")
)
print("WRITE HTTP COMMAND - DONE")
print("READ LINE - GO")
first_line: bytes = await reader.readline()
print(f"READ LINE - DONE {first_line!r}")
if not first_line.startswith(b"HTTP/1.1 200 "):
print("NO 200")
raise ConnectionError(f"Unexpected response: {first_line.decode('utf-8', errors='ignore')}") # noqa: TRY003
async for line in reader:
print(f"READ LINE - {line!r}")
if line == b"\r\n":
break
print("RETURN - GO")
transport: asyncio.WriteTransport = writer.transport
protocol: asyncio.BaseProtocol = protocol_factory()
transport.set_protocol(protocol)
protocol.connection_made(transport)
print("RETURN - DONE")
return transport, protocol When I run it, it fails, but it is after returning the asyncssh control.
If I remove the break when the reader finds it \r\n, I see that it connects to the server.
Have there been any changes that need to be applied to the tunnel? Or why is it disconnected? I can't find the problem |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 20 replies
-
I think there may be a race condition here, where the SSH version line is sometimes being delivered to the HTTP reader rather than to the new AsyncSSH protocol object. You might try adding a couple of lines such as: print("RETURN - GO")
transport: asyncio.WriteTransport = writer.transport
protocol: asyncio.BaseProtocol = protocol_factory()
transport.set_protocol(protocol)
buffered_data = await reader.read(1024)
protocol.connection_made(transport)
protocol.data_received(buffered_data)
print("RETURN - DONE")
return transport, protocol |
Beta Was this translation helpful? Give feedback.
-
I was testing with an HTTP proxy here as well (no SSL involved). If the connection to the remote host succeeds (which it should have if you got back a 200 OK response) and you're connecting to an SSH server, it should be writing its version string when you connect. So, even if there was no data buffered yet, something should be coming. Perhaps try a different order here: print("RETURN - GO")
transport: asyncio.WriteTransport = writer.transport
protocol: asyncio.BaseProtocol = protocol_factory()
buffered_data = await reader.read(1024)
transport.set_protocol(protocol)
protocol.connection_made(transport)
protocol.data_received(buffered_data)
print("RETURN - DONE")
return transport, protocol That way the read will definitely complete before switching the transport over to delivering new incoming bytes to the SSH protocol object. |
Beta Was this translation helpful? Give feedback.
-
Great! I was seeing slightly different behavior myself in 3.8, but I haven't had a chance to dig into that yet. Also, I found a problem with the second ordering I suggested. You might want to try the following instead: print("RETURN - GO")
buffered_data = await reader.read(1024)
transport: asyncio.WriteTransport = writer.transport
protocol: asyncio.BaseProtocol = protocol_factory()
transport.set_protocol(protocol)
protocol.connection_made(transport)
protocol.data_received(buffered_data)
print("RETURN - DONE")
return transport, protocol This makes sure all reads are done before switch to the new protocol object, and the set_protocol happens before the protocol object attempts to process any input or write any output. |
Beta Was this translation helpful? Give feedback.
-
I've been able to reproduce this here, with a simple HTTP CONNECT proxy I wrote in Python (so I can more easily see what it receives before the connection fails). It works properly for me for Python 3.7-3.10 and fails for Python 3.11 and later. In the case where it fails, it seems to send out the version line and the KEXINIT, which means the call to connection_made() on the new protocol must be getting made, as is the call to data_received() on the buffered data (incoming SSH version line). However, after the new transport and new protocol are returned, somehow the connection_lost() method is immediately called on the protocol. I'm still trying to figure out what the differences are between the 3.10 and 3.11 versions of asyncio that could explain this. |
Beta Was this translation helpful? Give feedback.
-
I leave here a message to make it easier to find. @ronf has discovered that as of python 3.11 the asyncio.StreamWriter closes the connection when destroyed. Here is an example of the implementation in case it is useful for someone in the future: import asyncio
import re
from pprint import pprint
import asyncssh
class HTTPProxyTunnel:
"""HTTP Proxy Tunner."""
def __init__(self, proxy_host: str, proxy_port: int) -> None:
"""HTTP Proxy Tunner.
Args:
proxy_host: Proxy Host
proxy_port: Proxy Port
"""
self.__proxy_host: str = proxy_host
self.__proxy_port: int = proxy_port
self.__reader: asyncio.StreamReader | None = None
self.__writer: asyncio.StreamWriter | None = None
async def create_connection(
self,
protocol_factory: Callable[[], asyncio.Protocol],
remote_host: str,
remote_port: int,
) -> tuple[asyncio.BaseTransport, asyncio.Protocol]:
"""Create Connection for http proxy.
Args:
protocol_factory: Session Factory from create tunnel
remote_host: Host Address to connect
remote_port: Port to connect
Returns:
Connection with HTTP Proxy
Raises:
ConnectionError: Connection Error
"""
self.__reader, self.__writer = await asyncio.open_connection(self.__proxy_host, self.__proxy_port)
self.__writer.write(
f"CONNECT {remote_host}:{remote_port} HTTP/1.1\r\nHost: {remote_host}:{remote_port}\r\n\r\n".encode("ascii")
)
first_line: bytes = await self.__reader.readline()
if not re.fullmatch(rb"HTTP/1.\d 200 .*\n", first_line):
raise ConnectionError(f"Unexpected response: {first_line.decode('utf-8', errors='ignore')}") # noqa: TRY003 EM102
async for line in self.__reader:
if line == b"\r\n":
break
buffered_data: bytes = await self.__reader.read(1024)
transport: asyncio.WriteTransport = self.__writer.transport
protocol: asyncio.Protocol = protocol_factory()
transport.set_protocol(protocol)
protocol.connection_made(transport)
protocol.data_received(buffered_data)
return transport, protocol
def close(self) -> None:
"""Close Connection for HTTP Proxy."""
if self.__writer:
self.__writer.close()
async def main():
tunnel = HTTPProxyTunnel("localhost", 3128)
executor = await asyncssh.connect(
host="HOST",
username="ROOT",
password="PASS",
tunnel=tunnel,
known_hosts=None,
)
result = await executor.run("uptime")
pprint(result)
executor.close()
await executor.wait_closed()
import logging
logging.basicConfig(level='DEBUG')
asyncssh.set_debug_level(3)
asyncio.run(main()) The differences with the code in the question are:
Thank you so much @ronf, without your help this wouldn't have been possible!!!! |
Beta Was this translation helpful? Give feedback.
I leave here a message to make it easier to find.
@ronf has discovered that as of python 3.11 the asyncio.StreamWriter closes the connection when destroyed.
So you have to change the implementation to make it work.
Here is an example of the implementation in case it is useful for someone in the future: