diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 98ea5c79..ec8f567e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2692,6 +2692,7 @@ dependencies = [ "regex-syntax", "reqwest 0.13.3", "rstest 0.26.1", + "rustls", "serde", "serde_json", "serde_urlencoded", diff --git a/rust/pact_ffi/Cargo.toml b/rust/pact_ffi/Cargo.toml index 41d6ad30..475d605e 100644 --- a/rust/pact_ffi/Cargo.toml +++ b/rust/pact_ffi/Cargo.toml @@ -59,6 +59,7 @@ pretty_assertions = "1.4.1" quickcheck = "1.0.3" reqwest = { version = "0.13.1", default-features = false, features = ["rustls", "blocking", "json", "multipart"] } rstest = "0.26.1" +rustls = { version = "0.23", features = ["ring"] } test-log = "0.2.19" tempfile = "3.24.0" diff --git a/rust/pact_ffi/src/mock_server/mod.rs b/rust/pact_ffi/src/mock_server/mod.rs index 71fd7d68..31f8f3f8 100644 --- a/rust/pact_ffi/src/mock_server/mod.rs +++ b/rust/pact_ffi/src/mock_server/mod.rs @@ -120,7 +120,7 @@ ffi_fn! { /// * `pact` - Handle to a Pact model created with created with `pactffi_new_pact`. /// * `addr` - Address to bind to (i.e. `127.0.0.1` or `[::1]`). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case the loopback adapter is used. /// * `port` - Port number to bind to. A value of zero will result in the operating system allocating an available port. - /// * `transport` - The transport to use (i.e. http, https, grpc). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case http will be used. + /// * `transport` - The transport to use (i.e. http, https, grpc). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case http will be used. Passing `https` will start a TLS-enabled server using a self-signed certificate; use `pactffi_get_tls_ca_certificate` to obtain the CA cert for client configuration. /// * `transport_config` - (OPTIONAL) Configuration for the transport as a valid JSON string. Set to NULL or empty if not required. /// /// The port of the mock server is returned. @@ -181,12 +181,22 @@ ffi_fn! { .with_id(Uuid::new_v4().to_string()) .bind_to(socket_addr.to_string()); - let builder = match builder.with_transport(transport.as_str()) { - Ok(builder) => builder, - Err(err) => { - error!("Failed to configure mock server transport '{}' - {}", transport, err); - return -3; - } + let builder = match transport.as_str() { + "https" => match builder.with_self_signed_tls() { + Ok(builder) => builder, + Err(err) => { + error!("Failed to configure TLS for HTTPS mock server - {}", err); + return -3; + } + }, + "http" => builder, + _ => match builder.with_transport(transport.as_str()) { + Ok(builder) => builder, + Err(err) => { + error!("Failed to configure mock server transport '{}' - {}", transport, err); + return -3; + } + }, }; match attach_to_manager(builder) { diff --git a/rust/pact_ffi/tests/tests.rs b/rust/pact_ffi/tests/tests.rs index 641a991e..67777cda 100644 --- a/rust/pact_ffi/tests/tests.rs +++ b/rust/pact_ffi/tests/tests.rs @@ -2557,3 +2557,53 @@ fn mime_multipart_with_json_and_image() { expect!(mismatches).to(be_equal_to("[]")); } + +#[test] +#[allow(deprecated)] +fn https_mock_server_starts_and_responds_over_tls() { + // Both ring (via pact_mock_server) and aws-lc-rs (via reqwest 0.13) are compiled into the test + // binary. Rustls panics if neither has been set as the process default, so install ring first. + rustls::crypto::ring::default_provider().install_default().ok(); + + let consumer_name = CString::new("https-consumer").unwrap(); + let provider_name = CString::new("https-provider").unwrap(); + let pact_handle = pactffi_new_pact(consumer_name.as_ptr(), provider_name.as_ptr()); + let description = CString::new("an HTTPS request").unwrap(); + let interaction = pactffi_new_interaction(pact_handle, description.as_ptr()); + let method = CString::new("GET").unwrap(); + let path = CString::new("/").unwrap(); + + pactffi_upon_receiving(interaction, description.as_ptr()); + pactffi_with_request(interaction, method.as_ptr(), path.as_ptr()); + pactffi_response_status(interaction, 200); + + let address = CString::new("127.0.0.1").unwrap(); + let transport = CString::new("https").unwrap(); + let port = pactffi_create_mock_server_for_transport( + pact_handle, address.as_ptr(), 0, transport.as_ptr(), null() + ); + expect!(port).to(be_greater_than(0)); + + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + let result = client.get(format!("https://127.0.0.1:{}/", port)).send(); + + thread::sleep(Duration::from_millis(100)); + + let matched = pactffi_mock_server_matched(port); + let mismatches = unsafe { + CStr::from_ptr(pactffi_mock_server_mismatches(port)).to_string_lossy().into_owned() + }; + + pactffi_cleanup_mock_server(port); + pactffi_free_pact_handle(pact_handle); + + match result { + Ok(res) => expect!(res.status()).to(be_eq(200)), + Err(err) => panic!("HTTPS request to mock server failed: {}", err), + }; + expect!(matched).to(be_true()); + expect!(mismatches).to(be_equal_to("[]")); +}