Skip to content

Commit 2682130

Browse files
authored
feat: Support selective ssl/tls backend in rust-server to optionally remove openssl (#22825)
* feat: Support selective ssl/tls backend in rust-server to avoid always requiring openssl * feat: Switch default features so a user must select SSL backend * Further tweaks to rust-server HTTPS feature flagging
1 parent ad2044c commit 2682130

43 files changed

Lines changed: 1125 additions & 354 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/samples-rust-server.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ jobs:
6464
if cargo read-manifest | grep -q '"validate"'; then
6565
cargo build --features validate --all-targets
6666
fi
67+
# Test TLS features if they exist
68+
if cargo read-manifest | grep -q '"client-tls"'; then
69+
# Client without TLS (HTTP-only)
70+
cargo build --no-default-features --features=client --lib
71+
# Client with TLS (using native-tls)
72+
cargo build --no-default-features --features=client,client-tls --lib
73+
# Server without TLS
74+
cargo build --no-default-features --features=server --lib
75+
fi
6776
cargo fmt
6877
cargo test
6978
cargo clippy

modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ homepage = "{{.}}"
3232
{{/homePageUrl}}
3333

3434
[features]
35-
default = ["client", "server"]
35+
default = ["client", "server", "client-tls"]
3636
client = [
3737
{{#apiUsesMultipartFormData}}
3838
"multipart", "multipart/client", "swagger/multipart_form",
@@ -47,7 +47,19 @@ client = [
4747
"serde_ignored", "percent-encoding", {{^apiUsesByteArray}}"lazy_static", "regex",{{/apiUsesByteArray}}
4848
{{/hasCallbacks}}
4949
{{! Anything added to the list below, should probably be added to the callbacks list below }}
50-
"hyper", "percent-encoding", "hyper-util/http1", "hyper-util/http2", "hyper-openssl", "hyper-tls", "native-tls", "openssl", "url"
50+
"hyper", "percent-encoding", "hyper-util/http1", "hyper-util/http2", "url"
51+
]
52+
# TLS support - automatically selects backend based on target OS:
53+
# - macOS/Windows/iOS: native-tls via hyper-tls
54+
# - Other platforms: OpenSSL via hyper-openssl
55+
# Dependencies are in target-specific sections below
56+
client-tls = [
57+
"client",
58+
"dep:native-tls",
59+
"dep:hyper-tls",
60+
"dep:openssl",
61+
"dep:hyper-openssl",
62+
"swagger/tls"
5163
]
5264
server = [
5365
{{#apiUsesMultipartFormData}}
@@ -57,7 +69,7 @@ server = [
5769
"mime_multipart", "swagger/multipart_related",
5870
{{/apiUsesMultipartRelated}}
5971
{{#hasCallbacks}}
60-
"native-tls", "hyper-openssl", "hyper-tls", "openssl",
72+
"hyper-util/http1", "hyper-util/http2",
6173
{{/hasCallbacks}}
6274
{{! Anything added to the list below, should probably be added to the callbacks list above }}
6375
"serde_ignored", "hyper", "percent-encoding", "url",
@@ -74,20 +86,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
7486
mock = ["mockall"]
7587
validate = [{{^apiUsesByteArray}}"regex",{{/apiUsesByteArray}} "serde_valid", "swagger/serdevalid"]
7688

77-
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))'.dependencies]
78-
native-tls = { version = "0.2", optional = true }
79-
hyper-tls = { version = "0.6", optional = true }
80-
81-
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
82-
hyper-openssl = { version = "0.10", optional = true }
83-
openssl = { version = "0.10", optional = true }
84-
8589
[dependencies]
8690
# Common
8791
async-trait = "0.1.88"
8892
chrono = { version = "0.4", features = ["serde"] }
8993
futures = "0.3"
90-
swagger = { version = "7.0.0", features = ["serdejson", "server", "client", "tls"] }
94+
swagger = { version = "7.0.0", features = ["serdejson", "server", "client"] }
9195
headers = "0.4.0"
9296
log = "0.4.27"
9397

@@ -157,6 +161,17 @@ frunk_core = { version = "0.4.3", optional = true }
157161
frunk-enum-derive = { version = "0.3.0", optional = true }
158162
frunk-enum-core = { version = "0.3.0", optional = true }
159163

164+
# TLS dependencies - platform-specific backends
165+
# On macOS/Windows/iOS, use native-tls via hyper-tls
166+
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))'.dependencies]
167+
native-tls = { version = "0.2", optional = true }
168+
hyper-tls = { version = "0.6", optional = true }
169+
170+
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
171+
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
172+
openssl = { version = "0.10", optional = true }
173+
hyper-openssl = { version = "0.10", optional = true }
174+
160175
[dev-dependencies]
161176
always_send = "0.1.1"
162177
clap = "4.5"
@@ -169,8 +184,8 @@ pin-project = "1.1.10"
169184
jsonwebtoken = {version = "10.0.0", features = ["rust_crypto"]}
170185

171186
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dev-dependencies]
172-
tokio-openssl = "0.6"
173187
openssl = "0.10"
188+
tokio-openssl = "0.6"
174189

175190
[[example]]
176191
name = "client"

modules/openapi-generator/src/main/resources/rust-server/README.mustache

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ The generated library has a few optional features that can be activated through
126126
* `client`
127127
* This defaults to enabled and creates the basic skeleton of a client implementation based on hyper
128128
* The constructed client implements the API trait by making remote API call.
129+
* `client-tls`
130+
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
131+
- macOS/Windows/iOS: native-tls + hyper-tls
132+
- Linux/Unix/others: OpenSSL + hyper-openssl
129133
* `conversions`
130134
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
131135
* `cli`
@@ -134,6 +138,25 @@ The generated library has a few optional features that can be activated through
134138
* This defaults to disabled and allows JSON Schema validation of received data using `MakeService::set_validation` or `Service::set_validation`.
135139
* Note, enabling validation will have a performance penalty, especially if the API heavily uses regex based checks.
136140

141+
### HTTPS/TLS Support
142+
143+
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
144+
145+
```toml
146+
[dependencies]
147+
{{{packageName}}} = { version = "{{{packageVersion}}}", default-features = false, features = ["client", "server"] }
148+
```
149+
150+
**For server with callbacks that need HTTPS:**
151+
```toml
152+
[dependencies]
153+
{{{packageName}}} = { version = "{{{packageVersion}}}", features = ["server", "client-tls"] }
154+
```
155+
156+
The TLS backend is automatically selected based on your target platform:
157+
- **macOS, Windows, iOS**: Uses `native-tls` (system TLS libraries)
158+
- **Linux, Unix, other platforms**: Uses `openssl`
159+
137160
See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`.
138161

139162
## Documentation for API Endpoints

modules/openapi-generator/src/main/resources/rust-server/bin-cli.mustache

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,17 @@ struct Cli {
4545
server_address: String,
4646
4747
/// Path to the client private key if using client-side TLS authentication
48-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
48+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
4949
#[clap(long, requires_all(&["client_certificate", "server_certificate"]))]
5050
client_key: Option<String>,
5151
5252
/// Path to the client's public certificate associated with the private key
53-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
53+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
5454
#[clap(long, requires_all(&["client_key", "server_certificate"]))]
5555
client_certificate: Option<String>,
5656
5757
/// Path to CA certificate used to authenticate the server
58-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
58+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
5959
#[clap(long)]
6060
server_certificate: Option<String>,
6161
@@ -130,7 +130,8 @@ enum Operation {
130130
{{/apiInfo}}
131131
}
132132

133-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
133+
// On Linux/Unix with OpenSSL (client-tls feature), support certificate pinning and mutual TLS
134+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
134135
fn create_client(args: &Cli, context: ClientContext) -> Result<Box<dyn ApiNoContext<ClientContext>>> {
135136
if args.client_certificate.is_some() {
136137
debug!("Using mutual TLS");
@@ -156,8 +157,15 @@ fn create_client(args: &Cli, context: ClientContext) -> Result<Box<dyn ApiNoCont
156157
}
157158
}
158159

159-
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
160+
// On macOS/Windows/iOS or without client-tls feature, use simple client (no cert pinning/mutual TLS)
161+
#[cfg(any(
162+
not(feature = "client-tls"),
163+
all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios"))
164+
))]
160165
fn create_client(args: &Cli, context: ClientContext) -> Result<Box<dyn ApiNoContext<ClientContext>>> {
166+
// Client::try_new() automatically detects the URL scheme (http:// or https://)
167+
// and creates the appropriate client.
168+
// Note: Certificate pinning and mutual TLS are only available on Linux/Unix with OpenSSL
161169
let client =
162170
Client::try_new(&args.server_address).context("Failed to create HTTP(S) client")?;
163171
Ok(Box::new(client.with_context(context)))

modules/openapi-generator/src/main/resources/rust-server/client-mod.mustache

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,17 @@ impl<Connector, C> Client<
118118
}
119119
}
120120
121+
#[cfg(all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios")))]
122+
type HyperHttpsConnector = hyper_tls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
123+
124+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
125+
type HyperHttpsConnector = hyper_openssl::client::legacy::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
126+
121127
#[derive(Debug, Clone)]
122128
pub enum HyperClient {
123129
Http(hyper_util::client::legacy::Client<hyper_util::client::legacy::connect::HttpConnector, BoxBody<Bytes, Infallible>>),
124-
Https(hyper_util::client::legacy::Client<HttpsConnector, BoxBody<Bytes, Infallible>>),
130+
#[cfg(feature = "client-tls")]
131+
Https(hyper_util::client::legacy::Client<HyperHttpsConnector, BoxBody<Bytes, Infallible>>),
125132
}
126133
127134
impl Service<Request<BoxBody<Bytes, Infallible>>> for HyperClient {
@@ -132,7 +139,8 @@ impl Service<Request<BoxBody<Bytes, Infallible>>> for HyperClient {
132139
fn call(&self, req: Request<BoxBody<Bytes, Infallible>>) -> Self::Future {
133140
match self {
134141
HyperClient::Http(client) => client.request(req),
135-
HyperClient::Https(client) => client.request(req)
142+
#[cfg(feature = "client-tls")]
143+
HyperClient::Https(client) => client.request(req),
136144
}
137145
}
138146
}
@@ -158,11 +166,17 @@ impl<C> Client<DropContextService<HyperClient, C>, C> where
158166
"http" => {
159167
HyperClient::Http(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector.build()))
160168
},
169+
#[cfg(feature = "client-tls")]
170+
"https" => {
171+
let https_connector = connector
172+
.https()
173+
.build()
174+
.map_err(ClientInitError::SslError)?;
175+
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(https_connector))
176+
},
177+
#[cfg(not(feature = "client-tls"))]
161178
"https" => {
162-
let connector = connector.https()
163-
.build()
164-
.map_err(ClientInitError::SslError)?;
165-
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
179+
return Err(ClientInitError::TlsNotEnabled);
166180
},
167181
_ => {
168182
return Err(ClientInitError::InvalidScheme);
@@ -206,12 +220,13 @@ impl<C> Client<
206220
}
207221
}
208222

209-
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
223+
#[cfg(all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios")))]
210224
type HttpsConnector = hyper_tls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
211225

212-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
226+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
213227
type HttpsConnector = hyper_openssl::client::legacy::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
214228

229+
#[cfg(feature = "client-tls")]
215230
impl<C> Client<
216231
DropContextService<
217232
hyper_util::service::TowerToHyperService<
@@ -226,10 +241,11 @@ impl<C> Client<
226241
> where
227242
C: Clone + Send + Sync + 'static
228243
{
229-
/// Create a client with a TLS connection to the server
244+
/// Create a client with a TLS connection to the server.
230245
///
231246
/// # Arguments
232-
/// * `base_path` - base path of the client API, i.e. "<http://www.my-api-implementation.com>"
247+
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
248+
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
233249
pub fn try_new_https(base_path: &str) -> Result<Self, ClientInitError>
234250
{
235251
let https_connector = Connector::builder()
@@ -239,10 +255,24 @@ impl<C> Client<
239255
Self::try_new_with_connector(base_path, Some("https"), https_connector)
240256
}
241257

242-
/// Create a client with a TLS connection to the server using a pinned certificate
258+
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
243259
///
244260
/// # Arguments
245-
/// * `base_path` - base path of the client API, i.e. "<http://www.my-api-implementation.com>"
261+
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
262+
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
263+
pub fn try_new_https(base_path: &str) -> Result<Self, ClientInitError>
264+
{
265+
let https_connector = Connector::builder()
266+
.https()
267+
.build()
268+
.map_err(ClientInitError::SslError)?;
269+
Self::try_new_with_connector(base_path, Some("https"), https_connector)
270+
}
271+
272+
/// Create a client with a TLS connection to the server using a pinned certificate.
273+
///
274+
/// # Arguments
275+
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
246276
/// * `ca_certificate` - Path to CA certificate used to authenticate the server
247277
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
248278
pub fn try_new_https_pinned<CA>(
@@ -263,7 +293,7 @@ impl<C> Client<
263293
/// Create a client with a mutually authenticated TLS connection to the server.
264294
///
265295
/// # Arguments
266-
/// * `base_path` - base path of the client API, i.e. "<http://www.my-api-implementation.com>"
296+
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
267297
/// * `ca_certificate` - Path to CA certificate used to authenticate the server
268298
/// * `client_key` - Path to the client private key
269299
/// * `client_certificate` - Path to the client's public certificate associated with the private key
@@ -325,12 +355,15 @@ pub enum ClientInitError {
325355
/// Missing Hostname
326356
MissingHost,
327357
358+
/// HTTPS requested but TLS features not enabled
359+
TlsNotEnabled,
360+
328361
/// SSL Connection Error
329-
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
362+
#[cfg(all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios")))]
330363
SslError(native_tls::Error),
331364
332365
/// SSL Connection Error
333-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
366+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
334367
SslError(openssl::error::ErrorStack),
335368
}
336369

modules/openapi-generator/src/main/resources/rust-server/example-client-main.mustache

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,33 @@ fn main() {
115115
let context: ClientContext =
116116
swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default());
117117

118-
let mut client : Box<dyn ApiNoContext<ClientContext>> = if is_https {
119-
// Using Simple HTTPS
120-
let client = Box::new(Client::try_new_https(&base_url)
121-
.expect("Failed to create HTTPS client"));
122-
Box::new(client.with_context(context))
123-
} else {
124-
// Using HTTP
125-
let client = Box::new(Client::try_new_http(
126-
&base_url)
127-
.expect("Failed to create HTTP client"));
128-
Box::new(client.with_context(context))
118+
let mut client : Box<dyn ApiNoContext<ClientContext>> = {
119+
#[cfg(feature = "client-tls")]
120+
{
121+
if is_https {
122+
// Using HTTPS with native-tls
123+
let client = Box::new(Client::try_new_https(&base_url)
124+
.expect("Failed to create HTTPS client"));
125+
Box::new(client.with_context(context))
126+
} else {
127+
// Using HTTP
128+
let client = Box::new(Client::try_new_http(&base_url)
129+
.expect("Failed to create HTTP client"));
130+
Box::new(client.with_context(context))
131+
}
132+
}
133+
134+
#[cfg(not(feature = "client-tls"))]
135+
{
136+
if is_https {
137+
panic!("HTTPS requested but TLS support not enabled. \
138+
Enable the 'client-tls' feature to use HTTPS.");
139+
}
140+
// Using HTTP only
141+
let client = Box::new(Client::try_new_http(&base_url)
142+
.expect("Failed to create HTTP client"));
143+
Box::new(client.with_context(context))
144+
}
129145
};
130146

131147
let mut rt = tokio::runtime::Runtime::new().unwrap();

modules/openapi-generator/src/main/resources/rust-server/server-callbacks.mustache

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,25 @@ impl<C> Client<DropContextService<hyper_util::service::TowerToHyperService<hyper
9797
}
9898
}
9999

100-
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
100+
#[cfg(all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios")))]
101101
type HttpsConnector = hyper_tls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
102102

103-
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
103+
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
104104
type HttpsConnector = hyper_openssl::client::legacy::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
105105

106+
#[cfg(feature = "client-tls")]
106107
impl<C> Client<DropContextService<hyper_util::service::TowerToHyperService<hyper_util::client::legacy::Client<HttpsConnector, BoxBody<Bytes, Infallible>>>, C>, C> where
107108
C: Clone + Send + Sync + 'static
108109
{
109-
/// Create a client with a TLS connection to the server.
110+
/// Create a client with a TLS connection to the server using native-tls.
110111
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
111112
pub fn new_https() -> Result<Self, native_tls::Error>
112113
{
113114
let https_connector = Connector::builder().https().build()?;
114115
Ok(Self::new_with_connector(https_connector))
115116
}
116117

117-
/// Create a client with a TLS connection to the server.
118+
/// Create a client with a TLS connection to the server using OpenSSL.
118119
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
119120
pub fn new_https() -> Result<Self, openssl::error::ErrorStack>
120121
{

0 commit comments

Comments
 (0)