Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d739f22
Initial work on SSL
alanjds Nov 10, 2025
b5a8548
Provide SSL context on ssl tests
alanjds Nov 11, 2025
670803c
Barebones implementation of SSL Server via openssl
alanjds Nov 12, 2025
ff720c9
Lots of logs within the implementation
alanjds Nov 12, 2025
d213ca8
Almost working SSL Server via openssl
alanjds Nov 13, 2025
81e4f23
Use `requests` as an independent SSL client
alanjds Nov 13, 2025
1c8a84a
Fixed the content-length on tests
alanjds Nov 13, 2025
00738c9
Fix asyncio SSL server handling
alanjds Nov 13, 2025
f9eb27d
Explicitly close the conn
alanjds Nov 13, 2025
c3b0be2
Lots of debugs
alanjds Nov 15, 2025
0144456
Ensure delayed closing
alanjds Nov 15, 2025
ed28ab9
Connect to localhost
alanjds Nov 15, 2025
9ff6c79
A requests test that uses compatible suites
alanjds Nov 15, 2025
2bba8e8
Handle SSL handshakes without corruption
alanjds Nov 15, 2025
23f47b2
Refactor the tests
alanjds Nov 15, 2025
8bdb9a4
OpenSSL Client as test canary
alanjds Nov 15, 2025
c6d7ac1
Run all tests with both asyncio and rloop
alanjds Nov 15, 2025
feda1e1
New debugs & test fixes
alanjds Nov 16, 2025
fee950b
Attempts to close the SSL connection
alanjds Nov 16, 2025
44ab233
Verbose test log
alanjds Nov 17, 2025
57f838e
Fix the Callback exception
alanjds Nov 17, 2025
3ff3588
Automate pytest options
alanjds Nov 17, 2025
6fef69a
Wait the TCP buffer to be empty before sending the TLS close
alanjds Nov 17, 2025
1ff5982
Proper logs: We are no more applying any version fix
alanjds Nov 17, 2025
c3cd4c9
Do not double-borrow the same RefCell
alanjds Nov 17, 2025
6da087d
Callback with None if the transport was already closed
alanjds Nov 17, 2025
9e94404
SSL tests passing _almost always_. Needs investigation
alanjds Nov 17, 2025
91f0e0e
Force TLS 1.2 for now.
alanjds Nov 19, 2025
f8c7ffb
Use rustls::Connection directly as TLSConnection
alanjds Nov 19, 2025
004a195
Packages cleanup
alanjds Nov 24, 2025
fdd334b
RLOOP_TLS_VERSION selects the TLS compatibility for RLoop
alanjds Nov 27, 2025
afd9878
Close TLS immediately for both TLS 1.2 and 1.3
alanjds Nov 27, 2025
a7306a1
Minor SSL handling updates and debug
alanjds Nov 29, 2025
c8c859a
Tests with easier to spot tech roles
alanjds Dec 5, 2025
8eefdc0
Cargo update
alanjds Dec 5, 2025
900381d
RLOOP_TLS_VERSION can be only '1.2' or '1.3'
alanjds Dec 5, 2025
d1ebe6a
Revert some simplification
alanjds Dec 5, 2025
60eead1
Reduce diff with upstream
alanjds Dec 5, 2025
208112b
Use Arc directly. Get smaller lines
alanjds Dec 5, 2025
c71576e
Remove dup test
alanjds Dec 6, 2025
07c0b5a
Lint
alanjds Dec 6, 2025
14050f4
Handle the error correctly
alanjds Dec 6, 2025
9c505fc
RLOOP_SSL_CLOSE_TIMEOUT defaults to 1 sec
alanjds Dec 7, 2025
d13c454
Trying to fix the Asynctio protocol handling
alanjds Dec 7, 2025
3f44c5a
TLS versions are 1.2, 1.2+ and 1.3. Defaults to 1.2
alanjds Dec 7, 2025
8c07a1c
Test TLS versions '' (default), 1.2, 1.2+ and 1.3
alanjds Dec 7, 2025
9f6f817
Comments cleanup
alanjds Dec 7, 2025
85304f3
XFAIL all TLS 1.2+ with RLoop
alanjds Dec 8, 2025
5346388
Test openssl s_client with -bugs and -comp params too
alanjds Dec 8, 2025
2171545
`requests` is used on SSL tests
alanjds Dec 10, 2025
f9549bc
Fix faling tests for Python 3.14+ and macOS
alanjds Dec 10, 2025
8338527
Prefer single-quote strings
alanjds Dec 10, 2025
1cd7a40
Cleaner use of multiprocessing
alanjds Dec 10, 2025
2282cba
Mark the requests test as flaky on TLS 1.2+
alanjds Dec 10, 2025
66f33a3
Handle real lint issues
alanjds Dec 10, 2025
80d56d0
Ignore fake lint issues
alanjds Dec 10, 2025
bc1fc9d
openssl client need `shell=True`
alanjds Dec 10, 2025
d6f5750
SSL test tries another port is the current is in use
alanjds Dec 10, 2025
152e658
Lint
alanjds Dec 10, 2025
4838794
Clippy lint
alanjds Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
638 changes: 602 additions & 36 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
anyhow = "=1.0"
env_logger = "0.11"
log = "0.4"
mio = { version = "=1.0", features = ["net", "os-ext", "os-poll"] }
papaya = "=0.2"
pyo3 = { version = "=0.26", features = ["anyhow", "extension-module", "generate-import-lib"] }
rcgen = "=0.13"
rustls = { version = "0.23", features = ["std"] }
rustls-pemfile = "2.0"
socket2 = { version = "=0.6", features = ["all"] }

[target.'cfg(unix)'.dependencies]
Expand Down
21 changes: 21 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ lint = [
test = [
'pytest~=8.3',
'pytest-asyncio~=0.26',
'pytest-timeout~=2.4',
'requests~=2.32',
]

all = [
Expand Down Expand Up @@ -105,5 +107,24 @@ known-first-party = ['rloop', 'tests']
[tool.pytest.ini_options]
asyncio_mode = 'auto'

log_cli = true
log_cli_level = "INFO"

doctest_optionflags = [
"NORMALIZE_WHITESPACE",
"ALLOW_UNICODE",
"ALLOW_BYTES",
"NUMBER",
]
addopts = [
"--continue-on-collection-errors",
"--ignore-glob=*_BACKUP_*",
"-v",
"-rxX",
"--doctest-modules",
"--doctest-ignore-import-errors",
]


[tool.uv]
package = false
37 changes: 13 additions & 24 deletions rloop/loop.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio as __asyncio
import errno
import logging
import os
import signal
import socket
Expand Down Expand Up @@ -32,6 +33,9 @@
from .utils import _can_use_pidfd, _HAS_IPv6, _interleave_addrinfos, _ipaddr_info, _noop, _set_reuseport


logger = logging.getLogger(__name__)


class RLoop(__BaseLoop, __asyncio.AbstractEventLoop):
def __init__(self):
super().__init__()
Expand Down Expand Up @@ -316,10 +320,6 @@ async def create_connection(
interleave=None,
all_errors=False,
):
# TODO
if ssl:
raise NotImplementedError

if server_hostname is not None and not ssl:
raise ValueError('server_hostname is only meaningful with ssl')

Expand Down Expand Up @@ -385,7 +385,7 @@ async def create_connection(
happy_eyeballs_delay,
loop=self,
)
)[0] # can't use sock, _, _ as it keeks a reference to exceptions
)[0] # can't use sock, _, _ as it keeps a reference to exceptions

if sock is None:
exceptions = [exc for sub in exceptions for exc in sub]
Expand Down Expand Up @@ -421,16 +421,8 @@ async def create_connection(
rsock = (sock.fileno(), sock.family)
sock.detach()

# TODO: ssl
transport, protocol = self._tcp_conn(rsock, protocol_factory)
# transport, protocol = await self._create_connection_transport(
# sock,
# protocol_factory,
# ssl,
# server_hostname,
# ssl_handshake_timeout=ssl_handshake_timeout,
# ssl_shutdown_timeout=ssl_shutdown_timeout,
# )
logger.debug('Creating %s connection', 'SSL' if ssl else 'TCP')
transport, protocol = self._tcp_conn(rsock, protocol_factory, ssl, server_hostname if ssl else None)

return transport, protocol

Expand Down Expand Up @@ -491,10 +483,6 @@ async def create_server(
ssl_shutdown_timeout=None,
start_serving=True,
):
# TODO
if ssl:
raise NotImplementedError

if isinstance(ssl, bool):
raise TypeError('ssl argument must be an SSLContext or None')

Expand Down Expand Up @@ -580,11 +568,12 @@ async def create_server(
rsocks.append((sock.fileno(), sock.family))
sock.detach()

# TODO: ssl
# server = self._tcp_server(sockets, rsocks, protocol_factory, backlog,
# ssl, ssl_handshake_timeout,
# ssl_shutdown_timeout)
server = Server(self._tcp_server(sockets, rsocks, protocol_factory, backlog))
if ssl:
logger.debug('Creating SSL server')
server = Server(self._tcp_server_ssl(sockets, rsocks, protocol_factory, backlog, ssl))
else:
logger.debug('Creating TCP server')
server = Server(self._tcp_server(sockets, rsocks, protocol_factory, backlog))

if start_serving:
await server.start_serving()
Expand Down
51 changes: 45 additions & 6 deletions src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ impl EventLoop {
let guard_poll = self.io.lock().unwrap();
_ = guard_poll.registry().reregister(&mut source, token, interests);
}
self.wake(); // interest changed
return IOHandle::TCPStream(interests);
}
unreachable!()
Expand All @@ -357,6 +358,7 @@ impl EventLoop {
let guard_poll = self.io.lock().unwrap();
_ = guard_poll.registry().register(&mut source, token, interest);
}
self.wake(); // interest registered
IOHandle::TCPStream(interest)
},
);
Expand Down Expand Up @@ -396,16 +398,20 @@ impl EventLoop {

#[inline(always)]
pub(crate) fn tcp_stream_close(&self, py: Python, fd: usize) {
if let Some(transport) = self.tcp_transports.pin().remove(&fd)
&& let Some(lfd) = transport.borrow(py).lfd
{
self.tcp_lstreams.pin().get(&lfd).map(|v| v.pin().remove(&fd));
if let Some(transport) = self.tcp_transports.pin().remove(&fd) {
// ensure TCPTransport::close() called, as it sends TLS close
if transport.borrow(py).is_tls() {
transport.borrow(py).close(py);
}
if let Some(lfd) = transport.borrow(py).lfd {
self.tcp_lstreams.pin().get(&lfd).map(|v| v.pin().remove(&fd));
}
}
}

#[inline(always)]
pub(crate) fn get_tcp_transport(&self, fd: usize, py: Python) -> Py<TCPTransport> {
self.tcp_transports.pin().get(&fd).unwrap().clone_ref(py)
pub(crate) fn get_tcp_transport(&self, fd: usize, py: Python) -> Option<Py<TCPTransport>> {
self.tcp_transports.pin().get(&fd).map(|t| t.clone_ref(py))
}

pub(crate) fn with_tcp_listener_streams<T>(&self, fd: usize, func: T)
Expand Down Expand Up @@ -1171,10 +1177,19 @@ impl EventLoop {
py: Python,
sock: (i32, i32),
protocol_factory: Py<PyAny>,
ssl_context: Option<Py<PyAny>>,
server_hostname: Option<String>,
) -> PyResult<(Py<TCPTransport>, Py<PyAny>)> {
let rself = pyself.get();
let transport = TCPTransport::from_py(py, &pyself, sock, protocol_factory);
let fd = transport.fd;

if let (Some(ssl_context), Some(server_hostname)) = (ssl_context, server_hostname) {
// Initialize TLS client connection
let ssl_config = crate::ssl::create_ssl_client_config_from_context(&ssl_context.bind(py))?;
transport.initialize_tls_client(ssl_config, server_hostname);
}

let pytransport = Py::new(py, transport)?;
let proto = TCPTransport::attach(&pytransport, py)?;
rself.tcp_transports.pin().insert(fd, pytransport.clone_ref(py));
Expand All @@ -1198,6 +1213,30 @@ impl EventLoop {
Py::new(py, server)
}

fn _tcp_server_ssl(
pyself: Py<Self>,
py: Python,
socks: Py<PyAny>,
rsocks: Vec<(i32, i32)>,
protocol_factory: Py<PyAny>,
backlog: i32,
ssl_context: Py<PyAny>,
) -> PyResult<Py<Server>> {
let ssl_config = crate::ssl::create_ssl_config_from_context(&ssl_context.bind(py))?;
let mut servers = Vec::new();
for (fd, family) in rsocks {
servers.push(TCPServer::from_fd_ssl(
fd,
family,
backlog,
protocol_factory.clone_ref(py),
ssl_config.clone(),
));
}
let server = Server::tcp(pyself.clone_ref(py), socks, servers);
Py::new(py, server)
}

fn _tcp_stream_bound(&self, fd: usize) -> bool {
self.tcp_transports.pin().contains_key(&fd)
}
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod log;
mod py;
mod server;
mod sock;
mod ssl;
mod tcp;
mod time;
mod udp;
Expand All @@ -24,11 +25,15 @@ pub(crate) fn get_lib_version() -> &'static str {

#[pymodule(gil_used = false)]
fn _rloop(_py: Python, module: &Bound<PyModule>) -> PyResult<()> {
// Configure logs via RUST_LOG= (default to INFO)
env_logger::init();

module.add("__version__", get_lib_version())?;

event_loop::init_pymodule(module)?;
handles::init_pymodule(module)?;
server::init_pymodule(module)?;
ssl::init_pymodule(module)?;

Ok(())
}
Loading