An experiment in upgradeable streams
Project description
asyncio-upgradeable-streams
An experiment in upgradeable streams.
Overview
An upgradeable stream starts life as a plain socket connection, but is capable of being "upgraded" to TLS. This is sometimes known as STARTTLS. Common examples of this are SMTP, LDAP, and HTTP proxy tunneling with CONNECT.
The asyncio library provides loop.start_tls for this purpose.
This project provides an implementation of asyncio.open_connection
and asyncio.start_server
with an extra optional boolean parameter upgradeadble
. When this is set, the writer
has a new method upgrade
which can be called to
upgrade the connection to TLS.
This was tested using Python 3.9.7 on Ubuntu Linux 21.10.
Issues
The solution makes use of private variables in the python standard library which may change at the will of the python library maintainer.
Installation
This can be installed with pip.
pip install jetblack-upgradeable-streams
Examples
The following examples can be found in the "demos" folder. They expect a Linux environment.
Client
Here is the client. A new argument upgradeable
has been added to the
open_connection
function, to enable upgrading. The writer
has a new method
upgrade
to upgrade the connection.
The client connects without TLS.
First the client sends "PING" to the server. The server should respond with "PONG".
Next the client sends "STARTTLS" to instruct the server to upgrade the
connection to TLS. The client then calls the upgrade
method on the writer
to
negotiate the secure connection. The method returns a new reader
and writer
.
Using the new writer the client sends "PING" to the server, this time over the encrypted stream. The server should respond with "PONG".
Finally the client sends "QUIT" to the server and closes the connection.
import asyncio
import socket
import ssl
from upgradeable_streams import open_connection
async def start_client():
ctx = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
cafile='/etc/ssl/certs/ca-certificates.crt'
)
host = socket.getfqdn()
print("Connect to server as upgradeable")
reader, writer = await open_connection(
host,
10001,
ssl=ctx,
upgradeable=True
)
print(f"The writer ssl context is {writer.get_extra_info('sslcontext')}")
print("Sending PING")
writer.write(b'PING\n')
response = (await reader.readline()).decode('utf-8').rstrip()
print(f"Received: {response}")
print("Sending STARTTLS")
writer.write(b'STARTTLS\n')
print("Upgrading the connection")
# Upgrade
reader, writer = await writer.upgrade()
print(f"The writer ssl context is {writer.get_extra_info('sslcontext')}")
print("Sending PING")
writer.write(b'PING\n')
response = (await reader.readline()).decode('utf-8').rstrip()
print(f"Received: {response}")
print("Sending QUIT")
writer.write(b'QUIT\n')
await writer.drain()
print("Closing client")
writer.close()
await writer.wait_closed()
print("Client disconnected")
if __name__ == '__main__':
asyncio.run(start_client())
Server
An extra argument upgradeable
has been added to the start_server
function
to enable upgrading to TLS. The writer
has a new method upgrade
to upgrade
the connection to TLS.
The server listens for client connections.
On receiving a connection it enters a read loop.
When the server receives "PING" it responds with "PONG".
When the server receives "QUIT" it closes the connection.
When the server receives "STARTTLS" it calls the upgrade
method on the writer
to negotiate the TLS connection. The method returns a new reader
and writer
.
The code expects certificate and key PEM files in "~/.keys/server.{crt,key}".
import asyncio
from asyncio import StreamReader, StreamWriter
from os.path import expanduser
import socket
import ssl
from typing import Union
from upgradeable_streams import start_server, UpgradeableStreamWriter
async def handle_client(
reader: StreamReader,
writer: Union[UpgradeableStreamWriter, StreamWriter]
) -> None:
print("Client connected")
while True:
request = (await reader.readline()).decode('utf8').rstrip()
print(f"Read '{request}'")
if request == 'QUIT':
break
elif request == 'PING':
print("Sending pong")
writer.write(b'PONG\n')
await writer.drain()
elif request == 'STARTTLS':
if not isinstance(writer, UpgradeableStreamWriter):
raise ValueError('writer not upgradeable')
print("Upgrading connection to TLS")
# Upgrade
reader, writer = await writer.upgrade()
print("Closing client")
writer.close()
await writer.wait_closed()
print("Client closed")
async def run_server():
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_verify_locations(cafile="/etc/ssl/certs/ca-certificates.crt")
ctx.load_cert_chain(
expanduser("~/.keys/server.crt"),
expanduser("~/.keys/server.key")
)
host = socket.getfqdn()
print("Starting server as upgradeable")
server = await start_server(
handle_client,
host,
10001,
ssl=ctx,
upgradeable=True
)
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(run_server())
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for jetblack-upgradeable-streams-0.2.0.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | 78c3bb59c7260ab7cc603653839a4e88055bc334b9b1c57309adc2137aafc50e |
|
MD5 | d2d981bc6508b1a56d897b87b25e2632 |
|
BLAKE2b-256 | cf7897aa04430123f4082277d398610b057c5e2b13870a7565e3643c9c82d026 |
Hashes for jetblack_upgradeable_streams-0.2.0-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | b4095db8832868ea9a4afd04b90877c3e6cf74664aa761673f7bbdc7a2f3462d |
|
MD5 | 755f8b707b3403bbda2831fd28d12dc8 |
|
BLAKE2b-256 | 91dd00caeb7482a9e265de1015f1a64fdf29cffac26e4025a3d06c79abb6afec |