Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .github/workflows/samples-rust-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ jobs:
if cargo read-manifest | grep -q '"validate"'; then
cargo build --features validate --all-targets
fi
# Test TLS features if they exist
if cargo read-manifest | grep -q '"client-tls"'; then
# Client without TLS (HTTP-only)
cargo build --no-default-features --features=client --lib
# Client with TLS (using native-tls)
cargo build --no-default-features --features=client,client-tls --lib
# Server without TLS
cargo build --no-default-features --features=server --lib
fi
cargo fmt
cargo test
cargo clippy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,19 @@ client = [
"serde_ignored", "percent-encoding", {{^apiUsesByteArray}}"lazy_static", "regex",{{/apiUsesByteArray}}
{{/hasCallbacks}}
{{! Anything added to the list below, should probably be added to the callbacks list below }}
"hyper", "percent-encoding", "hyper-util/http1", "hyper-util/http2", "hyper-openssl", "hyper-tls", "native-tls", "openssl", "url"
"hyper", "percent-encoding", "hyper-util/http1", "hyper-util/http2", "url"
]
# TLS support - automatically selects backend based on target OS:
# - macOS/Windows/iOS: native-tls via hyper-tls
# - Other platforms: OpenSSL via hyper-openssl
# Dependencies are in target-specific sections below
client-tls = [
"client",
"dep:native-tls",
"dep:hyper-tls",
"dep:openssl",
"dep:hyper-openssl",
"swagger/tls"
]
server = [
{{#apiUsesMultipartFormData}}
Expand All @@ -57,7 +69,6 @@ server = [
"mime_multipart", "swagger/multipart_related",
{{/apiUsesMultipartRelated}}
{{#hasCallbacks}}
"native-tls", "hyper-openssl", "hyper-tls", "openssl",
{{/hasCallbacks}}
{{! Anything added to the list below, should probably be added to the callbacks list above }}
"serde_ignored", "hyper", "percent-encoding", "url",
Expand All @@ -74,20 +85,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
mock = ["mockall"]
validate = [{{^apiUsesByteArray}}"regex",{{/apiUsesByteArray}} "serde_valid", "swagger/serdevalid"]

[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))'.dependencies]
native-tls = { version = "0.2", optional = true }
hyper-tls = { version = "0.6", optional = true }

[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
hyper-openssl = { version = "0.10", optional = true }
openssl = { version = "0.10", optional = true }

[dependencies]
# Common
async-trait = "0.1.88"
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
swagger = { version = "7.0.0", features = ["serdejson", "server", "client", "tls"] }
swagger = { version = "7.0.0", features = ["serdejson", "server", "client"] }
headers = "0.4.0"
log = "0.4.27"

Expand Down Expand Up @@ -133,6 +136,12 @@ serde_urlencoded = { version = "0.7.1", optional = true }
{{/usesUrlEncodedForm}}
tower-service = "0.3.3"

# TLS support - all listed here, actual usage determined by cfg attributes in code
native-tls = { version = "0.2", optional = true }
hyper-tls = { version = "0.6", optional = true }
openssl = { version = "0.10", optional = true }
hyper-openssl = { version = "0.10", optional = true }

# Server, and client callback-specific
{{^apiUsesByteArray}}
lazy_static = { version = "1.5", optional = true }
Expand Down Expand Up @@ -163,15 +172,13 @@ clap = "4.5"
env_logger = "0.11"
tokio = { version = "1.49", features = ["full"] }
native-tls = "0.2"
openssl = "0.10"
tokio-openssl = "0.6"
pin-project = "1.1.10"

# Bearer authentication, used in examples
jsonwebtoken = {version = "10.0.0", features = ["rust_crypto"]}

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

[[example]]
name = "client"
required-features = ["client"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ The generated library has a few optional features that can be activated through
* `client`
* This defaults to enabled and creates the basic skeleton of a client implementation based on hyper
* The constructed client implements the API trait by making remote API call.
* `client-tls`
* Optional feature that provides HTTPS support with automatic TLS backend selection:
- macOS/Windows/iOS: native-tls + hyper-tls
- Linux/Unix/others: OpenSSL + hyper-openssl
* Not enabled by default to minimize dependencies.
* `conversions`
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
* `cli`
Expand All @@ -134,6 +139,27 @@ The generated library has a few optional features that can be activated through
* This defaults to disabled and allows JSON Schema validation of received data using `MakeService::set_validation` or `Service::set_validation`.
* Note, enabling validation will have a performance penalty, especially if the API heavily uses regex based checks.

### Enabling HTTPS/TLS Support

By default, only HTTP support is included. To enable HTTPS, add the `client-tls` feature:

```toml
[dependencies]
{{{packageName}}} = { version = "{{{packageVersion}}}", features = ["client-tls"] }
```

**For server with callbacks that need HTTPS:**
```toml
[dependencies]
{{{packageName}}} = { version = "{{{packageVersion}}}", features = ["server", "client-tls"] }
```

The TLS backend is automatically selected based on your target platform:
- **macOS, Windows, iOS**: Uses `native-tls` (system TLS libraries)
- **Linux, Unix, other platforms**: Uses `openssl`

This ensures the best compatibility and native integration on each platform.

See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`.

## Documentation for API Endpoints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ struct Cli {
server_address: String,

/// Path to the client private key if using client-side TLS authentication
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
#[clap(long, requires_all(&["client_certificate", "server_certificate"]))]
client_key: Option<String>,

/// Path to the client's public certificate associated with the private key
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
#[clap(long, requires_all(&["client_key", "server_certificate"]))]
client_certificate: Option<String>,

/// Path to CA certificate used to authenticate the server
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
#[clap(long)]
server_certificate: Option<String>,

Expand Down Expand Up @@ -130,7 +130,8 @@ enum Operation {
{{/apiInfo}}
}

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

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
// On macOS/Windows/iOS or without client-tls feature, use simple client (no cert pinning/mutual TLS)
#[cfg(any(
not(feature = "client-tls"),
all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios"))
))]
fn create_client(args: &Cli, context: ClientContext) -> Result<Box<dyn ApiNoContext<ClientContext>>> {
// Client::try_new() automatically detects the URL scheme (http:// or https://)
// and creates the appropriate client.
// Note: Certificate pinning and mutual TLS are only available on Linux/Unix with OpenSSL
let client =
Client::try_new(&args.server_address).context("Failed to create HTTP(S) client")?;
Ok(Box::new(client.with_context(context)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,17 @@ impl<Connector, C> Client<
}
}

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

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

#[derive(Debug, Clone)]
pub enum HyperClient {
Http(hyper_util::client::legacy::Client<hyper_util::client::legacy::connect::HttpConnector, BoxBody<Bytes, Infallible>>),
Https(hyper_util::client::legacy::Client<HttpsConnector, BoxBody<Bytes, Infallible>>),
#[cfg(feature = "client-tls")]
Https(hyper_util::client::legacy::Client<HyperHttpsConnector, BoxBody<Bytes, Infallible>>),
}

impl Service<Request<BoxBody<Bytes, Infallible>>> for HyperClient {
Expand All @@ -132,7 +139,8 @@ impl Service<Request<BoxBody<Bytes, Infallible>>> for HyperClient {
fn call(&self, req: Request<BoxBody<Bytes, Infallible>>) -> Self::Future {
match self {
HyperClient::Http(client) => client.request(req),
HyperClient::Https(client) => client.request(req)
#[cfg(feature = "client-tls")]
HyperClient::Https(client) => client.request(req),
}
}
}
Expand All @@ -158,11 +166,17 @@ impl<C> Client<DropContextService<HyperClient, C>, C> where
"http" => {
HyperClient::Http(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector.build()))
},
#[cfg(feature = "client-tls")]
"https" => {
let https_connector = connector
.https()
.build()
.map_err(ClientInitError::SslError)?;
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(https_connector))
},
#[cfg(not(feature = "client-tls"))]
"https" => {
let connector = connector.https()
.build()
.map_err(ClientInitError::SslError)?;
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
return Err(ClientInitError::TlsNotEnabled);
},
_ => {
return Err(ClientInitError::InvalidScheme);
Expand Down Expand Up @@ -206,12 +220,13 @@ impl<C> Client<
}
}

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

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

#[cfg(feature = "client-tls")]
impl<C> Client<
DropContextService<
hyper_util::service::TowerToHyperService<
Expand All @@ -226,10 +241,25 @@ impl<C> Client<
> where
C: Clone + Send + Sync + 'static
{
/// Create a client with a TLS connection to the server
/// Create a client with a TLS connection to the server using native-tls.
///
/// # Arguments
/// * `base_path` - base path of the client API, i.e. "<http://www.my-api-implementation.com>"
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
pub fn try_new_https(base_path: &str) -> Result<Self, ClientInitError>
{
let https_connector = Connector::builder()
.https()
.build()
.map_err(ClientInitError::SslError)?;
Self::try_new_with_connector(base_path, Some("https"), https_connector)
}

/// Create a client with a TLS connection to the server using OpenSSL.
///
/// # Arguments
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
pub fn try_new_https(base_path: &str) -> Result<Self, ClientInitError>
{
let https_connector = Connector::builder()
Expand All @@ -242,7 +272,7 @@ impl<C> Client<
/// Create a client with a TLS connection to the server using a pinned certificate
///
/// # Arguments
/// * `base_path` - base path of the client API, i.e. "<http://www.my-api-implementation.com>"
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
/// * `ca_certificate` - Path to CA certificate used to authenticate the server
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
pub fn try_new_https_pinned<CA>(
Expand All @@ -263,7 +293,7 @@ impl<C> Client<
/// Create a client with a mutually authenticated TLS connection to the server.
///
/// # Arguments
/// * `base_path` - base path of the client API, i.e. "<http://www.my-api-implementation.com>"
/// * `base_path` - base path of the client API, i.e. "<https://www.my-api-implementation.com>"
/// * `ca_certificate` - Path to CA certificate used to authenticate the server
/// * `client_key` - Path to the client private key
/// * `client_certificate` - Path to the client's public certificate associated with the private key
Expand Down Expand Up @@ -325,12 +355,15 @@ pub enum ClientInitError {
/// Missing Hostname
MissingHost,

/// HTTPS requested but TLS features not enabled
TlsNotEnabled,

/// SSL Connection Error
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))]
#[cfg(all(feature = "client-tls", any(target_os = "macos", target_os = "windows", target_os = "ios")))]
SslError(native_tls::Error),

/// SSL Connection Error
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))]
#[cfg(all(feature = "client-tls", not(any(target_os = "macos", target_os = "windows", target_os = "ios"))))]
SslError(openssl::error::ErrorStack),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,33 @@ fn main() {
let context: ClientContext =
swagger::make_context!(ContextBuilder, EmptyContext, auth_data, XSpanIdString::default());

let mut client : Box<dyn ApiNoContext<ClientContext>> = if is_https {
// Using Simple HTTPS
let client = Box::new(Client::try_new_https(&base_url)
.expect("Failed to create HTTPS client"));
Box::new(client.with_context(context))
} else {
// Using HTTP
let client = Box::new(Client::try_new_http(
&base_url)
.expect("Failed to create HTTP client"));
Box::new(client.with_context(context))
let mut client : Box<dyn ApiNoContext<ClientContext>> = {
#[cfg(feature = "client-tls")]
{
if is_https {
// Using HTTPS with native-tls
let client = Box::new(Client::try_new_https(&base_url)
.expect("Failed to create HTTPS client"));
Box::new(client.with_context(context))
} else {
// Using HTTP
let client = Box::new(Client::try_new_http(&base_url)
.expect("Failed to create HTTP client"));
Box::new(client.with_context(context))
}
}

#[cfg(not(feature = "client-tls"))]
{
if is_https {
panic!("HTTPS requested but TLS support not enabled. \
Enable the 'client-tls' feature to use HTTPS.");
}
// Using HTTP only
let client = Box::new(Client::try_new_http(&base_url)
.expect("Failed to create HTTP client"));
Box::new(client.with_context(context))
}
};

let mut rt = tokio::runtime::Runtime::new().unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,25 @@ impl<C> Client<DropContextService<hyper_util::service::TowerToHyperService<hyper
}
}

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

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

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

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