Equivalent to OpenSSL's SSL_read() Reads application data from TLS connection. Automatically handles TLS record I/O as needed. = SSL_read_ex()
(
conn: &mut TlsConnection,
buf: &mut [u8],
socket: &PySSLSocket,
vm: &VirtualMachine,
)
| 1609 | /// |
| 1610 | /// = SSL_read_ex() |
| 1611 | pub(super) fn ssl_read( |
| 1612 | conn: &mut TlsConnection, |
| 1613 | buf: &mut [u8], |
| 1614 | socket: &PySSLSocket, |
| 1615 | vm: &VirtualMachine, |
| 1616 | ) -> SslResult<usize> { |
| 1617 | let is_bio = socket.is_bio_mode(); |
| 1618 | |
| 1619 | // Get socket timeout and calculate deadline (= _PyDeadline_Init) |
| 1620 | let deadline = if !is_bio { |
| 1621 | match socket.get_socket_timeout(vm).map_err(SslError::Py)? { |
| 1622 | Some(timeout) if !timeout.is_zero() => Some(std::time::Instant::now() + timeout), |
| 1623 | _ => None, // None = blocking (no deadline), Some(0) = non-blocking (handled below) |
| 1624 | } |
| 1625 | } else { |
| 1626 | None // BIO mode has no deadline |
| 1627 | }; |
| 1628 | |
| 1629 | // CRITICAL: Flush any pending TLS output before reading |
| 1630 | // This ensures data from previous write() calls is sent before we wait for response. |
| 1631 | // Without this, write() may leave data in pending_tls_output (if socket buffer was full), |
| 1632 | // and read() would timeout waiting for a response that the server never received. |
| 1633 | if !is_bio { |
| 1634 | socket |
| 1635 | .flush_pending_tls_output(vm, deadline) |
| 1636 | .map_err(SslError::Py)?; |
| 1637 | } |
| 1638 | |
| 1639 | // Loop to handle TLS records and post-handshake messages |
| 1640 | // Matches SSL_read behavior which loops until data is available |
| 1641 | // - CPython uses OpenSSL's SSL_read which loops on SSL_ERROR_WANT_READ/WANT_WRITE |
| 1642 | // - We use rustls which requires manual read_tls/process_new_packets loop |
| 1643 | // - No iteration limit: relies on deadline and blocking I/O |
| 1644 | // - Blocking sockets: sock_select() and recv() wait at kernel level (no CPU busy-wait) |
| 1645 | // - Non-blocking sockets: immediate return on first WantRead |
| 1646 | // - Deadline prevents timeout issues |
| 1647 | |
| 1648 | loop { |
| 1649 | // Check deadline |
| 1650 | if let Some(deadline) = deadline |
| 1651 | && std::time::Instant::now() >= deadline |
| 1652 | { |
| 1653 | // Timeout expired |
| 1654 | return Err(SslError::Timeout( |
| 1655 | "The read operation timed out".to_string(), |
| 1656 | )); |
| 1657 | } |
| 1658 | // Check if we need to read more TLS records BEFORE trying plaintext read |
| 1659 | // This ensures we don't miss data that's already been processed |
| 1660 | let needs_more_tls = conn.wants_read(); |
| 1661 | |
| 1662 | // Try to read plaintext from rustls buffer |
| 1663 | if let Some(n) = try_read_plaintext(conn, buf)? { |
| 1664 | if n == 0 { |
| 1665 | // EOF from TLS - close_notify received |
| 1666 | // Return ZeroReturn so Python raises SSLZeroReturnError |
| 1667 | return Err(SslError::ZeroReturn); |
| 1668 | } |
no test coverage detected