forked from loafle/openapi-generator-original
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
This commit is contained in:
9
.github/workflows/samples-rust-server.yaml
vendored
9
.github/workflows/samples-rust-server.yaml
vendored
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ homepage = "{{.}}"
|
||||
{{/homePageUrl}}
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
{{#apiUsesMultipartFormData}}
|
||||
"multipart", "multipart/client", "swagger/multipart_form",
|
||||
@@ -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}}
|
||||
@@ -57,7 +69,7 @@ server = [
|
||||
"mime_multipart", "swagger/multipart_related",
|
||||
{{/apiUsesMultipartRelated}}
|
||||
{{#hasCallbacks}}
|
||||
"native-tls", "hyper-openssl", "hyper-tls", "openssl",
|
||||
"hyper-util/http1", "hyper-util/http2",
|
||||
{{/hasCallbacks}}
|
||||
{{! Anything added to the list below, should probably be added to the callbacks list above }}
|
||||
"serde_ignored", "hyper", "percent-encoding", "url",
|
||||
@@ -74,20 +86,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"
|
||||
|
||||
@@ -157,6 +161,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -169,8 +184,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -126,6 +126,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -134,6 +138,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
{{{packageName}}} = { version = "{{{packageVersion}}}", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**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`
|
||||
|
||||
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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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");
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 {
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -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<
|
||||
@@ -226,10 +241,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -239,10 +255,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -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
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -97,16 +97,17 @@ 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>
|
||||
{
|
||||
@@ -114,7 +115,7 @@ impl<C> Client<DropContextService<hyper_util::service::TowerToHyperService<hyper
|
||||
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>
|
||||
{
|
||||
|
||||
@@ -8,11 +8,23 @@ license = "Unlicense"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"multipart", "multipart/client", "swagger/multipart_form",
|
||||
"mime_multipart", "swagger/multipart_related",
|
||||
"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 = [
|
||||
"multipart", "multipart/server", "swagger/multipart_form",
|
||||
@@ -28,20 +40,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = [ "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"
|
||||
|
||||
@@ -89,6 +93,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -101,8 +116,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -111,6 +111,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -119,6 +123,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
multipart-v3 = { version = "1.0.7", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
multipart-v3 = { version = "1.0.7", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -35,17 +35,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>,
|
||||
|
||||
@@ -83,7 +83,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -109,8 +110,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)))
|
||||
|
||||
@@ -89,17 +89,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();
|
||||
|
||||
@@ -160,10 +160,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 {
|
||||
@@ -174,7 +181,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,11 +208,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -248,12 +262,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<
|
||||
@@ -268,10 +283,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -281,10 +297,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -305,7 +335,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
|
||||
@@ -367,12 +397,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),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,21 @@ license = "Unlicense"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"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 = [
|
||||
"serde_ignored", "hyper", "percent-encoding", "url",
|
||||
@@ -24,20 +36,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = ["regex", "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"
|
||||
|
||||
@@ -83,6 +87,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -95,8 +110,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -108,6 +108,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -116,6 +120,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
no-example-v3 = { version = "0.0.1", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
no-example-v3 = { version = "0.0.1", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -33,17 +33,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>,
|
||||
|
||||
@@ -63,7 +63,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -89,8 +90,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)))
|
||||
|
||||
@@ -85,17 +85,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();
|
||||
|
||||
@@ -153,10 +153,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 {
|
||||
@@ -167,7 +174,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,11 +201,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -241,12 +255,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<
|
||||
@@ -261,10 +276,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -274,10 +290,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -298,7 +328,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
|
||||
@@ -360,12 +390,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),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,26 @@ license = "Unlicense"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"serde_urlencoded",
|
||||
"serde_ignored", "percent-encoding",
|
||||
"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 = [
|
||||
"native-tls", "hyper-openssl", "hyper-tls", "openssl",
|
||||
"hyper-util/http1", "hyper-util/http2",
|
||||
"serde_ignored", "hyper", "percent-encoding", "url",
|
||||
|
||||
]
|
||||
@@ -27,20 +39,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = [ "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"
|
||||
|
||||
@@ -89,6 +93,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -101,8 +116,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -139,6 +139,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -147,6 +151,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
openapi-v3 = { version = "1.0.7", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
openapi-v3 = { version = "1.0.7", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -63,17 +63,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>,
|
||||
|
||||
@@ -214,7 +214,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -240,8 +241,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)))
|
||||
|
||||
@@ -151,17 +151,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();
|
||||
|
||||
@@ -185,10 +185,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 {
|
||||
@@ -199,7 +206,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,11 +233,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -273,12 +287,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<
|
||||
@@ -293,10 +308,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -306,10 +322,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -330,7 +360,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
|
||||
@@ -392,12 +422,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),
|
||||
}
|
||||
|
||||
|
||||
@@ -123,16 +123,17 @@ 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>
|
||||
{
|
||||
@@ -140,7 +141,7 @@ impl<C> Client<DropContextService<hyper_util::service::TowerToHyperService<hyper
|
||||
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>
|
||||
{
|
||||
|
||||
@@ -8,9 +8,21 @@ license = "Unlicense"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"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 = [
|
||||
"serde_ignored", "hyper", "percent-encoding", "url",
|
||||
@@ -24,20 +36,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = ["regex", "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"
|
||||
|
||||
@@ -83,6 +87,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -95,8 +110,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -145,6 +145,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -153,6 +157,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ops-v3 = { version = "0.0.1", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
ops-v3 = { version = "0.0.1", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -69,17 +69,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>,
|
||||
|
||||
@@ -169,7 +169,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -195,8 +196,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)))
|
||||
|
||||
@@ -157,17 +157,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();
|
||||
|
||||
@@ -189,10 +189,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 {
|
||||
@@ -203,7 +210,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,11 +237,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -277,12 +291,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<
|
||||
@@ -297,10 +312,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -310,10 +326,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -334,7 +364,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
|
||||
@@ -396,12 +426,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),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,23 @@ edition = "2018"
|
||||
publish = ["crates-io"]
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"multipart", "multipart/client", "swagger/multipart_form",
|
||||
"serde_urlencoded",
|
||||
"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 = [
|
||||
"multipart", "multipart/server", "swagger/multipart_form",
|
||||
@@ -28,20 +40,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = [ "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"
|
||||
|
||||
@@ -92,6 +96,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -104,8 +119,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -133,6 +133,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -141,6 +145,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
petstore-with-fake-endpoints-models-for-testing = { version = "1.0.0", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
petstore-with-fake-endpoints-models-for-testing = { version = "1.0.0", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -68,17 +68,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>,
|
||||
|
||||
@@ -342,7 +342,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -368,8 +369,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)))
|
||||
|
||||
@@ -156,17 +156,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();
|
||||
|
||||
@@ -190,10 +190,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 {
|
||||
@@ -204,7 +211,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,11 +238,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -278,12 +292,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<
|
||||
@@ -298,10 +313,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -311,10 +327,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -335,7 +365,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
|
||||
@@ -397,12 +427,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),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,21 @@ license = "Unlicense"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"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 = [
|
||||
"serde_ignored", "hyper", "percent-encoding", "url",
|
||||
@@ -24,20 +36,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = ["regex", "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"
|
||||
|
||||
@@ -83,6 +87,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -95,8 +110,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -109,6 +109,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -117,6 +121,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ping-bearer-auth = { version = "1.0.0", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
ping-bearer-auth = { version = "1.0.0", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -33,17 +33,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>,
|
||||
|
||||
@@ -65,7 +65,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -91,8 +92,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)))
|
||||
|
||||
@@ -85,17 +85,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();
|
||||
|
||||
@@ -153,10 +153,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 {
|
||||
@@ -167,7 +174,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,11 +201,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -241,12 +255,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<
|
||||
@@ -261,10 +276,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -274,10 +290,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -298,7 +328,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
|
||||
@@ -360,12 +390,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),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,21 @@ license = "Unlicense"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["client", "server"]
|
||||
default = ["client", "server", "client-tls"]
|
||||
client = [
|
||||
"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 = [
|
||||
"serde_ignored", "hyper", "percent-encoding", "url",
|
||||
@@ -24,20 +36,12 @@ conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-
|
||||
mock = ["mockall"]
|
||||
validate = ["regex", "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"
|
||||
|
||||
@@ -83,6 +87,17 @@ frunk_core = { version = "0.4.3", optional = true }
|
||||
frunk-enum-derive = { version = "0.3.0", optional = true }
|
||||
frunk-enum-core = { version = "0.3.0", optional = true }
|
||||
|
||||
# TLS dependencies - platform-specific backends
|
||||
# On macOS/Windows/iOS, use native-tls via hyper-tls
|
||||
[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 }
|
||||
|
||||
# On other platforms (Linux, etc.), use OpenSSL via hyper-openssl
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))'.dependencies]
|
||||
openssl = { version = "0.10", optional = true }
|
||||
hyper-openssl = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
always_send = "0.1.1"
|
||||
clap = "4.5"
|
||||
@@ -95,8 +110,8 @@ pin-project = "1.1.10"
|
||||
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"
|
||||
tokio-openssl = "0.6"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
||||
@@ -115,6 +115,10 @@ 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`
|
||||
* This default to enabled and provides HTTPS support with automatic TLS backend selection:
|
||||
- macOS/Windows/iOS: native-tls + hyper-tls
|
||||
- Linux/Unix/others: OpenSSL + hyper-openssl
|
||||
* `conversions`
|
||||
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
|
||||
* `cli`
|
||||
@@ -123,6 +127,25 @@ 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.
|
||||
|
||||
### HTTPS/TLS Support
|
||||
|
||||
HTTPS support is included by default. To disable it (for example, to reduce dependencies), you can:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rust-server-test = { version = "2.3.4", default-features = false, features = ["client", "server"] }
|
||||
```
|
||||
|
||||
**For server with callbacks that need HTTPS:**
|
||||
```toml
|
||||
[dependencies]
|
||||
rust-server-test = { version = "2.3.4", 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`
|
||||
|
||||
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
|
||||
|
||||
@@ -41,17 +41,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>,
|
||||
|
||||
@@ -96,7 +96,8 @@ enum Operation {
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
@@ -122,8 +123,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)))
|
||||
|
||||
@@ -101,17 +101,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();
|
||||
|
||||
@@ -161,10 +161,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 {
|
||||
@@ -175,7 +182,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,11 +209,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 connector = connector.https()
|
||||
.build()
|
||||
.map_err(ClientInitError::SslError)?;
|
||||
HyperClient::Https(hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector))
|
||||
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" => {
|
||||
return Err(ClientInitError::TlsNotEnabled);
|
||||
},
|
||||
_ => {
|
||||
return Err(ClientInitError::InvalidScheme);
|
||||
@@ -249,12 +263,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<
|
||||
@@ -269,10 +284,11 @@ 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.
|
||||
///
|
||||
/// # 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()
|
||||
@@ -282,10 +298,24 @@ impl<C> Client<
|
||||
Self::try_new_with_connector(base_path, Some("https"), https_connector)
|
||||
}
|
||||
|
||||
/// Create a client with a TLS connection to the server using a pinned certificate
|
||||
/// Create a client with a TLS connection to the server using OpenSSL via swagger.
|
||||
///
|
||||
/// # 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(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()
|
||||
.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 a pinned certificate.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `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>(
|
||||
@@ -306,7 +336,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
|
||||
@@ -368,12 +398,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),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user