Skip to content

Commit b5d0a8f

Browse files
committed
is_connection_closed
1 parent e372507 commit b5d0a8f

File tree

1 file changed

+45
-2
lines changed

1 file changed

+45
-2
lines changed

crates/stdlib/src/ssl/compat.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ use rustls::server::ServerConfig;
2424
use rustls::server::ServerConnection;
2525
use rustls::sign::CertifiedKey;
2626
use rustpython_vm::builtins::PyBaseExceptionRef;
27-
use rustpython_vm::function::ArgBytesLike;
2827
use rustpython_vm::convert::IntoPyException;
28+
use rustpython_vm::function::ArgBytesLike;
2929
use rustpython_vm::{AsObject, PyObjectRef, PyPayload, PyResult, TryFromObject};
3030
use std::io::Read;
3131
use std::sync::{Arc, Once};
@@ -1528,6 +1528,29 @@ fn ssl_read_tls_records(
15281528
Ok(())
15291529
}
15301530

1531+
/// Check if an exception is a connection closed error
1532+
/// In SSL context, these errors indicate unexpected connection termination without proper TLS shutdown
1533+
fn is_connection_closed_error(exc: &PyBaseExceptionRef, vm: &VirtualMachine) -> bool {
1534+
use rustpython_vm::stdlib::errno::errors;
1535+
1536+
// Check for ConnectionAbortedError, ConnectionResetError (Python exception types)
1537+
if exc.fast_isinstance(vm.ctx.exceptions.connection_aborted_error)
1538+
|| exc.fast_isinstance(vm.ctx.exceptions.connection_reset_error)
1539+
{
1540+
return true;
1541+
}
1542+
1543+
// Also check OSError with specific errno values (ECONNABORTED, ECONNRESET)
1544+
if exc.fast_isinstance(vm.ctx.exceptions.os_error)
1545+
&& let Ok(errno) = exc.as_object().get_attr("errno", vm)
1546+
&& let Ok(errno_int) = errno.try_int(vm)
1547+
&& let Ok(errno_val) = errno_int.try_to_primitive::<i32>(vm)
1548+
{
1549+
return errno_val == errors::ECONNABORTED || errno_val == errors::ECONNRESET;
1550+
}
1551+
false
1552+
}
1553+
15311554
/// Ensure TLS data is available for reading
15321555
/// Returns the number of bytes read from the socket
15331556
fn ssl_ensure_data_available(
@@ -1563,7 +1586,27 @@ fn ssl_ensure_data_available(
15631586
// else: non-blocking socket (timeout=0) or blocking socket (timeout=None) - skip select
15641587
}
15651588

1566-
let data = socket.sock_recv(2048, vm).map_err(SslError::Py)?;
1589+
let data = match socket.sock_recv(2048, vm) {
1590+
Ok(data) => data,
1591+
Err(e) => {
1592+
// Before returning socket error, check if rustls already has a queued TLS alert
1593+
// This mirrors CPython/OpenSSL behavior: SSL errors take precedence over socket errors
1594+
// On Windows, TCP RST may arrive before we read the alert, but rustls may have
1595+
// already received and buffered the alert from a previous read
1596+
if let Err(rustls_err) = conn.process_new_packets() {
1597+
return Err(SslError::from_rustls(rustls_err));
1598+
}
1599+
// In SSL context, connection closed errors (ECONNABORTED, ECONNRESET) indicate
1600+
// unexpected connection termination - the peer closed without proper TLS shutdown.
1601+
// This is semantically equivalent to "EOF occurred in violation of protocol"
1602+
// because no close_notify alert was received.
1603+
// On Windows, TCP RST can arrive before we read the TLS alert, causing these errors.
1604+
if is_connection_closed_error(&e, vm) {
1605+
return Err(SslError::Eof);
1606+
}
1607+
return Err(SslError::Py(e));
1608+
}
1609+
};
15671610

15681611
// Get the size of received data
15691612
let bytes_read = data

0 commit comments

Comments
 (0)