Skip to content

Commit 92127da

Browse files
fantixBerrysoft
andauthored
feat(tls): support py-dynamic-openssl (#700)
* feat(tls): support py-dynamic-openssl compio-py-dynamic-openssl is a specialized compio-tls implementation dedicated for the use of compio-py, which loads OpenSSL >= 1.1.1 from an already-loaded library dynamically at runtime, so it is recommended against being used for general purposes. * fix(tls): use real mod * fix(tls): nit on imports * ci(tls): test py_ossl * fix(tls): make pyo3 optional * ci: test py-dynamic-openssl * fix(tls,py): bump compio-py-dynamic-openssl to 0.5.0 0.4.0 reexports pyo3 and honors ctx.check_hostname. 0.5.0 fixes Windows support. --------- Co-authored-by: 王宇逸 <Strawberry_Str@hotmail.com>
1 parent 8cc8d93 commit 92127da

File tree

9 files changed

+298
-11
lines changed

9 files changed

+298
-11
lines changed

.github/workflows/ci_test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ jobs:
4545
no_default_features: true
4646
- os: 'ubuntu-22.04'
4747
features: 'codec-serde-json'
48+
- os: 'ubuntu-22.04'
49+
features: 'native-tls,ring,py-dynamic-openssl'
4850
- os: 'windows-latest'
4951
target: 'x86_64-pc-windows-msvc'
5052
- os: 'windows-latest'
@@ -57,8 +59,13 @@ jobs:
5759
- os: 'windows-latest'
5860
target: 'x86_64-pc-windows-msvc'
5961
features: 'iocp-wait-packet'
62+
- os: 'windows-latest'
63+
target: 'x86_64-pc-windows-msvc'
64+
features: 'native-tls,ring,py-dynamic-openssl'
6065
- os: 'macos-14'
6166
- os: 'macos-15'
67+
- os: 'macos-15'
68+
features: 'native-tls,ring,py-dynamic-openssl'
6269
steps:
6370
- uses: actions/checkout@v4
6471
- name: Setup Rust Toolchain

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ futures-rustls = { version = "0.26.0", default-features = false }
5050
futures-util = "0.3.29"
5151
libc = "0.2.175"
5252
native-tls = "0.2.13"
53+
compio-py-dynamic-openssl = "0.5.0"
5354
nix = "0.31.1"
5455
once_cell = "1.18.0"
5556
os_pipe = "1.1.4"

compio-tls/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ rustls = { workspace = true, default-features = false, optional = true, features
2323
"std",
2424
"tls12",
2525
] }
26+
compio-py-dynamic-openssl = { workspace = true, optional = true }
2627

2728
futures-rustls = { workspace = true, default-features = false, optional = true, features = [
2829
"logging",
@@ -47,6 +48,7 @@ default = ["native-tls"]
4748
all = ["native-tls", "rustls"]
4849
rustls = ["dep:rustls", "dep:futures-rustls", "dep:futures-util"]
4950
native-tls-vendored = ["native-tls/vendored"]
51+
py-dynamic-openssl = ["dep:compio-py-dynamic-openssl"]
5052

5153
ring = ["rustls", "rustls/ring", "futures-rustls/ring"]
5254

compio-tls/src/adapter.rs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ enum TlsConnectorInner {
1313
NativeTls(native_tls::TlsConnector),
1414
#[cfg(feature = "rustls")]
1515
Rustls(futures_rustls::TlsConnector),
16-
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
16+
#[cfg(feature = "py-dynamic-openssl")]
17+
PyDynamicOpenSsl(compio_py_dynamic_openssl::SSLContext),
18+
#[cfg(not(any(
19+
feature = "native-tls",
20+
feature = "rustls",
21+
feature = "py-dynamic-openssl"
22+
)))]
1723
None(std::convert::Infallible),
1824
}
1925

@@ -24,7 +30,13 @@ impl Debug for TlsConnectorInner {
2430
Self::NativeTls(_) => f.debug_tuple("NativeTls").finish(),
2531
#[cfg(feature = "rustls")]
2632
Self::Rustls(_) => f.debug_tuple("Rustls").finish(),
27-
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
33+
#[cfg(feature = "py-dynamic-openssl")]
34+
Self::PyDynamicOpenSsl(_) => f.debug_tuple("PyDynamicOpenSsl").finish(),
35+
#[cfg(not(any(
36+
feature = "native-tls",
37+
feature = "rustls",
38+
feature = "py-dynamic-openssl"
39+
)))]
2840
Self::None(f) => match *f {},
2941
}
3042
}
@@ -49,6 +61,14 @@ impl From<std::sync::Arc<rustls::ClientConfig>> for TlsConnector {
4961
}
5062
}
5163

64+
#[cfg(feature = "py-dynamic-openssl")]
65+
#[doc(hidden)]
66+
impl From<compio_py_dynamic_openssl::SSLContext> for TlsConnector {
67+
fn from(value: compio_py_dynamic_openssl::SSLContext) -> Self {
68+
Self(TlsConnectorInner::PyDynamicOpenSsl(value))
69+
}
70+
}
71+
5272
impl TlsConnector {
5373
/// Connects the provided stream with this connector, assuming the provided
5474
/// domain.
@@ -82,7 +102,15 @@ impl TlsConnector {
82102
.await?;
83103
Ok(TlsStream::from(client))
84104
}
85-
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
105+
#[cfg(feature = "py-dynamic-openssl")]
106+
TlsConnectorInner::PyDynamicOpenSsl(c) => {
107+
crate::py_ossl::handshake(c.connect(domain, SyncStream::new(stream))).await
108+
}
109+
#[cfg(not(any(
110+
feature = "native-tls",
111+
feature = "rustls",
112+
feature = "py-dynamic-openssl"
113+
)))]
86114
TlsConnectorInner::None(f) => match *f {},
87115
}
88116
}
@@ -94,7 +122,13 @@ enum TlsAcceptorInner {
94122
NativeTls(native_tls::TlsAcceptor),
95123
#[cfg(feature = "rustls")]
96124
Rustls(futures_rustls::TlsAcceptor),
97-
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
125+
#[cfg(feature = "py-dynamic-openssl")]
126+
PyDynamicOpenSsl(compio_py_dynamic_openssl::SSLContext),
127+
#[cfg(not(any(
128+
feature = "native-tls",
129+
feature = "rustls",
130+
feature = "py-dynamic-openssl"
131+
)))]
98132
None(std::convert::Infallible),
99133
}
100134

@@ -120,6 +154,13 @@ impl From<std::sync::Arc<rustls::ServerConfig>> for TlsAcceptor {
120154
}
121155
}
122156

157+
#[cfg(feature = "py-dynamic-openssl")]
158+
impl From<compio_py_dynamic_openssl::SSLContext> for TlsAcceptor {
159+
fn from(value: compio_py_dynamic_openssl::SSLContext) -> Self {
160+
Self(TlsAcceptorInner::PyDynamicOpenSsl(value))
161+
}
162+
}
163+
123164
impl TlsAcceptor {
124165
/// Accepts a new client connection with the provided stream.
125166
///
@@ -145,7 +186,15 @@ impl TlsAcceptor {
145186
let server = c.accept(AsyncStream::new(stream)).await?;
146187
Ok(TlsStream::from(server))
147188
}
148-
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
189+
#[cfg(feature = "py-dynamic-openssl")]
190+
TlsAcceptorInner::PyDynamicOpenSsl(a) => {
191+
crate::py_ossl::handshake(a.accept(SyncStream::new(stream))).await
192+
}
193+
#[cfg(not(any(
194+
feature = "native-tls",
195+
feature = "rustls",
196+
feature = "py-dynamic-openssl"
197+
)))]
149198
TlsAcceptorInner::None(f) => match *f {},
150199
}
151200
}

compio-tls/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#![cfg_attr(feature = "read_buf", feature(read_buf, core_io_borrowed_buf))]
1212
#![cfg_attr(docsrs, feature(doc_cfg))]
1313

14+
#[cfg(feature = "py-dynamic-openssl")]
15+
pub use compio_py_dynamic_openssl as py_dynamic_openssl;
1416
#[cfg(feature = "native-tls")]
1517
pub use native_tls;
1618
#[cfg(feature = "rustls")]
@@ -28,3 +30,7 @@ pub use stream::*;
2830
mod rtls;
2931
#[cfg(feature = "rustls")]
3032
pub use rtls::*;
33+
34+
#[cfg(feature = "py-dynamic-openssl")]
35+
#[doc(hidden)]
36+
mod py_ossl;

compio-tls/src/py_ossl.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::{borrow::Cow, io};
2+
3+
use compio_io::{AsyncRead, AsyncWrite, compat::SyncStream};
4+
use compio_py_dynamic_openssl::ssl::{Error, ErrorCode, HandshakeError, ShutdownResult, SslStream};
5+
6+
use crate::TlsStream;
7+
8+
pub(crate) async fn handshake<S: AsyncRead + AsyncWrite>(
9+
mut res: Result<SslStream<SyncStream<S>>, HandshakeError<SyncStream<S>>>,
10+
) -> io::Result<TlsStream<S>> {
11+
loop {
12+
match res {
13+
Ok(mut s) => {
14+
let inner = s.get_mut();
15+
if inner.has_pending_write() {
16+
inner.flush_write_buf().await?;
17+
}
18+
return Ok(TlsStream::from(s));
19+
}
20+
Err(e) => match e {
21+
HandshakeError::SetupFailure(e) => return Err(io::Error::other(e)),
22+
HandshakeError::Failure(mid_stream) => {
23+
return Err(io::Error::other(mid_stream.into_error()));
24+
}
25+
HandshakeError::WouldBlock(mut mid_stream) => {
26+
let s = mid_stream.get_mut();
27+
if s.has_pending_write() {
28+
s.flush_write_buf().await?;
29+
} else {
30+
s.fill_read_buf().await?;
31+
}
32+
res = mid_stream.handshake();
33+
}
34+
},
35+
}
36+
}
37+
}
38+
39+
enum DriveResult<T> {
40+
WantRead,
41+
WantWrite,
42+
Ready(io::Result<T>),
43+
}
44+
45+
impl<T> From<Error> for DriveResult<T> {
46+
fn from(e: Error) -> Self {
47+
match e.code() {
48+
ErrorCode::WANT_READ => DriveResult::WantRead,
49+
ErrorCode::WANT_WRITE => DriveResult::WantWrite,
50+
_ => DriveResult::Ready(Err(e.into_io_error().unwrap_or_else(io::Error::other))),
51+
}
52+
}
53+
}
54+
55+
impl<T> From<Result<T, Error>> for DriveResult<T> {
56+
fn from(res: Result<T, Error>) -> Self {
57+
match res {
58+
Ok(t) => DriveResult::Ready(Ok(t)),
59+
Err(e) => e.into(),
60+
}
61+
}
62+
}
63+
64+
#[inline]
65+
async fn drive<S, F, T>(s: &mut SslStream<SyncStream<S>>, mut f: F) -> io::Result<T>
66+
where
67+
S: AsyncRead + AsyncWrite,
68+
F: FnMut(&mut SslStream<SyncStream<S>>) -> DriveResult<T>,
69+
{
70+
loop {
71+
let res = f(s);
72+
let s = s.get_mut();
73+
if s.has_pending_write() {
74+
s.flush_write_buf().await?;
75+
}
76+
match res {
77+
DriveResult::Ready(res) => break res,
78+
DriveResult::WantRead => _ = s.fill_read_buf().await?,
79+
DriveResult::WantWrite => {}
80+
}
81+
}
82+
}
83+
84+
pub(crate) fn negotiated_alpn<S>(s: &SslStream<SyncStream<S>>) -> Option<Cow<'_, [u8]>> {
85+
s.ssl()
86+
.selected_alpn_protocol()
87+
.map(|alpn| alpn.to_vec())
88+
.map(Cow::from)
89+
}
90+
91+
pub(crate) async fn read<S>(s: &mut SslStream<SyncStream<S>>, slice: &mut [u8]) -> io::Result<usize>
92+
where
93+
S: AsyncRead + AsyncWrite,
94+
{
95+
drive(s, |s| match s.ssl_read(slice) {
96+
Ok(n) => DriveResult::Ready(Ok(n)),
97+
Err(e) => match e.code() {
98+
ErrorCode::ZERO_RETURN => DriveResult::Ready(Ok(0)),
99+
ErrorCode::SYSCALL if e.io_error().is_none() => DriveResult::Ready(Ok(0)),
100+
_ => e.into(),
101+
},
102+
})
103+
.await
104+
}
105+
106+
pub(crate) async fn write<S>(s: &mut SslStream<SyncStream<S>>, slice: &[u8]) -> io::Result<usize>
107+
where
108+
S: AsyncRead + AsyncWrite,
109+
{
110+
drive(s, |s| s.ssl_write(slice).into()).await
111+
}
112+
113+
pub(crate) async fn shutdown<S>(s: &mut SslStream<SyncStream<S>>) -> io::Result<()>
114+
where
115+
S: AsyncRead + AsyncWrite,
116+
{
117+
let res = drive(s, |s| match s.shutdown() {
118+
Ok(res) => DriveResult::Ready(Ok(res)),
119+
Err(e) => {
120+
if e.code() == ErrorCode::ZERO_RETURN {
121+
DriveResult::Ready(Ok(ShutdownResult::Received))
122+
} else {
123+
e.into()
124+
}
125+
}
126+
})
127+
.await?;
128+
if let Err(e) = s.get_mut().get_mut().shutdown().await
129+
&& e.kind() != io::ErrorKind::NotConnected
130+
{
131+
return Err(e);
132+
}
133+
match res {
134+
// If close_notify has been sent but the peer has not responded with
135+
// close_notify, we let the caller know by returning Err(WouldBlock).
136+
// This behavior is different from the others as a Python-only hack.
137+
ShutdownResult::Sent => Err(io::Error::new(
138+
io::ErrorKind::WouldBlock,
139+
"close_notify sent",
140+
)),
141+
ShutdownResult::Received => Ok(()),
142+
}
143+
}

0 commit comments

Comments
 (0)