#![warn(missing_docs)]
use std::{ptr, str};
use std::any::Any;
use std::ffi::CStr;
use std::ffi::CString;
use std::net::ToSocketAddrs;
use std::panic::catch_unwind;
use std::str::from_utf8;
use chrono::Local;
use either::Either;
use libc::c_char;
use onig::Regex;
use pact_models::pact::Pact;
use pact_models::time_utils::{parse_pattern, to_chrono_pattern};
use rand::prelude::*;
use serde_json::Value;
use tokio_rustls::rustls::ServerConfig;
use tracing::error;
use uuid::Uuid;
use pact_matching::logging::fetch_buffer_contents;
use pact_matching::metrics::{MetricEvent, send_metrics};
use pact_mock_server::{MANAGER, mock_server_mismatches, MockServerError, tls::TlsConfigBuilder, WritePactFileErr};
use pact_mock_server::mock_server::MockServerConfig;
use pact_mock_server::server_manager::ServerManager;
use crate::{convert_cstr, ffi_fn, safe_str};
use crate::mock_server::handles::{PactHandle, path_from_dir};
use crate::string::optional_str;
pub mod handles;
pub mod bodies;
mod xml;
#[no_mangle]
#[deprecated(since = "0.1.7", note = "replaced with pactffi_create_mock_server_for_transport")]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern fn pactffi_create_mock_server(pact_str: *const c_char, addr_str: *const c_char, tls: bool) -> i32 {
let result = catch_unwind(|| {
let c_str = unsafe {
if pact_str.is_null() {
log::error!("Got a null pointer instead of pact json");
return -1;
}
CStr::from_ptr(pact_str)
};
let addr_c_str = unsafe {
if addr_str.is_null() {
log::error!("Got a null pointer instead of listener address");
return -1;
}
CStr::from_ptr(addr_str)
};
let tls_config = match setup_tls_config(tls) {
Ok(config) => config,
Err(err) => return err
};
if let Ok(Ok(addr)) = str::from_utf8(addr_c_str.to_bytes()).map(|s| s.parse::<std::net::SocketAddr>()) {
let server_result = match tls_config {
Some(tls_config) => pact_mock_server::create_tls_mock_server(str::from_utf8(c_str.to_bytes()).unwrap(), addr, &tls_config),
None => pact_mock_server::create_mock_server(str::from_utf8(c_str.to_bytes()).unwrap(), addr)
};
match server_result {
Ok(ms_port) => ms_port,
Err(err) => match err.downcast_ref::<MockServerError>() {
Some(err) => match err {
MockServerError::InvalidPactJson => -2,
MockServerError::MockServerFailedToStart => -3
},
None => -3
}
}
}
else {
-5
}
});
match result {
Ok(val) => val,
Err(cause) => {
log::error!("Caught a general panic: {:?}", cause);
-4
}
}
}
#[no_mangle]
pub extern fn pactffi_get_tls_ca_certificate() -> *mut c_char {
let cert_file = include_str!("ca.pem");
let cert_str = CString::new(cert_file).unwrap_or_default();
cert_str.into_raw()
}
#[no_mangle]
#[tracing::instrument(level = "trace")]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern fn pactffi_create_mock_server_for_pact(pact: PactHandle, addr_str: *const c_char, tls: bool) -> i32 {
let result = catch_unwind(|| {
let addr_c_str = unsafe {
if addr_str.is_null() {
error!("Got a null pointer instead of listener address");
return -5;
}
CStr::from_ptr(addr_str)
};
let tls_config = match setup_tls_config(tls) {
Ok(config) => config,
Err(err) => return err
};
if let Ok(Ok(addr)) = from_utf8(addr_c_str.to_bytes()).map(|s| s.parse::<std::net::SocketAddr>()) {
pact.with_pact(&move |_, inner| {
let config = MockServerConfig {
cors_preflight: true,
pact_specification: inner.specification_version,
.. MockServerConfig::default()
};
let server_result = match &tls_config {
Some(tls_config) => pact_mock_server::start_tls_mock_server_with_config(
Uuid::new_v4().to_string(), inner.pact.boxed(), addr, tls_config, config),
None => pact_mock_server::start_mock_server_with_config(Uuid::new_v4().to_string(),
inner.pact.boxed(), addr, config)
};
match server_result {
Ok(ms_port) => {
inner.mock_server_started = true;
ms_port
},
Err(err) => {
error!("Failed to start mock server - {}", err);
-3
}
}
}).unwrap_or(-1)
}
else {
-5
}
});
match result {
Ok(val) => val,
Err(cause) => {
error!("Caught a general panic: {:?}", cause);
-4
}
}
}
fn setup_tls_config(tls: bool) -> Result<Option<ServerConfig>, i32> {
if tls {
let key = include_str!("self-signed.key");
let cert = include_str!("self-signed.crt");
match TlsConfigBuilder::new()
.key(key.as_bytes())
.cert(cert.as_bytes())
.build() {
Ok(tls_config) => Ok(Some(tls_config)),
Err(err) => {
error!("Failed to build TLS configuration - {}", err);
Err(-6)
}
}
} else {
Ok(None)
}
}
ffi_fn! {
#[tracing::instrument(level = "trace")]
fn pactffi_create_mock_server_for_transport(
pact: PactHandle,
addr: *const c_char,
port: u16,
transport: *const c_char,
transport_config: *const c_char
) -> i32 {
let addr = safe_str!(addr);
let transport = safe_str!(transport);
let transport_config = match optional_str(transport_config).map(|config| str::parse::<Value>(config.as_str())) {
None => Ok(None),
Some(result) => match result {
Ok(value) => Ok(Some(MockServerConfig::from_json(&value))),
Err(err) => {
error!("Failed to parse transport_config as JSON - {}", err);
Err(-2)
}
}
};
match transport_config {
Ok(transport_config) => if let Ok(mut socket_addr) = (addr, port).to_socket_addrs() {
let socket_addr = socket_addr.next().unwrap();
pact.with_pact(&move |_, inner| {
let transport_config = transport_config.clone();
let config = MockServerConfig {
pact_specification: inner.specification_version,
.. transport_config.unwrap_or_default()
};
match pact_mock_server::start_mock_server_for_transport(Uuid::new_v4().to_string(),
inner.pact.boxed(), socket_addr, transport, config) {
Ok(ms_port) => {
inner.mock_server_started = true;
ms_port
},
Err(err) => {
error!("Failed to start mock server - {}", err);
-3
}
}
}).unwrap_or(-1)
} else {
error!("Failed to parse '{}', {} as an address", addr, port);
-5
}
Err(err) => err
}
} {
-4
}
}
#[no_mangle]
pub extern fn pactffi_mock_server_matched(mock_server_port: i32) -> bool {
let result = catch_unwind(|| {
pact_mock_server::mock_server_matched(mock_server_port)
});
match result {
Ok(val) => val,
Err(cause) => {
error!("Caught a general panic: {:?}", cause);
false
}
}
}
#[no_mangle]
pub extern fn pactffi_mock_server_mismatches(mock_server_port: i32) -> *mut c_char {
let result = catch_unwind(|| {
let result = mock_server_mismatches(mock_server_port);
match result {
Some(str) => {
let s = CString::new(str).unwrap();
let p = s.as_ptr() as *mut _;
MANAGER.lock().unwrap()
.get_or_insert_with(ServerManager::new)
.store_mock_server_resource(mock_server_port as u16, s);
p
},
None => std::ptr::null_mut()
}
});
match result {
Ok(val) => val,
Err(cause) => {
error!("{}", error_message(cause, "mock_server_mismatches"));
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern fn pactffi_cleanup_mock_server(mock_server_port: i32) -> bool {
let result = catch_unwind(|| {
let id = pact_mock_server::find_mock_server_by_port(mock_server_port as u16, &|_, id, mock_server| {
let interactions = match mock_server {
Either::Left(ms) => {
let pact = ms.pact.lock().unwrap();
pact.interactions().len()
},
Either::Right(ms) => ms.pact.interactions.len()
};
send_metrics(MetricEvent::ConsumerTestRun {
interactions,
test_framework: "pact_ffi".to_string(),
app_name: "pact_ffi".to_string(),
app_version: env!("CARGO_PKG_VERSION").to_string()
});
id.clone()
});
if let Some(id) = id {
pact_mock_server::shutdown_mock_server_by_id(id.as_str())
} else {
false
}
});
match result {
Ok(val) => val,
Err(cause) => {
error!("Caught a general panic: {:?}", cause);
false
}
}
}
#[no_mangle]
pub extern fn pactffi_write_pact_file(mock_server_port: i32, directory: *const c_char, overwrite: bool) -> i32 {
let result = catch_unwind(|| {
let dir = path_from_dir(directory, None);
let path = dir.map(|path| path.into_os_string().into_string().unwrap_or_default());
pact_mock_server::write_pact_file(mock_server_port, path, overwrite)
});
match result {
Ok(val) => match val {
Ok(_) => 0,
Err(err) => match err {
WritePactFileErr::IOError => 2,
WritePactFileErr::NoMockServer => 3
}
},
Err(cause) => {
log::error!("Caught a general panic: {:?}", cause);
1
}
}
}
#[no_mangle]
pub extern fn pactffi_mock_server_logs(mock_server_port: i32) -> *const c_char {
let result = catch_unwind(|| {
let mut guard = MANAGER.lock().unwrap();
let manager = guard.get_or_insert_with(ServerManager::new);
let logs = manager.find_mock_server_by_port_mut(mock_server_port as u16, &|mock_server| {
fetch_buffer_contents(&mock_server.id)
});
match logs {
Some(bytes) => {
match from_utf8(&bytes) {
Ok(contents) => match CString::new(contents.to_string()) {
Ok(c_str) => {
let p = c_str.as_ptr();
manager.store_mock_server_resource(mock_server_port as u16, c_str);
p
},
Err(err) => {
error!("Failed to copy in-memory log buffer - {}", err);
ptr::null()
}
}
Err(err) => {
error!("Failed to convert in-memory log buffer to UTF-8 = {}", err);
ptr::null()
}
}
}
None => {
error!("No mock server found for port {}", mock_server_port);
ptr::null()
}
}
});
match result {
Ok(val) => val,
Err(cause) => {
error!("Caught a general panic: {:?}", cause);
ptr::null()
}
}
}
fn error_message(err: Box<dyn Any>, method: &str) -> String {
if let Some(err) = err.downcast_ref::<&str>() {
format!("{} failed with an error - {}", method, err)
} else if let Some(err) = err.downcast_ref::<String>() {
format!("{} failed with an error - {}", method, err)
} else {
format!("{} failed with an unknown error", method)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum StringResult {
Ok(*mut c_char),
Failed(*mut c_char)
}
#[no_mangle]
pub unsafe extern fn pactffi_generate_datetime_string(format: *const c_char) -> StringResult {
if format.is_null() {
let error = CString::new("generate_datetime_string: format is NULL").unwrap();
StringResult::Failed(error.into_raw())
} else {
let c_str = CStr::from_ptr(format);
match c_str.to_str() {
Ok(s) => match parse_pattern(s) {
Ok(pattern_tokens) => {
let result = Local::now().format(to_chrono_pattern(&pattern_tokens).as_str()).to_string();
let result_str = CString::new(result.as_str()).unwrap();
StringResult::Ok(result_str.into_raw())
},
Err(err) => {
let error = format!("Error parsing '{}': {:?}", s, err);
let error_str = CString::new(error.as_str()).unwrap();
StringResult::Failed(error_str.into_raw())
}
},
Err(err) => {
let error = format!("generate_datetime_string: format is not a valid UTF-8 string: {:?}", err);
let error_str = CString::new(error.as_str()).unwrap();
StringResult::Failed(error_str.into_raw())
}
}
}
}
#[no_mangle]
pub unsafe extern fn pactffi_check_regex(regex: *const c_char, example: *const c_char) -> bool {
if regex.is_null() {
false
} else {
let c_str = CStr::from_ptr(regex);
match c_str.to_str() {
Ok(regex) => {
let example = convert_cstr("example", example).unwrap_or_default();
match Regex::new(regex) {
Ok(re) => re.is_match(example),
Err(err) => {
error!("check_regex: '{}' is not a valid regular expression - {}", regex, err);
false
}
}
},
Err(err) => {
error!("check_regex: regex is not a valid UTF-8 string: {:?}", err);
false
}
}
}
}
pub fn generate_regex_value_internal(regex: &str) -> Result<String, String> {
let mut parser = regex_syntax::ParserBuilder::new().unicode(false).build();
match parser.parse(regex) {
Ok(hir) => {
let mut rnd = rand::thread_rng();
let gen = rand_regex::Regex::with_hir(hir, 20).unwrap();
let result: String = rnd.sample(gen);
Ok(result)
},
Err(err) => {
let error = format!("generate_regex_value: '{}' is not a valid regular expression - {}", regex, err);
Err(error)
}
}
}
#[no_mangle]
pub unsafe extern fn pactffi_generate_regex_value(regex: *const c_char) -> StringResult {
if regex.is_null() {
let error = CString::new("generate_regex_value: regex is NULL").unwrap();
StringResult::Failed(error.into_raw())
} else {
let c_str = CStr::from_ptr(regex);
match c_str.to_str() {
Ok(regex) => match generate_regex_value_internal(regex) {
Ok(val) => {
let result_str = CString::new(val.as_str()).unwrap();
StringResult::Ok(result_str.into_raw())
},
Err(err) => {
let error = CString::new(err).unwrap();
StringResult::Failed(error.into_raw())
}
},
Err(err) => {
let error = CString::new(format!("generate_regex_value: regex is not a valid UTF-8 string: {:?}", err)).unwrap();
StringResult::Failed(error.into_raw())
}
}
}
}
#[no_mangle]
#[deprecated(since = "0.1.0", note = "Use pactffi_string_delete instead")]
pub unsafe extern fn pactffi_free_string(s: *mut c_char) {
if s.is_null() {
return;
}
drop(CString::from_raw(s));
}