mirror of
https://github.com/seemueller-io/hyper-custom-cert.git
synced 2025-09-08 22:46:45 +00:00
cleanup (#1)
* 0.3.4 * Add `RequestOptions` for per-request customization with headers and timeouts - Introduced the `RequestOptions` struct for flexible HTTP request configurations. - Added `request_with_options` and `post_with_options` methods. - Deprecated `request` and `post` in favor of the new methods. - Updated examples and tests to reflect the new API. * run cargo fmt * Update HTTP client methods to use `request_with_options` for improved flexibility. Adjusted related test cases and examples accordingly. * Format `request_with_options` calls for improved readability. * - Downgrade `edition` from 2024 to 2021 in Cargo.toml files for compatibility. - Fix nested `if let` statements to improve readability and correctness. - Reorganize imports for consistency and structure. * Restore github old workflows * update ci --------- Co-authored-by: geoffsee <>
This commit is contained in:
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -6,8 +6,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: build-and-test (test-all.sh)
|
name: build-and-test (${{ matrix.name }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -31,12 +33,15 @@ jobs:
|
|||||||
- name: Cargo fmt (check)
|
- name: Cargo fmt (check)
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
- name: Clippy (default)
|
- name: Clippy
|
||||||
run: cargo clippy --all-targets -- -D warnings
|
|
||||||
|
|
||||||
- name: Ensure test-all.sh is executable
|
|
||||||
run: chmod +x ./scripts/test-all.sh
|
|
||||||
|
|
||||||
- name: Run comprehensive test suite
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./scripts/test-all.sh
|
run: cargo clippy --all-targets
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
shell: bash
|
||||||
|
run: cargo test --all-features
|
||||||
|
|
||||||
|
- name: Build Docs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cargo doc -p hyper-custom-cert --no-deps
|
||||||
|
74
.github/workflows/release.yml
vendored
74
.github/workflows/release.yml
vendored
@@ -10,8 +10,30 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docs:
|
docs:
|
||||||
name: Build and validate documentation (test-all.sh quick)
|
name: Build and validate documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: crates/hyper-custom-cert
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: default-features
|
||||||
|
features: ""
|
||||||
|
no-default-features: false
|
||||||
|
- name: no-default-features
|
||||||
|
features: ""
|
||||||
|
no-default-features: true
|
||||||
|
- name: rustls
|
||||||
|
features: "rustls"
|
||||||
|
no-default-features: true
|
||||||
|
- name: insecure-dangerous
|
||||||
|
features: "insecure-dangerous"
|
||||||
|
no-default-features: false
|
||||||
|
- name: all-features
|
||||||
|
features: "rustls,insecure-dangerous"
|
||||||
|
no-default-features: true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -29,17 +51,42 @@ jobs:
|
|||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
run: rustup update stable && rustup default stable
|
run: rustup update stable && rustup default stable
|
||||||
|
|
||||||
- name: Ensure test-all.sh is executable
|
- name: Build documentation
|
||||||
run: chmod +x ./scripts/test-all.sh
|
|
||||||
|
|
||||||
- name: Run test-all.sh (quick mode)
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./scripts/test-all.sh --quick
|
run: |
|
||||||
|
FLAGS=""
|
||||||
|
if [ "${{ matrix.no-default-features }}" = "true" ]; then FLAGS="$FLAGS --no-default-features"; fi
|
||||||
|
if [ -n "${{ matrix.features }}" ]; then FLAGS="$FLAGS --features ${{ matrix.features }}"; fi
|
||||||
|
echo "Running: cargo doc $FLAGS --no-deps"
|
||||||
|
cargo doc $FLAGS --no-deps
|
||||||
|
|
||||||
|
- name: Check documentation warnings
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
FLAGS=""
|
||||||
|
if [ "${{ matrix.no-default-features }}" = "true" ]; then FLAGS="$FLAGS --no-default-features"; fi
|
||||||
|
if [ -n "${{ matrix.features }}" ]; then FLAGS="$FLAGS --features ${{ matrix.features }}"; fi
|
||||||
|
echo "Running: cargo doc $FLAGS --no-deps"
|
||||||
|
RUSTDOCFLAGS="-D warnings" cargo doc $FLAGS --no-deps
|
||||||
|
|
||||||
|
- name: Test documentation examples
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
FLAGS=""
|
||||||
|
if [ "${{ matrix.no-default-features }}" = "true" ]; then FLAGS="$FLAGS --no-default-features"; fi
|
||||||
|
if [ -n "${{ matrix.features }}" ]; then FLAGS="$FLAGS --features ${{ matrix.features }}"; fi
|
||||||
|
echo "Running: cargo test --doc $FLAGS"
|
||||||
|
cargo test --doc $FLAGS
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test before release (test-all.sh)
|
name: Test before release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: docs
|
needs: docs
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: crates/hyper-custom-cert
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -63,16 +110,13 @@ jobs:
|
|||||||
- name: Cargo fmt (check)
|
- name: Cargo fmt (check)
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
- name: Clippy (default)
|
- name: Clippy
|
||||||
run: cargo clippy --all-targets -- -D warnings
|
|
||||||
|
|
||||||
- name: Ensure test-all.sh is executable
|
|
||||||
run: chmod +x ./scripts/test-all.sh
|
|
||||||
|
|
||||||
- name: Run comprehensive test suite
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./scripts/test-all.sh
|
run: cargo clippy --all-targets
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
shell: bash
|
||||||
|
run: cargo test --all-features
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish to crates.io
|
name: Publish to crates.io
|
||||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -489,7 +489,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-custom-cert"
|
name = "hyper-custom-cert"
|
||||||
version = "0.3.2"
|
version = "0.3.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "example"
|
name = "example"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# No-op features used only by the example to allow conditional compilation in docs/examples.
|
# No-op features used only by the example to allow conditional compilation in docs/examples.
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query},
|
extract::{Path, Query},
|
||||||
response::Json,
|
response::Json,
|
||||||
routing::{get, post, put, delete},
|
routing::{delete, get, post, put},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use hyper_custom_cert::HttpClient;
|
use hyper_custom_cert::HttpClient;
|
||||||
@@ -10,7 +10,7 @@ use serde_json::{json, Value};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const SERVER_ADDRESS: &str = "0.0.0.0:8393";
|
const SERVER_ADDRESS: &str = "127.0.0.1:8393";
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct TestResponse {
|
struct TestResponse {
|
||||||
@@ -38,39 +38,35 @@ async fn main() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
// Root endpoint with API overview
|
// Root endpoint with API overview
|
||||||
.route("/", get(api_overview))
|
.route("/", get(api_overview))
|
||||||
|
|
||||||
// Basic HTTP client tests
|
// Basic HTTP client tests
|
||||||
.route("/test/client/default", get(test_default_client))
|
.route("/test/client/default", get(test_default_client))
|
||||||
.route("/test/client/builder", get(test_builder_client))
|
.route("/test/client/builder", get(test_builder_client))
|
||||||
.route("/test/client/timeout", get(test_timeout_client))
|
.route("/test/client/timeout", get(test_timeout_client))
|
||||||
.route("/test/client/headers", get(test_headers_client))
|
.route("/test/client/headers", get(test_headers_client))
|
||||||
.route("/test/client/combined", get(test_combined_config))
|
.route("/test/client/combined", get(test_combined_config))
|
||||||
|
|
||||||
// Feature-specific tests
|
// Feature-specific tests
|
||||||
.route("/test/features/native-tls", get(test_native_tls_feature))
|
.route("/test/features/native-tls", get(test_native_tls_feature))
|
||||||
.route("/test/features/rustls", get(test_rustls_feature))
|
.route("/test/features/rustls", get(test_rustls_feature))
|
||||||
.route("/test/features/insecure", get(test_insecure_feature))
|
.route("/test/features/insecure", get(test_insecure_feature))
|
||||||
|
|
||||||
// HTTP method tests
|
// HTTP method tests
|
||||||
.route("/test/methods/get", get(test_get_method))
|
.route("/test/methods/get", get(test_get_method))
|
||||||
.route("/test/methods/post", post(test_post_method))
|
.route("/test/methods/post", post(test_post_method))
|
||||||
.route("/test/methods/put", put(test_put_method))
|
.route("/test/methods/put", put(test_put_method))
|
||||||
.route("/test/methods/delete", delete(test_delete_method))
|
.route("/test/methods/delete", delete(test_delete_method))
|
||||||
|
|
||||||
// Certificate and TLS tests
|
// Certificate and TLS tests
|
||||||
.route("/test/tls/custom-ca", get(test_custom_ca))
|
.route("/test/tls/custom-ca", get(test_custom_ca))
|
||||||
.route("/test/tls/cert-pinning", get(test_cert_pinning))
|
.route("/test/tls/cert-pinning", get(test_cert_pinning))
|
||||||
.route("/test/tls/self-signed", get(test_self_signed))
|
.route("/test/tls/self-signed", get(test_self_signed))
|
||||||
|
|
||||||
// Configuration tests
|
// Configuration tests
|
||||||
.route("/test/config/timeout/{seconds}", get(test_custom_timeout))
|
.route("/test/config/timeout/{seconds}", get(test_custom_timeout))
|
||||||
.route("/test/config/headers/{header_count}", get(test_custom_headers))
|
.route(
|
||||||
|
"/test/config/headers/{header_count}",
|
||||||
|
get(test_custom_headers),
|
||||||
|
)
|
||||||
// Error simulation tests
|
// Error simulation tests
|
||||||
.route("/test/errors/timeout", get(test_timeout_error))
|
.route("/test/errors/timeout", get(test_timeout_error))
|
||||||
.route("/test/errors/invalid-url", get(test_invalid_url))
|
.route("/test/errors/invalid-url", get(test_invalid_url))
|
||||||
.route("/test/errors/connection", get(test_connection_error))
|
.route("/test/errors/connection", get(test_connection_error))
|
||||||
|
|
||||||
// Health and status endpoints
|
// Health and status endpoints
|
||||||
.route("/health", get(health_check))
|
.route("/health", get(health_check))
|
||||||
.route("/status", get(status_check));
|
.route("/status", get(status_check));
|
||||||
@@ -143,7 +139,9 @@ async fn api_overview() -> Json<Value> {
|
|||||||
/// Test default HttpClient creation
|
/// Test default HttpClient creation
|
||||||
async fn test_default_client() -> Json<TestResponse> {
|
async fn test_default_client() -> Json<TestResponse> {
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
let result = client.request("https://httpbin.org/get").await;
|
let result = client
|
||||||
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/client/default".to_string(),
|
endpoint: "/test/client/default".to_string(),
|
||||||
@@ -160,7 +158,9 @@ async fn test_default_client() -> Json<TestResponse> {
|
|||||||
/// Test HttpClient builder pattern
|
/// Test HttpClient builder pattern
|
||||||
async fn test_builder_client() -> Json<TestResponse> {
|
async fn test_builder_client() -> Json<TestResponse> {
|
||||||
let client = HttpClient::builder().build();
|
let client = HttpClient::builder().build();
|
||||||
let result = client.request("https://httpbin.org/get").await;
|
let result = client
|
||||||
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/client/builder".to_string(),
|
endpoint: "/test/client/builder".to_string(),
|
||||||
@@ -180,12 +180,17 @@ async fn test_timeout_client(Query(params): Query<TimeoutQuery>) -> Json<TestRes
|
|||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder()
|
||||||
.with_timeout(Duration::from_secs(timeout_secs))
|
.with_timeout(Duration::from_secs(timeout_secs))
|
||||||
.build();
|
.build();
|
||||||
let result = client.request("https://httpbin.org/get").await;
|
let result = client
|
||||||
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/client/timeout".to_string(),
|
endpoint: "/test/client/timeout".to_string(),
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
message: format!("HttpClient with {}s timeout configured successfully", timeout_secs),
|
message: format!(
|
||||||
|
"HttpClient with {}s timeout configured successfully",
|
||||||
|
timeout_secs
|
||||||
|
),
|
||||||
features_tested: vec!["timeout-config".to_string()],
|
features_tested: vec!["timeout-config".to_string()],
|
||||||
error: match result {
|
error: match result {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
@@ -197,14 +202,17 @@ async fn test_timeout_client(Query(params): Query<TimeoutQuery>) -> Json<TestRes
|
|||||||
/// Test custom headers configuration
|
/// Test custom headers configuration
|
||||||
async fn test_headers_client() -> Json<TestResponse> {
|
async fn test_headers_client() -> Json<TestResponse> {
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("User-Agent".to_string(), "hyper-custom-cert-test/1.0".to_string());
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"hyper-custom-cert-test/1.0".to_string(),
|
||||||
|
);
|
||||||
headers.insert("X-Test-Header".to_string(), "test-value".to_string());
|
headers.insert("X-Test-Header".to_string(), "test-value".to_string());
|
||||||
headers.insert("Accept".to_string(), "application/json".to_string());
|
headers.insert("Accept".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder().with_default_headers(headers).build();
|
||||||
.with_default_headers(headers)
|
let result = client
|
||||||
.build();
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
let result = client.request("https://httpbin.org/get").await;
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/client/headers".to_string(),
|
endpoint: "/test/client/headers".to_string(),
|
||||||
@@ -221,19 +229,25 @@ async fn test_headers_client() -> Json<TestResponse> {
|
|||||||
/// Test combined configuration options
|
/// Test combined configuration options
|
||||||
async fn test_combined_config() -> Json<TestResponse> {
|
async fn test_combined_config() -> Json<TestResponse> {
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("User-Agent".to_string(), "hyper-custom-cert-combined/1.0".to_string());
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"hyper-custom-cert-combined/1.0".to_string(),
|
||||||
|
);
|
||||||
headers.insert("X-Combined-Test".to_string(), "true".to_string());
|
headers.insert("X-Combined-Test".to_string(), "true".to_string());
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder()
|
||||||
.with_timeout(Duration::from_secs(30))
|
.with_timeout(Duration::from_secs(30))
|
||||||
.with_default_headers(headers)
|
.with_default_headers(headers)
|
||||||
.build();
|
.build();
|
||||||
let result = client.request("https://httpbin.org/get").await;
|
let result = client
|
||||||
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/client/combined".to_string(),
|
endpoint: "/test/client/combined".to_string(),
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
message: "HttpClient with combined configuration (timeout + headers) works correctly".to_string(),
|
message: "HttpClient with combined configuration (timeout + headers) works correctly"
|
||||||
|
.to_string(),
|
||||||
features_tested: vec!["timeout-config".to_string(), "custom-headers".to_string()],
|
features_tested: vec!["timeout-config".to_string(), "custom-headers".to_string()],
|
||||||
error: match result {
|
error: match result {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
@@ -331,7 +345,8 @@ async fn test_insecure_feature() -> Json<TestResponse> {
|
|||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/features/insecure".to_string(),
|
endpoint: "/test/features/insecure".to_string(),
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
message: "insecure-dangerous feature is working (DO NOT USE IN PRODUCTION!)".to_string(),
|
message: "insecure-dangerous feature is working (DO NOT USE IN PRODUCTION!)"
|
||||||
|
.to_string(),
|
||||||
features_tested: vec!["insecure-dangerous".to_string()],
|
features_tested: vec!["insecure-dangerous".to_string()],
|
||||||
error: match (result, result2) {
|
error: match (result, result2) {
|
||||||
(Ok(_), Ok(_)) => None,
|
(Ok(_), Ok(_)) => None,
|
||||||
@@ -345,7 +360,8 @@ async fn test_insecure_feature() -> Json<TestResponse> {
|
|||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/features/insecure".to_string(),
|
endpoint: "/test/features/insecure".to_string(),
|
||||||
status: "skipped".to_string(),
|
status: "skipped".to_string(),
|
||||||
message: "insecure-dangerous feature is not enabled (this is good for security!)".to_string(),
|
message: "insecure-dangerous feature is not enabled (this is good for security!)"
|
||||||
|
.to_string(),
|
||||||
features_tested: vec![],
|
features_tested: vec![],
|
||||||
error: Some("Feature not enabled".to_string()),
|
error: Some("Feature not enabled".to_string()),
|
||||||
})
|
})
|
||||||
@@ -359,7 +375,9 @@ async fn test_insecure_feature() -> Json<TestResponse> {
|
|||||||
/// Test HTTP GET method
|
/// Test HTTP GET method
|
||||||
async fn test_get_method() -> Json<TestResponse> {
|
async fn test_get_method() -> Json<TestResponse> {
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
let result = client.request("https://httpbin.org/get").await;
|
let result = client
|
||||||
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/methods/get".to_string(),
|
endpoint: "/test/methods/get".to_string(),
|
||||||
@@ -377,12 +395,17 @@ async fn test_get_method() -> Json<TestResponse> {
|
|||||||
async fn test_post_method(Json(payload): Json<PostData>) -> Json<TestResponse> {
|
async fn test_post_method(Json(payload): Json<PostData>) -> Json<TestResponse> {
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
let body = serde_json::to_vec(&payload).unwrap_or_default();
|
let body = serde_json::to_vec(&payload).unwrap_or_default();
|
||||||
let result = client.post("https://httpbin.org/post", &body).await;
|
let result = client
|
||||||
|
.post_with_options("https://httpbin.org/post", &body, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/methods/post".to_string(),
|
endpoint: "/test/methods/post".to_string(),
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
message: format!("HTTP POST method test completed with data: {}", payload.data),
|
message: format!(
|
||||||
|
"HTTP POST method test completed with data: {}",
|
||||||
|
payload.data
|
||||||
|
),
|
||||||
features_tested: vec!["post-request".to_string()],
|
features_tested: vec!["post-request".to_string()],
|
||||||
error: match result {
|
error: match result {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
@@ -395,12 +418,17 @@ async fn test_post_method(Json(payload): Json<PostData>) -> Json<TestResponse> {
|
|||||||
async fn test_put_method(Json(payload): Json<PostData>) -> Json<TestResponse> {
|
async fn test_put_method(Json(payload): Json<PostData>) -> Json<TestResponse> {
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
let body = serde_json::to_vec(&payload).unwrap_or_default();
|
let body = serde_json::to_vec(&payload).unwrap_or_default();
|
||||||
let result = client.post("https://httpbin.org/put", &body).await;
|
let result = client
|
||||||
|
.post_with_options("https://httpbin.org/put", &body, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/methods/put".to_string(),
|
endpoint: "/test/methods/put".to_string(),
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
message: format!("HTTP PUT method test completed (simulated via POST) with data: {}", payload.data),
|
message: format!(
|
||||||
|
"HTTP PUT method test completed (simulated via POST) with data: {}",
|
||||||
|
payload.data
|
||||||
|
),
|
||||||
features_tested: vec!["put-request-simulation".to_string()],
|
features_tested: vec!["put-request-simulation".to_string()],
|
||||||
error: match result {
|
error: match result {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
@@ -412,7 +440,9 @@ async fn test_put_method(Json(payload): Json<PostData>) -> Json<TestResponse> {
|
|||||||
/// Test HTTP DELETE method (simulated via GET since library doesn't have DELETE yet)
|
/// Test HTTP DELETE method (simulated via GET since library doesn't have DELETE yet)
|
||||||
async fn test_delete_method() -> Json<TestResponse> {
|
async fn test_delete_method() -> Json<TestResponse> {
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
let result = client.request("https://httpbin.org/delete").await;
|
let result = client
|
||||||
|
.request_with_options("https://httpbin.org/delete", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/methods/delete".to_string(),
|
endpoint: "/test/methods/delete".to_string(),
|
||||||
@@ -440,7 +470,7 @@ async fn test_custom_ca() -> Json<TestResponse> {
|
|||||||
.with_timeout(Duration::from_secs(10))
|
.with_timeout(Duration::from_secs(10))
|
||||||
.with_root_ca_pem(ca_pem)
|
.with_root_ca_pem(ca_pem)
|
||||||
.build();
|
.build();
|
||||||
let result = client.request("https://httpbin.org/get");
|
let result = client.request_with_options("https://httpbin.org/get", None);
|
||||||
|
|
||||||
let awaited = result.await;
|
let awaited = result.await;
|
||||||
|
|
||||||
@@ -472,18 +502,17 @@ async fn test_cert_pinning() -> Json<TestResponse> {
|
|||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
{
|
{
|
||||||
// Example SHA256 fingerprints (these are demo values)
|
// Example SHA256 fingerprints (these are demo values)
|
||||||
let pins = vec![
|
let pins = vec![[
|
||||||
[0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f,
|
0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, 0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef,
|
||||||
0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef, 0xff, 0x0f,
|
0xff, 0x0f, 0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f, 0x9f, 0xaf, 0xbf, 0xcf,
|
||||||
0x1f, 0x2f, 0x3f, 0x4f, 0x5f, 0x6f, 0x7f, 0x8f,
|
0xdf, 0xef, 0xff, 0x0f,
|
||||||
0x9f, 0xaf, 0xbf, 0xcf, 0xdf, 0xef, 0xff, 0x0f],
|
]];
|
||||||
];
|
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder()
|
||||||
.with_timeout(Duration::from_secs(10))
|
.with_timeout(Duration::from_secs(10))
|
||||||
.with_pinned_cert_sha256(pins)
|
.with_pinned_cert_sha256(pins)
|
||||||
.build();
|
.build();
|
||||||
let result = client.request("https://httpbin.org/get");
|
let result = client.request_with_options("https://httpbin.org/get", None);
|
||||||
|
|
||||||
let awaited = result.await;
|
let awaited = result.await;
|
||||||
|
|
||||||
@@ -515,7 +544,7 @@ async fn test_self_signed() -> Json<TestResponse> {
|
|||||||
#[cfg(feature = "insecure-dangerous")]
|
#[cfg(feature = "insecure-dangerous")]
|
||||||
{
|
{
|
||||||
let client = HttpClient::with_self_signed_certs();
|
let client = HttpClient::with_self_signed_certs();
|
||||||
let result = client.request("https://self-signed.badssl.com/");
|
let result = client.request_with_options("https://self-signed.badssl.com/", None);
|
||||||
|
|
||||||
let awaited = result.await;
|
let awaited = result.await;
|
||||||
|
|
||||||
@@ -535,7 +564,8 @@ async fn test_self_signed() -> Json<TestResponse> {
|
|||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/tls/self-signed".to_string(),
|
endpoint: "/test/tls/self-signed".to_string(),
|
||||||
status: "skipped".to_string(),
|
status: "skipped".to_string(),
|
||||||
message: "Self-signed test requires insecure-dangerous feature (good for security!)".to_string(),
|
message: "Self-signed test requires insecure-dangerous feature (good for security!)"
|
||||||
|
.to_string(),
|
||||||
features_tested: vec![],
|
features_tested: vec![],
|
||||||
error: Some("insecure-dangerous feature not enabled".to_string()),
|
error: Some("insecure-dangerous feature not enabled".to_string()),
|
||||||
})
|
})
|
||||||
@@ -549,10 +579,8 @@ async fn test_self_signed() -> Json<TestResponse> {
|
|||||||
/// Test custom timeout configuration
|
/// Test custom timeout configuration
|
||||||
async fn test_custom_timeout(Path(seconds): Path<u64>) -> Json<TestResponse> {
|
async fn test_custom_timeout(Path(seconds): Path<u64>) -> Json<TestResponse> {
|
||||||
let timeout_duration = Duration::from_secs(seconds);
|
let timeout_duration = Duration::from_secs(seconds);
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder().with_timeout(timeout_duration).build();
|
||||||
.with_timeout(timeout_duration)
|
let result = client.request_with_options("https://httpbin.org/delay/1", None);
|
||||||
.build();
|
|
||||||
let result = client.request("https://httpbin.org/delay/1");
|
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: format!("/test/config/timeout/{}", seconds),
|
endpoint: format!("/test/config/timeout/{}", seconds),
|
||||||
@@ -571,26 +599,29 @@ async fn test_custom_headers(Path(header_count): Path<usize>) -> Json<TestRespon
|
|||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
|
|
||||||
for i in 0..header_count {
|
for i in 0..header_count {
|
||||||
headers.insert(
|
headers.insert(format!("X-Test-Header-{}", i), format!("test-value-{}", i));
|
||||||
format!("X-Test-Header-{}", i),
|
|
||||||
format!("test-value-{}", i),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add some standard headers
|
// Add some standard headers
|
||||||
headers.insert("User-Agent".to_string(), "hyper-custom-cert-headers-test/1.0".to_string());
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"hyper-custom-cert-headers-test/1.0".to_string(),
|
||||||
|
);
|
||||||
headers.insert("Accept".to_string(), "application/json".to_string());
|
headers.insert("Accept".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder()
|
||||||
.with_timeout(Duration::from_secs(10))
|
.with_timeout(Duration::from_secs(10))
|
||||||
.with_default_headers(headers)
|
.with_default_headers(headers)
|
||||||
.build();
|
.build();
|
||||||
let result = client.request("https://httpbin.org/headers");
|
let result = client.request_with_options("https://httpbin.org/headers", None);
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: format!("/test/config/headers/{}", header_count),
|
endpoint: format!("/test/config/headers/{}", header_count),
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
message: format!("Custom headers test with {} headers completed", header_count + 2),
|
message: format!(
|
||||||
|
"Custom headers test with {} headers completed",
|
||||||
|
header_count + 2
|
||||||
|
),
|
||||||
features_tested: vec!["custom-headers".to_string()],
|
features_tested: vec!["custom-headers".to_string()],
|
||||||
error: match result.await {
|
error: match result.await {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
@@ -609,12 +640,17 @@ async fn test_timeout_error() -> Json<TestResponse> {
|
|||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder()
|
||||||
.with_timeout(Duration::from_millis(1))
|
.with_timeout(Duration::from_millis(1))
|
||||||
.build();
|
.build();
|
||||||
let result = client.request("https://httpbin.org/delay/5");
|
let result = client.request_with_options("https://httpbin.org/delay/5", None);
|
||||||
|
|
||||||
let awaited = result.await;
|
let awaited = result.await;
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/errors/timeout".to_string(),
|
endpoint: "/test/errors/timeout".to_string(),
|
||||||
status: if awaited.is_err() { "success" } else { "unexpected" }.to_string(),
|
status: if awaited.is_err() {
|
||||||
|
"success"
|
||||||
|
} else {
|
||||||
|
"unexpected"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
message: "Timeout error simulation test completed".to_string(),
|
message: "Timeout error simulation test completed".to_string(),
|
||||||
features_tested: vec!["timeout-error-handling".to_string()],
|
features_tested: vec!["timeout-error-handling".to_string()],
|
||||||
error: match awaited {
|
error: match awaited {
|
||||||
@@ -627,13 +663,18 @@ async fn test_timeout_error() -> Json<TestResponse> {
|
|||||||
/// Test invalid URL handling
|
/// Test invalid URL handling
|
||||||
async fn test_invalid_url() -> Json<TestResponse> {
|
async fn test_invalid_url() -> Json<TestResponse> {
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
let result = client.request("invalid-url-format");
|
let result = client.request_with_options("invalid-url-format", None);
|
||||||
|
|
||||||
let awaited = result.await;
|
let awaited = result.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/errors/invalid-url".to_string(),
|
endpoint: "/test/errors/invalid-url".to_string(),
|
||||||
status: if awaited.is_err() { "success" } else { "unexpected" }.to_string(),
|
status: if awaited.is_err() {
|
||||||
|
"success"
|
||||||
|
} else {
|
||||||
|
"unexpected"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
message: "Invalid URL error simulation test completed".to_string(),
|
message: "Invalid URL error simulation test completed".to_string(),
|
||||||
features_tested: vec!["url-validation".to_string()],
|
features_tested: vec!["url-validation".to_string()],
|
||||||
error: match awaited {
|
error: match awaited {
|
||||||
@@ -649,12 +690,17 @@ async fn test_connection_error() -> Json<TestResponse> {
|
|||||||
.with_timeout(Duration::from_secs(5))
|
.with_timeout(Duration::from_secs(5))
|
||||||
.build();
|
.build();
|
||||||
// Try to connect to a non-existent host
|
// Try to connect to a non-existent host
|
||||||
let result = client.request("https://non-existent-host-12345.example.com/");
|
let result = client.request_with_options("https://non-existent-host-12345.example.com/", None);
|
||||||
let awaited = result.await;
|
let awaited = result.await;
|
||||||
|
|
||||||
Json(TestResponse {
|
Json(TestResponse {
|
||||||
endpoint: "/test/errors/connection".to_string(),
|
endpoint: "/test/errors/connection".to_string(),
|
||||||
status: if awaited.is_err() { "success" } else { "unexpected" }.to_string(),
|
status: if awaited.is_err() {
|
||||||
|
"success"
|
||||||
|
} else {
|
||||||
|
"unexpected"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
message: "Connection error simulation test completed".to_string(),
|
message: "Connection error simulation test completed".to_string(),
|
||||||
features_tested: vec!["connection-error-handling".to_string()],
|
features_tested: vec!["connection-error-handling".to_string()],
|
||||||
error: match awaited {
|
error: match awaited {
|
||||||
@@ -693,9 +739,12 @@ async fn status_check() -> Json<Value> {
|
|||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
// Test basic client creation to verify library is working
|
// Test basic client creation to verify library is working
|
||||||
let client_test = match HttpClient::new().request("https://httpbin.org/get").await {
|
let client_test = match HttpClient::new()
|
||||||
|
.request_with_options("https://httpbin.org/get", None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(_) => "operational",
|
Ok(_) => "operational",
|
||||||
Err(_) => "degraded"
|
Err(_) => "degraded",
|
||||||
};
|
};
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hyper-custom-cert"
|
name = "hyper-custom-cert"
|
||||||
version = "0.3.2"
|
version = "0.3.6"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
description = "A small, ergonomic HTTP client wrapper around hyper with optional support for custom Root CAs and a dev-only insecure mode for self-signed certificates."
|
description = "A small, ergonomic HTTP client wrapper around hyper with optional support for custom Root CAs and a dev-only insecure mode for self-signed certificates."
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/seemueller-io/hyper-custom-cert"
|
repository = "https://github.com/seemueller-io/hyper-custom-cert"
|
||||||
|
@@ -15,7 +15,7 @@ async fn main() {
|
|||||||
|
|
||||||
// Demonstrate a request (now returns HttpResponse with raw body data)
|
// Demonstrate a request (now returns HttpResponse with raw body data)
|
||||||
let _response = client
|
let _response = client
|
||||||
.request("https://example.com")
|
.request_with_options("https://example.com", None)
|
||||||
.await
|
.await
|
||||||
.expect("request should succeed on native targets");
|
.expect("request should succeed on native targets");
|
||||||
|
|
||||||
@@ -30,7 +30,9 @@ async fn main() {
|
|||||||
.with_timeout(Duration::from_secs(10))
|
.with_timeout(Duration::from_secs(10))
|
||||||
.with_root_ca_pem(ca_pem)
|
.with_root_ca_pem(ca_pem)
|
||||||
.build();
|
.build();
|
||||||
let _ = _rustls_client.request("https://private.local").await;
|
let _ = _rustls_client
|
||||||
|
.request_with_options("https://private.local", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Option 2: Load CA certificate from a file path
|
// Option 2: Load CA certificate from a file path
|
||||||
// Note: This will panic if the file doesn't exist - ensure your cert file is available
|
// Note: This will panic if the file doesn't exist - ensure your cert file is available
|
||||||
@@ -47,13 +49,17 @@ async fn main() {
|
|||||||
{
|
{
|
||||||
// Shortcut:
|
// Shortcut:
|
||||||
let _dev_client = HttpClient::with_self_signed_certs();
|
let _dev_client = HttpClient::with_self_signed_certs();
|
||||||
let _ = _dev_client.request("https://localhost:8443").await;
|
let _ = _dev_client
|
||||||
|
.request_with_options("https://localhost:8443", None)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Or explicit builder method:
|
// Or explicit builder method:
|
||||||
let _dev_client2 = HttpClient::builder()
|
let _dev_client2 = HttpClient::builder()
|
||||||
.insecure_accept_invalid_certs(true)
|
.insecure_accept_invalid_certs(true)
|
||||||
.build();
|
.build();
|
||||||
let _ = _dev_client2.request("https://localhost:8443").await;
|
let _ = _dev_client2
|
||||||
|
.request_with_options("https://localhost:8443", None)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Example finished. See README for feature flags and commands.");
|
println!("Example finished. See README for feature flags and commands.");
|
||||||
|
@@ -36,10 +36,64 @@ use std::path::Path;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use hyper::{body::Incoming, Request, Response, StatusCode, Uri, Method};
|
use http_body_util::BodyExt;
|
||||||
|
use hyper::{body::Incoming, Method, Request, Response, StatusCode, Uri};
|
||||||
use hyper_util::client::legacy::Client;
|
use hyper_util::client::legacy::Client;
|
||||||
use hyper_util::rt::TokioExecutor;
|
use hyper_util::rt::TokioExecutor;
|
||||||
use http_body_util::BodyExt;
|
|
||||||
|
/// Options for controlling HTTP requests.
|
||||||
|
///
|
||||||
|
/// This struct provides a flexible interface for configuring individual
|
||||||
|
/// HTTP requests without modifying the client's default settings.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Adding custom headers to a specific request:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use hyper_custom_cert::{HttpClient, RequestOptions};
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
///
|
||||||
|
/// // Create request-specific headers
|
||||||
|
/// let mut headers = HashMap::new();
|
||||||
|
/// headers.insert("x-request-id".to_string(), "123456".to_string());
|
||||||
|
///
|
||||||
|
/// // Create request options with these headers
|
||||||
|
/// let options = RequestOptions::new()
|
||||||
|
/// .with_headers(headers);
|
||||||
|
///
|
||||||
|
/// // Make request with custom options
|
||||||
|
/// # async {
|
||||||
|
/// let client = HttpClient::new();
|
||||||
|
/// let _response = client.request_with_options("https://example.com", Some(options)).await;
|
||||||
|
/// # };
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct RequestOptions {
|
||||||
|
/// Headers to add to this specific request
|
||||||
|
pub headers: Option<HashMap<String, String>>,
|
||||||
|
/// Override the client's default timeout for this request
|
||||||
|
pub timeout: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestOptions {
|
||||||
|
/// Create a new empty RequestOptions with default values.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
RequestOptions::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add custom headers to this request.
|
||||||
|
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
|
||||||
|
self.headers = Some(headers);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the client's default timeout for this request.
|
||||||
|
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.timeout = Some(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// HTTP response with raw body data exposed as bytes.
|
/// HTTP response with raw body data exposed as bytes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -131,7 +185,7 @@ impl From<hyper_util::client::legacy::Error> for ClientError {
|
|||||||
/// Build a client with a custom timeout and default headers:
|
/// Build a client with a custom timeout and default headers:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use hyper_custom_cert::HttpClient;
|
/// use hyper_custom_cert::{HttpClient, RequestOptions};
|
||||||
/// use std::time::Duration;
|
/// use std::time::Duration;
|
||||||
/// use std::collections::HashMap;
|
/// use std::collections::HashMap;
|
||||||
///
|
///
|
||||||
@@ -144,7 +198,7 @@ impl From<hyper_util::client::legacy::Error> for ClientError {
|
|||||||
/// .build();
|
/// .build();
|
||||||
///
|
///
|
||||||
/// // Placeholder call; does not perform I/O in this crate.
|
/// // Placeholder call; does not perform I/O in this crate.
|
||||||
/// let _ = client.request("https://example.com");
|
/// let _ = client.request_with_options("https://example.com", None);
|
||||||
/// ```
|
/// ```
|
||||||
pub struct HttpClient {
|
pub struct HttpClient {
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
@@ -234,12 +288,75 @@ impl HttpClient {
|
|||||||
/// This method constructs a `hyper::Request` with the GET method and any
|
/// This method constructs a `hyper::Request` with the GET method and any
|
||||||
/// default headers configured on the client, then dispatches it via `perform_request`.
|
/// default headers configured on the client, then dispatches it via `perform_request`.
|
||||||
/// Returns HttpResponse with raw body data exposed without any permutations.
|
/// Returns HttpResponse with raw body data exposed without any permutations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `url` - The URL to request
|
||||||
|
/// * `options` - Optional request options to customize this specific request
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # async {
|
||||||
|
/// use hyper_custom_cert::{HttpClient, RequestOptions};
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
///
|
||||||
|
/// let client = HttpClient::new();
|
||||||
|
///
|
||||||
|
/// // Basic request with no custom options
|
||||||
|
/// let response1 = client.request_with_options("https://example.com", None).await?;
|
||||||
|
///
|
||||||
|
/// // Request with custom options
|
||||||
|
/// let mut headers = HashMap::new();
|
||||||
|
/// headers.insert("x-request-id".into(), "abc123".into());
|
||||||
|
/// let options = RequestOptions::new().with_headers(headers);
|
||||||
|
/// let response2 = client.request_with_options("https://example.com", Some(options)).await?;
|
||||||
|
/// # Ok::<(), hyper_custom_cert::ClientError>(())
|
||||||
|
/// # };
|
||||||
|
/// ```
|
||||||
|
#[deprecated(since = "0.4.0", note = "Use request(url, Some(options)) instead")]
|
||||||
pub async fn request(&self, url: &str) -> Result<HttpResponse, ClientError> {
|
pub async fn request(&self, url: &str) -> Result<HttpResponse, ClientError> {
|
||||||
|
self.request_with_options(url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a GET request and returns the raw response body.
|
||||||
|
/// This method constructs a `hyper::Request` with the GET method and any
|
||||||
|
/// default headers configured on the client, then dispatches it via `perform_request`.
|
||||||
|
/// Returns HttpResponse with raw body data exposed without any permutations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `url` - The URL to request
|
||||||
|
/// * `options` - Optional request options to customize this specific request
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # async {
|
||||||
|
/// use hyper_custom_cert::{HttpClient, RequestOptions};
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
///
|
||||||
|
/// let client = HttpClient::new();
|
||||||
|
///
|
||||||
|
/// // Basic request with no custom options
|
||||||
|
/// let response1 = client.request_with_options("https://example.com", None).await?;
|
||||||
|
///
|
||||||
|
/// // Request with custom options
|
||||||
|
/// let mut headers = HashMap::new();
|
||||||
|
/// headers.insert("x-request-id".into(), "abc123".into());
|
||||||
|
/// let options = RequestOptions::new().with_headers(headers);
|
||||||
|
/// let response2 = client.request_with_options("https://example.com", Some(options)).await?;
|
||||||
|
/// # Ok::<(), hyper_custom_cert::ClientError>(())
|
||||||
|
/// # };
|
||||||
|
/// ```
|
||||||
|
pub async fn request_with_options(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
options: Option<RequestOptions>,
|
||||||
|
) -> Result<HttpResponse, ClientError> {
|
||||||
let uri: Uri = url.parse()?;
|
let uri: Uri = url.parse()?;
|
||||||
|
|
||||||
let req = Request::builder()
|
let req = Request::builder().method(Method::GET).uri(uri);
|
||||||
.method(Method::GET)
|
|
||||||
.uri(uri);
|
|
||||||
|
|
||||||
// Add default headers to the request. This ensures that any headers
|
// Add default headers to the request. This ensures that any headers
|
||||||
// set during the client's construction (e.g., API keys, User-Agent)
|
// set during the client's construction (e.g., API keys, User-Agent)
|
||||||
@@ -249,9 +366,42 @@ impl HttpClient {
|
|||||||
req = req.header(key, value);
|
req = req.header(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add any request-specific headers from options
|
||||||
|
if let Some(options) = &options {
|
||||||
|
if let Some(headers) = &options.headers {
|
||||||
|
for (key, value) in headers {
|
||||||
|
req = req.header(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let req = req.body(http_body_util::Empty::<Bytes>::new())?;
|
let req = req.body(http_body_util::Empty::<Bytes>::new())?;
|
||||||
|
|
||||||
self.perform_request(req).await
|
// If options contain a timeout, temporarily modify self to use it
|
||||||
|
// This is a bit of a hack since we can't modify perform_request easily
|
||||||
|
if let Some(opts) = &options {
|
||||||
|
if let Some(timeout) = opts.timeout {
|
||||||
|
// Create a copy of self with the new timeout
|
||||||
|
let client_copy = HttpClient {
|
||||||
|
timeout,
|
||||||
|
default_headers: self.default_headers.clone(),
|
||||||
|
#[cfg(feature = "insecure-dangerous")]
|
||||||
|
accept_invalid_certs: self.accept_invalid_certs,
|
||||||
|
root_ca_pem: self.root_ca_pem.clone(),
|
||||||
|
#[cfg(feature = "rustls")]
|
||||||
|
pinned_cert_sha256: self.pinned_cert_sha256.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the modified client for this request only
|
||||||
|
client_copy.perform_request(req).await
|
||||||
|
} else {
|
||||||
|
// No timeout override, use normal client
|
||||||
|
self.perform_request(req).await
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No options, use normal client
|
||||||
|
self.perform_request(req).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs a POST request with the given body and returns the raw response.
|
/// Performs a POST request with the given body and returns the raw response.
|
||||||
@@ -259,12 +409,92 @@ impl HttpClient {
|
|||||||
/// operation, handles the request body conversion to `Bytes`, and applies
|
/// operation, handles the request body conversion to `Bytes`, and applies
|
||||||
/// default headers before calling `perform_request`.
|
/// default headers before calling `perform_request`.
|
||||||
/// Returns HttpResponse with raw body data exposed without any permutations.
|
/// Returns HttpResponse with raw body data exposed without any permutations.
|
||||||
pub async fn post<B: AsRef<[u8]>>(&self, url: &str, body: B) -> Result<HttpResponse, ClientError> {
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `url` - The URL to request
|
||||||
|
/// * `body` - The body content to send with the POST request
|
||||||
|
/// * `options` - Optional request options to customize this specific request
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # async {
|
||||||
|
/// use hyper_custom_cert::{HttpClient, RequestOptions};
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
/// use std::time::Duration;
|
||||||
|
///
|
||||||
|
/// let client = HttpClient::new();
|
||||||
|
///
|
||||||
|
/// // Basic POST request with no custom options
|
||||||
|
/// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?;
|
||||||
|
///
|
||||||
|
/// // POST request with custom options
|
||||||
|
/// let mut headers = HashMap::new();
|
||||||
|
/// headers.insert("Content-Type".into(), "application/json".into());
|
||||||
|
/// let options = RequestOptions::new()
|
||||||
|
/// .with_headers(headers)
|
||||||
|
/// .with_timeout(Duration::from_secs(5));
|
||||||
|
/// let response2 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", Some(options)).await?;
|
||||||
|
/// # Ok::<(), hyper_custom_cert::ClientError>(())
|
||||||
|
/// # };
|
||||||
|
/// ```
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.4.0",
|
||||||
|
note = "Use post_with_options(url, body, Some(options)) instead"
|
||||||
|
)]
|
||||||
|
pub async fn post<B: AsRef<[u8]>>(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
body: B,
|
||||||
|
) -> Result<HttpResponse, ClientError> {
|
||||||
|
self.post_with_options(url, body, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a POST request with the given body and returns the raw response.
|
||||||
|
/// Similar to `request`, this method builds a `hyper::Request` for a POST
|
||||||
|
/// operation, handles the request body conversion to `Bytes`, and applies
|
||||||
|
/// default headers before calling `perform_request`.
|
||||||
|
/// Returns HttpResponse with raw body data exposed without any permutations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `url` - The URL to request
|
||||||
|
/// * `body` - The body content to send with the POST request
|
||||||
|
/// * `options` - Optional request options to customize this specific request
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # async {
|
||||||
|
/// use hyper_custom_cert::{HttpClient, RequestOptions};
|
||||||
|
/// use std::collections::HashMap;
|
||||||
|
/// use std::time::Duration;
|
||||||
|
///
|
||||||
|
/// let client = HttpClient::new();
|
||||||
|
///
|
||||||
|
/// // Basic POST request with no custom options
|
||||||
|
/// let response1 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", None).await?;
|
||||||
|
///
|
||||||
|
/// // POST request with custom options
|
||||||
|
/// let mut headers = HashMap::new();
|
||||||
|
/// headers.insert("Content-Type".into(), "application/json".into());
|
||||||
|
/// let options = RequestOptions::new()
|
||||||
|
/// .with_headers(headers)
|
||||||
|
/// .with_timeout(Duration::from_secs(5));
|
||||||
|
/// let response2 = client.post_with_options("https://example.com/api", b"{\"key\":\"value\"}", Some(options)).await?;
|
||||||
|
/// # Ok::<(), hyper_custom_cert::ClientError>(())
|
||||||
|
/// # };
|
||||||
|
/// ```
|
||||||
|
pub async fn post_with_options<B: AsRef<[u8]>>(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
body: B,
|
||||||
|
options: Option<RequestOptions>,
|
||||||
|
) -> Result<HttpResponse, ClientError> {
|
||||||
let uri: Uri = url.parse()?;
|
let uri: Uri = url.parse()?;
|
||||||
|
|
||||||
let req = Request::builder()
|
let req = Request::builder().method(Method::POST).uri(uri);
|
||||||
.method(Method::POST)
|
|
||||||
.uri(uri);
|
|
||||||
|
|
||||||
// Add default headers to the request for consistency across client operations.
|
// Add default headers to the request for consistency across client operations.
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
@@ -272,10 +502,43 @@ impl HttpClient {
|
|||||||
req = req.header(key, value);
|
req = req.header(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add any request-specific headers from options
|
||||||
|
if let Some(options) = &options {
|
||||||
|
if let Some(headers) = &options.headers {
|
||||||
|
for (key, value) in headers {
|
||||||
|
req = req.header(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let body_bytes = Bytes::copy_from_slice(body.as_ref());
|
let body_bytes = Bytes::copy_from_slice(body.as_ref());
|
||||||
let req = req.body(http_body_util::Full::new(body_bytes))?;
|
let req = req.body(http_body_util::Full::new(body_bytes))?;
|
||||||
|
|
||||||
self.perform_request(req).await
|
// If options contain a timeout, temporarily modify self to use it
|
||||||
|
// This is a bit of a hack since we can't modify perform_request easily
|
||||||
|
if let Some(opts) = &options {
|
||||||
|
if let Some(timeout) = opts.timeout {
|
||||||
|
// Create a copy of self with the new timeout
|
||||||
|
let client_copy = HttpClient {
|
||||||
|
timeout,
|
||||||
|
default_headers: self.default_headers.clone(),
|
||||||
|
#[cfg(feature = "insecure-dangerous")]
|
||||||
|
accept_invalid_certs: self.accept_invalid_certs,
|
||||||
|
root_ca_pem: self.root_ca_pem.clone(),
|
||||||
|
#[cfg(feature = "rustls")]
|
||||||
|
pinned_cert_sha256: self.pinned_cert_sha256.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the modified client for this request only
|
||||||
|
client_copy.perform_request(req).await
|
||||||
|
} else {
|
||||||
|
// No timeout override, use normal client
|
||||||
|
self.perform_request(req).await
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No options, use normal client
|
||||||
|
self.perform_request(req).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to perform HTTP requests using the configured settings.
|
/// Helper method to perform HTTP requests using the configured settings.
|
||||||
@@ -307,8 +570,9 @@ impl HttpClient {
|
|||||||
// Create a TLS connector that accepts invalid certificates
|
// Create a TLS connector that accepts invalid certificates
|
||||||
let mut tls_builder = native_tls::TlsConnector::builder();
|
let mut tls_builder = native_tls::TlsConnector::builder();
|
||||||
tls_builder.danger_accept_invalid_certs(true);
|
tls_builder.danger_accept_invalid_certs(true);
|
||||||
let tls_connector = tls_builder.build()
|
let tls_connector = tls_builder.build().map_err(|e| {
|
||||||
.map_err(|e| ClientError::TlsError(format!("Failed to build TLS connector: {}", e)))?;
|
ClientError::TlsError(format!("Failed to build TLS connector: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Create the tokio-native-tls connector
|
// Create the tokio-native-tls connector
|
||||||
let tokio_connector = tokio_native_tls::TlsConnector::from(tls_connector);
|
let tokio_connector = tokio_native_tls::TlsConnector::from(tls_connector);
|
||||||
@@ -316,12 +580,10 @@ impl HttpClient {
|
|||||||
// Create the HTTPS connector using the HTTP and TLS connectors
|
// Create the HTTPS connector using the HTTP and TLS connectors
|
||||||
let connector = hyper_tls::HttpsConnector::from((http_connector, tokio_connector));
|
let connector = hyper_tls::HttpsConnector::from((http_connector, tokio_connector));
|
||||||
|
|
||||||
let client = Client::builder(TokioExecutor::new())
|
let client = Client::builder(TokioExecutor::new()).build(connector);
|
||||||
.build(connector);
|
|
||||||
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
|
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
|
||||||
?;
|
|
||||||
return self.build_response(resp).await;
|
return self.build_response(resp).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +592,7 @@ impl HttpClient {
|
|||||||
let client = Client::builder(TokioExecutor::new()).build(connector);
|
let client = Client::builder(TokioExecutor::new()).build(connector);
|
||||||
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
|
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
|
||||||
?;
|
|
||||||
self.build_response(resp).await
|
self.build_response(resp).await
|
||||||
}
|
}
|
||||||
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
|
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
|
||||||
@@ -350,7 +611,10 @@ impl HttpClient {
|
|||||||
// Add each cert to the root store
|
// Add each cert to the root store
|
||||||
for cert in &native_certs.certs {
|
for cert in &native_certs.certs {
|
||||||
if let Err(e) = root_cert_store.add(cert.clone()) {
|
if let Err(e) = root_cert_store.add(cert.clone()) {
|
||||||
return Err(ClientError::TlsError(format!("Failed to add native cert to root store: {}", e)));
|
return Err(ClientError::TlsError(format!(
|
||||||
|
"Failed to add native cert to root store: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,17 +624,26 @@ impl HttpClient {
|
|||||||
for cert_result in rustls_pemfile::certs(&mut reader) {
|
for cert_result in rustls_pemfile::certs(&mut reader) {
|
||||||
match cert_result {
|
match cert_result {
|
||||||
Ok(cert) => {
|
Ok(cert) => {
|
||||||
root_cert_store.add(cert)
|
root_cert_store.add(cert).map_err(|e| {
|
||||||
.map_err(|e| ClientError::TlsError(format!("Failed to add custom cert to root store: {}", e)))?;
|
ClientError::TlsError(format!(
|
||||||
},
|
"Failed to add custom cert to root store: {}",
|
||||||
Err(e) => return Err(ClientError::TlsError(format!("Failed to parse PEM cert: {}", e))),
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ClientError::TlsError(format!(
|
||||||
|
"Failed to parse PEM cert: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure rustls
|
// Configure rustls
|
||||||
let mut config_builder = rustls::ClientConfig::builder()
|
let mut config_builder =
|
||||||
.with_root_certificates(root_cert_store);
|
rustls::ClientConfig::builder().with_root_certificates(root_cert_store);
|
||||||
|
|
||||||
let rustls_config = config_builder.with_no_client_auth();
|
let rustls_config = config_builder.with_no_client_auth();
|
||||||
|
|
||||||
@@ -381,11 +654,11 @@ impl HttpClient {
|
|||||||
// and NEVER in production environments. This creates a vulnerability to
|
// and NEVER in production environments. This creates a vulnerability to
|
||||||
// man-in-the-middle attacks and is extremely dangerous.
|
// man-in-the-middle attacks and is extremely dangerous.
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
|
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
|
||||||
|
use rustls::pki_types::UnixTime;
|
||||||
use rustls::DigitallySignedStruct;
|
use rustls::DigitallySignedStruct;
|
||||||
use rustls::SignatureScheme;
|
use rustls::SignatureScheme;
|
||||||
use rustls::pki_types::UnixTime;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// Override the certificate verifier with a no-op verifier that accepts all certificates
|
// Override the certificate verifier with a no-op verifier that accepts all certificates
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -446,7 +719,9 @@ impl HttpClient {
|
|||||||
|
|
||||||
// Set up the dangerous configuration with no certificate verification
|
// Set up the dangerous configuration with no certificate verification
|
||||||
let mut config = rustls_config.clone();
|
let mut config = rustls_config.clone();
|
||||||
config.dangerous().set_certificate_verifier(Arc::new(NoCertificateVerification {}));
|
config
|
||||||
|
.dangerous()
|
||||||
|
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
|
||||||
config
|
config
|
||||||
} else {
|
} else {
|
||||||
rustls_config
|
rustls_config
|
||||||
@@ -456,11 +731,13 @@ impl HttpClient {
|
|||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
let rustls_config = if let Some(ref pins) = self.pinned_cert_sha256 {
|
let rustls_config = if let Some(ref pins) = self.pinned_cert_sha256 {
|
||||||
// Implement certificate pinning by creating a custom certificate verifier
|
// Implement certificate pinning by creating a custom certificate verifier
|
||||||
use std::sync::Arc;
|
use rustls::client::danger::{
|
||||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
|
||||||
|
};
|
||||||
|
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||||
use rustls::DigitallySignedStruct;
|
use rustls::DigitallySignedStruct;
|
||||||
use rustls::SignatureScheme;
|
use rustls::SignatureScheme;
|
||||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
use std::sync::Arc;
|
||||||
|
|
||||||
// Create a custom certificate verifier that checks certificate pins
|
// Create a custom certificate verifier that checks certificate pins
|
||||||
struct CertificatePinner {
|
struct CertificatePinner {
|
||||||
@@ -478,10 +755,16 @@ impl HttpClient {
|
|||||||
now: UnixTime,
|
now: UnixTime,
|
||||||
) -> Result<ServerCertVerified, rustls::Error> {
|
) -> Result<ServerCertVerified, rustls::Error> {
|
||||||
// First, use the inner verifier to do standard verification
|
// First, use the inner verifier to do standard verification
|
||||||
self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)?;
|
self.inner.verify_server_cert(
|
||||||
|
end_entity,
|
||||||
|
intermediates,
|
||||||
|
server_name,
|
||||||
|
ocsp_response,
|
||||||
|
now,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Then verify the pin
|
// Then verify the pin
|
||||||
use sha2::{Sha256, Digest};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(end_entity.as_ref());
|
hasher.update(end_entity.as_ref());
|
||||||
@@ -495,7 +778,9 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we got here, none of the pins matched
|
// If we got here, none of the pins matched
|
||||||
Err(rustls::Error::General("Certificate pin verification failed".into()))
|
Err(rustls::Error::General(
|
||||||
|
"Certificate pin verification failed".into(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_tls12_signature(
|
fn verify_tls12_signature(
|
||||||
@@ -528,7 +813,12 @@ impl HttpClient {
|
|||||||
let default_verifier = rustls::client::WebPkiServerVerifier::builder()
|
let default_verifier = rustls::client::WebPkiServerVerifier::builder()
|
||||||
.with_root_certificates(root_cert_store.clone())
|
.with_root_certificates(root_cert_store.clone())
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| ClientError::TlsError(format!("Failed to build certificate verifier: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
ClientError::TlsError(format!(
|
||||||
|
"Failed to build certificate verifier: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
let cert_pinner = Arc::new(CertificatePinner {
|
let cert_pinner = Arc::new(CertificatePinner {
|
||||||
pins: pins.clone(),
|
pins: pins.clone(),
|
||||||
@@ -555,8 +845,7 @@ impl HttpClient {
|
|||||||
let client = Client::builder(TokioExecutor::new()).build(https_connector);
|
let client = Client::builder(TokioExecutor::new()).build(https_connector);
|
||||||
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
|
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
|
||||||
?;
|
|
||||||
self.build_response(resp).await
|
self.build_response(resp).await
|
||||||
}
|
}
|
||||||
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
||||||
@@ -569,8 +858,7 @@ impl HttpClient {
|
|||||||
let client = Client::builder(TokioExecutor::new()).build(connector);
|
let client = Client::builder(TokioExecutor::new()).build(connector);
|
||||||
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
let resp = tokio::time::timeout(self.timeout, client.request(req))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))?
|
.map_err(|_| ClientError::TlsError("Request timed out".to_string()))??;
|
||||||
?;
|
|
||||||
self.build_response(resp).await
|
self.build_response(resp).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,14 +898,43 @@ impl HttpClient {
|
|||||||
/// On wasm32 targets, runtime methods are stubbed and return
|
/// On wasm32 targets, runtime methods are stubbed and return
|
||||||
/// `ClientError::WasmNotImplemented` because browsers do not allow
|
/// `ClientError::WasmNotImplemented` because browsers do not allow
|
||||||
/// programmatic installation/trust of custom CAs.
|
/// programmatic installation/trust of custom CAs.
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.4.0",
|
||||||
|
note = "Use request_with_options(url, Some(options)) instead"
|
||||||
|
)]
|
||||||
pub fn request(&self, _url: &str) -> Result<(), ClientError> {
|
pub fn request(&self, _url: &str) -> Result<(), ClientError> {
|
||||||
Err(ClientError::WasmNotImplemented)
|
Err(ClientError::WasmNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// On wasm32 targets, runtime methods are stubbed and return
|
||||||
|
/// `ClientError::WasmNotImplemented` because browsers do not allow
|
||||||
|
/// programmatic installation/trust of custom CAs.
|
||||||
|
pub fn request_with_options(
|
||||||
|
&self,
|
||||||
|
_url: &str,
|
||||||
|
_options: Option<RequestOptions>,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
Err(ClientError::WasmNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
/// POST is also not implemented on wasm32 targets for the same reason.
|
/// POST is also not implemented on wasm32 targets for the same reason.
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.4.0",
|
||||||
|
note = "Use post_with_options(url, body, Some(options)) instead"
|
||||||
|
)]
|
||||||
pub fn post<B: AsRef<[u8]>>(&self, _url: &str, _body: B) -> Result<(), ClientError> {
|
pub fn post<B: AsRef<[u8]>>(&self, _url: &str, _body: B) -> Result<(), ClientError> {
|
||||||
Err(ClientError::WasmNotImplemented)
|
Err(ClientError::WasmNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// POST is also not implemented on wasm32 targets for the same reason.
|
||||||
|
pub fn post_with_options<B: AsRef<[u8]>>(
|
||||||
|
&self,
|
||||||
|
_url: &str,
|
||||||
|
_body: B,
|
||||||
|
_options: Option<RequestOptions>,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
Err(ClientError::WasmNotImplemented)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for configuring and creating an [`HttpClient`].
|
/// Builder for configuring and creating an [`HttpClient`].
|
||||||
|
@@ -97,7 +97,6 @@ fn default_client_static_method() {
|
|||||||
let _client = HttpClient::default();
|
let _client = HttpClient::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn post_smoke_default() {
|
async fn post_smoke_default() {
|
||||||
// Smoke test for POST support with default features
|
// Smoke test for POST support with default features
|
||||||
|
@@ -55,13 +55,14 @@ fn test_timeout_configuration_for_example_server() {
|
|||||||
fn test_headers_configuration_for_example_server() {
|
fn test_headers_configuration_for_example_server() {
|
||||||
// Test custom headers configuration
|
// Test custom headers configuration
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("User-Agent".to_string(), "hyper-custom-cert-integration-test/1.0".to_string());
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"hyper-custom-cert-integration-test/1.0".to_string(),
|
||||||
|
);
|
||||||
headers.insert("X-Test-Client".to_string(), "integration".to_string());
|
headers.insert("X-Test-Client".to_string(), "integration".to_string());
|
||||||
headers.insert("Accept".to_string(), "application/json".to_string());
|
headers.insert("Accept".to_string(), "application/json".to_string());
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder().with_default_headers(headers).build();
|
||||||
.with_default_headers(headers)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Smoke test - verify header configuration compiles
|
// Smoke test - verify header configuration compiles
|
||||||
let _ = client;
|
let _ = client;
|
||||||
@@ -71,7 +72,10 @@ fn test_headers_configuration_for_example_server() {
|
|||||||
fn test_combined_configuration_for_example_server() {
|
fn test_combined_configuration_for_example_server() {
|
||||||
// Test combining multiple configuration options
|
// Test combining multiple configuration options
|
||||||
let mut headers = HashMap::new();
|
let mut headers = HashMap::new();
|
||||||
headers.insert("User-Agent".to_string(), "hyper-custom-cert-combined-test/1.0".to_string());
|
headers.insert(
|
||||||
|
"User-Agent".to_string(),
|
||||||
|
"hyper-custom-cert-combined-test/1.0".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder()
|
||||||
.with_timeout(Duration::from_secs(30))
|
.with_timeout(Duration::from_secs(30))
|
||||||
@@ -132,9 +136,7 @@ fn test_rustls_cert_pinning_configuration() {
|
|||||||
let dummy_pin = [0u8; 32];
|
let dummy_pin = [0u8; 32];
|
||||||
let pins = vec![dummy_pin];
|
let pins = vec![dummy_pin];
|
||||||
|
|
||||||
let client = HttpClient::builder()
|
let client = HttpClient::builder().with_pinned_cert_sha256(pins).build();
|
||||||
.with_pinned_cert_sha256(pins)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Smoke test - verify cert pinning compiles
|
// Smoke test - verify cert pinning compiles
|
||||||
let _ = client;
|
let _ = client;
|
||||||
@@ -166,13 +168,37 @@ fn test_self_signed_convenience_constructor() {
|
|||||||
// HTTP METHOD TESTS - Test different HTTP methods
|
// HTTP METHOD TESTS - Test different HTTP methods
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request_options() {
|
||||||
|
use hyper_custom_cert::RequestOptions;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// Test RequestOptions functionality
|
||||||
|
let client = HttpClient::new();
|
||||||
|
|
||||||
|
// Create request options
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
headers.insert("X-Custom-Header".to_string(), "test-value".to_string());
|
||||||
|
|
||||||
|
let options = RequestOptions::new()
|
||||||
|
.with_headers(headers)
|
||||||
|
.with_timeout(Duration::from_secs(15));
|
||||||
|
|
||||||
|
// Smoke test - verify request options can be used with both GET and POST
|
||||||
|
// In real usage:
|
||||||
|
// let _get_resp = client.request("https://example.com", Some(options.clone())).await.unwrap();
|
||||||
|
// let _post_resp = client.post("https://example.com", b"{}", Some(options)).await.unwrap();
|
||||||
|
let _ = (client, options);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_requests_to_example_server() {
|
async fn test_get_requests_to_example_server() {
|
||||||
// Test GET requests
|
// Test GET requests
|
||||||
let client = HttpClient::new();
|
let client = HttpClient::new();
|
||||||
|
|
||||||
// Smoke test - verify GET method API exists
|
// Smoke test - verify GET method API exists
|
||||||
// In real usage: let _response = client.request("http://localhost:8080/test/methods/get").await.unwrap();
|
// In real usage: let _response = client.request("http://localhost:8080/test/methods/get", None).await.unwrap();
|
||||||
let _ = client;
|
let _ = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,8 +210,8 @@ async fn test_post_requests_to_example_server() {
|
|||||||
// Smoke test - verify POST method API exists
|
// Smoke test - verify POST method API exists
|
||||||
// In real usage:
|
// In real usage:
|
||||||
// let json_payload = r#"{"name": "test", "value": "integration-test"}"#;
|
// let json_payload = r#"{"name": "test", "value": "integration-test"}"#;
|
||||||
// let _response = client.post("http://localhost:8080/test/methods/post", json_payload.as_bytes()).await.unwrap();
|
// let _response = client.post("http://localhost:8080/test/methods/post", json_payload.as_bytes(), None).await.unwrap();
|
||||||
// let _response = client.post("http://localhost:8080/test/methods/post", b"").await.unwrap();
|
// let _response = client.post("http://localhost:8080/test/methods/post", b"", None).await.unwrap();
|
||||||
let _ = client;
|
let _ = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +238,7 @@ async fn test_invalid_url_handling() {
|
|||||||
|
|
||||||
// Smoke test - verify client creation
|
// Smoke test - verify client creation
|
||||||
// In real usage, this would test actual URL validation:
|
// In real usage, this would test actual URL validation:
|
||||||
// let result = client.request("invalid-url").await;
|
// let result = client.request("invalid-url", None).await;
|
||||||
// assert!(result.is_err()); // Should fail with invalid URI error
|
// assert!(result.is_err()); // Should fail with invalid URI error
|
||||||
let _ = client;
|
let _ = client;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user