diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 7d498e5e32..419cfa8563 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -147,6 +147,7 @@ { "filename": "**/*.md", "dictionaries": [ + "crates", "rust", "rust-custom" ], diff --git a/Cargo.lock b/Cargo.lock index 9165a0b840..d8a64269b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,7 @@ dependencies = [ "criterion", "futures", "hmac", + "http", "openssl", "pin-project", "reqwest", @@ -199,6 +200,7 @@ dependencies = [ "tracing-subscriber", "typespec", "typespec_client_core", + "ureq", ] [[package]] @@ -542,6 +544,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bitflags" version = "2.9.1" @@ -924,6 +932,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1949,6 +1967,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2370,6 +2397,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -3147,6 +3183,37 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "ureq" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00432f493971db5d8e47a65aeb3b02f8226b9b11f1450ff86bb772776ebadd70" +dependencies = [ + "base64", + "der", + "flate2", + "log", + "native-tls", + "percent-encoding", + "rustls-pemfile", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b6cabebbecc4c45189ab06b52f956206cea7d8c8a20851c35a85cb169224cc" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -3158,6 +3225,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3376,6 +3449,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/eng/dict/crates.txt b/eng/dict/crates.txt index 6a2163073c..88f3d62492 100644 --- a/eng/dict/crates.txt +++ b/eng/dict/crates.txt @@ -66,6 +66,7 @@ typespec_client_core typespec_client_core typespec_macros typespec_macros +ureq url uuid zerofrom diff --git a/eng/scripts/verify-dependencies.rs b/eng/scripts/verify-dependencies.rs index 36efb52800..24d252633e 100755 --- a/eng/scripts/verify-dependencies.rs +++ b/eng/scripts/verify-dependencies.rs @@ -18,6 +18,8 @@ use std::{ }; static EXEMPTIONS: &[(&str, &str)] = &[ + ("azure_core", "http"), + ("azure_core", "ureq"), ("azure_core_test", "dotenvy"), ("azure_template", "serde"), ("azure_core_opentelemetry", "opentelemetry"), diff --git a/sdk/core/azure_core/CHANGELOG.md b/sdk/core/azure_core/CHANGELOG.md index dc4ee17a87..b51f3465ae 100644 --- a/sdk/core/azure_core/CHANGELOG.md +++ b/sdk/core/azure_core/CHANGELOG.md @@ -6,6 +6,7 @@ ### Breaking Changes +- Removed feature `reqwest_rustls_tls`. See [README.md](https://github.com/heaths/azure-sdk-for-rust/blob/main/sdk/core/azure_core/README.md) for alternative HTTP client configuration. - Removed the `fs` module including the `FileStream` and `FileStreamBuilder` types. Moved to `examples/` for `typespec_client_core` to copy if needed. - Removed the `setters` macro. diff --git a/sdk/core/azure_core/Cargo.toml b/sdk/core/azure_core/Cargo.toml index 04d79e73ef..d7bbf0bc4d 100644 --- a/sdk/core/azure_core/Cargo.toml +++ b/sdk/core/azure_core/Cargo.toml @@ -44,10 +44,15 @@ azure_identity.workspace = true azure_security_keyvault_certificates.path = "../../keyvault/azure_security_keyvault_certificates" azure_security_keyvault_secrets.path = "../../keyvault/azure_security_keyvault_secrets" criterion.workspace = true +http = "1.3.1" reqwest.workspace = true thiserror.workspace = true tokio.workspace = true tracing-subscriber.workspace = true +ureq = { version = "3", default-features = false, features = [ + "gzip", + "native-tls", +] } [features] default = [ diff --git a/sdk/core/azure_core/README.md b/sdk/core/azure_core/README.md index d68480b935..03f85c077d 100644 --- a/sdk/core/azure_core/README.md +++ b/sdk/core/azure_core/README.md @@ -363,9 +363,133 @@ async fn main() -> Result<(), Box> { Awaiting `wait()` will only fail if the HTTP status code does not indicate successfully fetching the status monitor. +### Replacing the HTTP client + +Though `azure_core` uses [`reqwest`] for its default HTTP client, you can replace it with either a customized `reqwest::Client` or an entirely different HTTP client. + +#### Reqwest + +We define a `reqwest` feature that provides a blanket implementation of our `HttpClient` trait for `reqwest::Client` and depends on the `reqwest` crate. +If you just want to configure a `reqwest::Client` to use different options including a different TLS provider, optionally add a dependency on `reqwest` and enable whichever feature you want: + +```sh +cargo add reqwest -F rustls-tls-native-roots +``` + +You can then disable default features of any of the Azure SDK crates and add a dependency on `azure_core` with the `reqwest` feature for the blanket `HttpClient` implementation: + +```sh +cargo add azure_core --no-default-features -F reqwest +``` + +You should end up with a `Cargo.toml` that looks something like: + +```toml +[dependencies] +azure_core = { version = "1", default-features = false, features = ["reqwest"] } +azure_identity = { version = "1", default-features = false } +azure_security_keyvault_secrets = { version = "1", default-features = false } +reqwest = { version = "0.12.23", default-features = false, features = [ + "deflate", + "gzip", + "rustls-tls-native-roots", +] } +``` + +In many cases with `reqwest`, importing features may be enough. See their [documentation][`reqwest`] for more information. +If you do need to write code to customize the `reqwest::Client`, you can pass it in `ClientOptions` to our client libraries: + +```rust no_run +use azure_core::http::{ClientOptions, TransportOptions}; +use azure_identity::DeveloperToolsCredential; +use azure_security_keyvault_secrets::{SecretClient, SecretClientOptions}; +use std::sync::Arc; + +let http_client = Arc::new(reqwest::ClientBuilder::new().gzip(true).build().unwrap()); + +let options = SecretClientOptions { + client_options: ClientOptions { + transport: Some(TransportOptions::new(http_client)), + ..Default::default() + }, + ..Default::default() +}; + +let credential = DeveloperToolsCredential::new(None).unwrap(); +let client = SecretClient::new( + "https://your-key-vault-name.vault.azure.net/", + credential.clone(), + Some(options), +) +.unwrap(); +``` + +#### Other + +If you do not want to take a dependency on [`reqwest`] at all - perhaps because you [want to use a different async runtime](#replacing-the-async-runtime) other than [`tokio`] - +you can implement the `HttpClient` (recommended) or the `Policy` trait yourself. + +Similar to [customizing `reqwest` above](#reqwest), you can disable default features for Azure SDK crates. In this example where we do not want a dependency on `reqwest` at all, +we need to import `azure_core` with no default features only to implement `HttpClient` so that your `Cargo.toml` looks something like: + +```toml +[dependencies] +azure_core = { version = "1", default-features = false } +azure_identity = { version = "1", default-features = false } +azure_security_keyvault_secrets = { version = "1", default-features = false } +http = "1" +ureq = { version = "3", default-features = false, features = [ + "gzip", + "native-tls", +] } +``` + +Then we need to implement `HttpClient` for another HTTP client like [`ureq`](https://docs.rs/ureq): + +```rust no_run +use azure_core::{error::{ErrorKind, ResultExt as _}, http::{HttpClient, RawResponse, Request}}; +use ureq::tls::{TlsConfig, TlsProvider}; + +#[derive(Debug)] +struct Agent(ureq::Agent); + +impl Default for Agent { + fn default() -> Self { + Self( + ureq::Agent::config_builder() + .https_only(true) + .tls_config( + TlsConfig::builder() + .provider(TlsProvider::NativeTls) + .build(), + ) + .build() + .into(), + ) + } +} + +#[async_trait::async_trait] +impl HttpClient for Agent { + async fn execute_request(&self, request: &Request) -> azure_core::Result { + let request: ::http::request::Request> = todo!("convert our request into their request"); + let response = self + .0 + .run(request) + .with_context(ErrorKind::Io, || "failed to send request")?; + + Ok(todo!("convert their response into our response")) + } +} +``` + +See the [example](https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/core/azure_core/examples/core_ureq_client.rs) for a full sample implementation. + +After you've implemented `HttpClient`, you pass it in `ClientOptions` to our client libraries as [shown for `reqwest` above](#reqwest). + ### Replacing the async runtime -Internally, the Azure SDK uses either the `tokio` async runtime (with the `tokio` feature), or it implements asynchronous functionality using functions in the `std` namespace. +Internally, the Azure SDK uses either the [`tokio`] async runtime (with the `tokio` feature), or it implements asynchronous functionality using functions in the `std` namespace. If your application uses a different asynchronous runtime, you can replace the asynchronous runtime used for internal functions by providing your own implementation of the `azure_core::async_runtime::AsyncRuntime` trait. @@ -483,9 +607,11 @@ When you submit a pull request, a CLA-bot will automatically determine whether y This project has adopted the [Microsoft Open Source Code of Conduct]. For more information see the [Code of Conduct FAQ] or contact with any additional questions or comments. -[Source code]: https://github.com/Azure/azure-sdk-for-rust/tree/main/sdk/core/azure_core/src -[Package (crates.io)]: https://crates.io/crates/azure_core [API Reference Documentation]: https://docs.rs/azure_core -[CONTRIBUTING.md]: https://github.com/Azure/azure-sdk-for-rust/blob/main/CONTRIBUTING.md [Code of Conduct FAQ]: https://opensource.microsoft.com/codeofconduct/faq/ +[CONTRIBUTING.md]: https://github.com/Azure/azure-sdk-for-rust/blob/main/CONTRIBUTING.md [guidelines]: https://azure.github.io/azure-sdk/rust_introduction.html +[Package (crates.io)]: https://crates.io/crates/azure_core +[`reqwest`]: https://docs.rs/reqwest +[`tokio`]: https://docs.rs/tokio +[Source code]: https://github.com/Azure/azure-sdk-for-rust/tree/main/sdk/core/azure_core/src diff --git a/sdk/core/azure_core/examples/core_ureq_client.rs b/sdk/core/azure_core/examples/core_ureq_client.rs new file mode 100644 index 0000000000..2d3e50961c --- /dev/null +++ b/sdk/core/azure_core/examples/core_ureq_client.rs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use async_trait::async_trait; +use azure_core::{ + error::ErrorKind, + http::{headers::Headers, ClientOptions, HttpClient, RawResponse, Request, TransportOptions}, +}; +use azure_identity::DeveloperToolsCredential; +use azure_security_keyvault_secrets::{ResourceExt as _, SecretClient, SecretClientOptions}; +use futures::TryStreamExt; +use std::{env, sync::Arc}; +use typespec::error::ResultExt; +use ureq::tls::{TlsConfig, TlsProvider}; + +#[derive(Debug)] +struct Agent(ureq::Agent); + +impl Default for Agent { + fn default() -> Self { + Self( + ureq::Agent::config_builder() + .https_only(true) + .tls_config( + TlsConfig::builder() + .provider(TlsProvider::NativeTls) + .build(), + ) + .build() + .into(), + ) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl HttpClient for Agent { + async fn execute_request(&self, request: &Request) -> azure_core::Result { + let request = into_request(request)?; + let response = self + .0 + .run(request) + .with_context(ErrorKind::Io, || "failed to send request")?; + + into_response(response) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let vault_url = env::var("AZURE_KEYVAULT_URL") + .map_err(|_| "Environment variable AZURE_KEYVAULT_URL is required")?; + + let credential = DeveloperToolsCredential::new(None)?; + + let agent = Arc::new(Agent::default()); + let options = SecretClientOptions { + client_options: ClientOptions { + transport: Some(TransportOptions::new(agent)), + ..Default::default() + }, + ..Default::default() + }; + + let client = SecretClient::new(&vault_url, credential.clone(), Some(options))?; + let mut pager = client.list_secret_properties(None)?; + while let Some(secret) = pager.try_next().await? { + let name = secret.resource_id()?.name; + println!("Secret: {name}"); + } + + Ok(()) +} + +fn into_request(request: &Request) -> azure_core::Result<::http::Request>> { + use ::http::{HeaderName, HeaderValue, Request}; + use azure_core::Bytes; + + let mut req: Request> = Default::default(); + *req.uri_mut() = request + .url() + .as_str() + .parse() + .with_context(ErrorKind::DataConversion, || "failed to parse url")?; + *req.method_mut() = request + .method() + .as_str() + .parse() + .with_context(ErrorKind::DataConversion, || "failed to parse method")?; + let headers = req.headers_mut(); + for (name, value) in request.headers().iter() { + headers.insert( + HeaderName::from_bytes(name.as_str().as_bytes()) + .with_context(ErrorKind::DataConversion, || "failed to parse header name")?, + HeaderValue::from_bytes(value.as_str().as_bytes()) + .with_context(ErrorKind::DataConversion, || "failed to parse header value")?, + ); + } + let body: Bytes = request.body().into(); + *req.body_mut() = body.into(); + + Ok(req) +} + +fn into_response(response: ::http::Response) -> azure_core::Result { + use ::http::response::Parts; + use azure_core::http::StatusCode; + + let ( + Parts { + status, headers, .. + }, + mut body, + ) = response.into_parts(); + + let status: StatusCode = status.as_u16().into(); + let mut response_headers = Headers::new(); + for (name, value) in headers.iter() { + response_headers.insert( + name.as_str().to_ascii_lowercase(), + value + .to_str() + .with_context(ErrorKind::DataConversion, || "failed to parse header value")? + .to_string(), + ); + } + let body: Vec = body + .read_to_vec() + .with_context(ErrorKind::Io, || "failed to read response body")?; + + Ok(RawResponse::from_bytes(status, response_headers, body)) +} diff --git a/sdk/typespec/typespec_client_core/CHANGELOG.md b/sdk/typespec/typespec_client_core/CHANGELOG.md index 888b705ccc..9f79b622aa 100644 --- a/sdk/typespec/typespec_client_core/CHANGELOG.md +++ b/sdk/typespec/typespec_client_core/CHANGELOG.md @@ -8,6 +8,7 @@ ### Breaking Changes +- Removed feature `reqwest_rustls_tls`. See [README.md](https://github.com/heaths/azure-sdk-for-rust/blob/main/sdk/typespec/typespec_client_core/README.md) for alternative HTTP client configuration. - Removed the `fs` module including the `FileStream` and `FileStreamBuilder` types. Moved to `examples/` to copy if needed. - Removed the `setters` macro.