#![warn(missing_docs)]
use std::any::Any;
use std::ffi::CStr;
use std::ffi::CString;
use std::panic::catch_unwind;
use std::ptr::null_mut;
use std::str;
use chrono::Local;
use env_logger::Builder;
use itertools::Itertools;
use libc::{c_char, c_ushort, size_t};
use log::*;
use maplit::*;
use onig::Regex;
use rand::prelude::*;
use serde_json::json;
use uuid::Uuid;
use pact_matching::models::{HttpPart, RequestResponseInteraction, OptionalBody};
use pact_matching::models::matchingrules::{MatchingRule, RuleLogic};
use pact_matching::models::provider_states::ProviderState;
use pact_matching::time_utils::{parse_pattern, to_chrono_pattern};
use pact_mock_server::{MANAGER, MockServerError, TlsConfigBuilder, WritePactFileErr};
use pact_mock_server::server_manager::ServerManager;
use crate::bodies::{file_as_multipart_body, process_json, request_multipart, response_multipart};
use crate::handles::InteractionPart;
pub mod handles;
pub mod bodies;
#[no_mangle]
pub unsafe extern fn init(log_env_var: *const c_char) {
let log_env_var = if !log_env_var.is_null() {
let c_str = CStr::from_ptr(log_env_var);
match c_str.to_str() {
Ok(str) => str,
Err(err) => {
warn!("Failed to parse the environment variable name as a UTF-8 string: {}", err);
"LOG_LEVEL"
}
}
} else {
"LOG_LEVEL"
};
let env = env_logger::Env::new().filter(log_env_var);
let mut builder = Builder::from_env(env);
builder.try_init().unwrap_or(());
}
#[no_mangle]
pub extern fn 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 = 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) => Some(tls_config),
Err(err) => {
error!("Failed to build TLS configuration - {}", err);
return -6;
}
}
} else {
None
};
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 {
MockServerError::InvalidPactJson => -2,
MockServerError::MockServerFailedToStart => -3
}
}
}
else {
-5
}
});
match result {
Ok(val) => val,
Err(cause) => {
log::error!("Caught a general panic: {:?}", cause);
-4
}
}
}
#[no_mangle]
pub extern fn 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]
pub extern fn create_mock_server_for_pact(pact: handles::PactHandle, addr_str: *const c_char, tls: bool) -> i32 {
let result = catch_unwind(|| {
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 = 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) => Some(tls_config),
Err(err) => {
error!("Failed to build TLS configuration - {}", err);
return -6;
}
}
} else {
None
};
if let Ok(Ok(addr)) = str::from_utf8(addr_c_str.to_bytes()).map(|s| s.parse::<std::net::SocketAddr>()) {
pact.with_pact(&move |_, inner| {
let server_result = match &tls_config {
Some(tls_config) => pact_mock_server::start_tls_mock_server(Uuid::new_v4().to_string(), inner.clone(), addr, tls_config),
None => pact_mock_server::start_mock_server(Uuid::new_v4().to_string(), inner.clone(), addr)
};
match server_result {
Ok(ms_port) => ms_port,
Err(err) => {
error!("Failed to start mock server - {}", err);
-3
}
}
}).unwrap_or(-1)
}
else {
-5
}
});
match result {
Ok(val) => val,
Err(cause) => {
log::error!("Caught a general panic: {:?}", cause);
-4
}
}
}
#[no_mangle]
pub extern fn 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) => {
log::error!("Caught a general panic: {:?}", cause);
false
}
}
}
#[no_mangle]
pub extern fn mock_server_mismatches(mock_server_port: i32) -> *mut c_char {
let result = catch_unwind(|| {
let result = MANAGER.lock().unwrap()
.get_or_insert_with(ServerManager::new)
.find_mock_server_by_port_mut(mock_server_port as u16, &|ref mut mock_server| {
let mismatches = mock_server.mismatches().iter()
.map(|mismatch| mismatch.to_json() )
.collect::<Vec<serde_json::Value>>();
let json = json!(mismatches);
let s = CString::new(json.to_string()).unwrap();
let p = s.as_ptr();
mock_server.resources.push(s);
p
});
match result {
Some(p) => p as *mut _,
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 cleanup_mock_server(mock_server_port: i32) -> bool {
let result = catch_unwind(|| {
MANAGER.lock().unwrap()
.get_or_insert_with(ServerManager::new)
.shutdown_mock_server_by_port(mock_server_port as u16)
});
match result {
Ok(val) => val,
Err(cause) => {
log::error!("Caught a general panic: {:?}", cause);
false
}
}
}
#[no_mangle]
pub extern fn write_pact_file(mock_server_port: i32, directory: *const c_char) -> i32 {
let result = catch_unwind(|| {
let dir = unsafe {
if directory.is_null() {
log::warn!("Directory to write to is NULL, defaulting to the current working directory");
None
} else {
let c_str = CStr::from_ptr(directory);
let dir_str = str::from_utf8(c_str.to_bytes()).unwrap();
if dir_str.is_empty() {
None
} else {
Some(dir_str.to_string())
}
}
};
pact_mock_server::write_pact_file(mock_server_port, dir)
});
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 new_pact(consumer_name: *const c_char, provider_name: *const c_char) -> handles::PactHandle {
let consumer = convert_cstr("consumer_name", consumer_name).unwrap_or_else(|| "Consumer");
let provider = convert_cstr("provider_name", provider_name).unwrap_or_else(|| "Provider");
handles::PactHandle::new(consumer, provider)
}
#[no_mangle]
pub extern fn new_interaction(pact: handles::PactHandle, description: *const c_char) -> handles::InteractionHandle {
if let Some(description) = convert_cstr("description", description) {
pact.with_pact(&|_, inner| {
let interaction = RequestResponseInteraction {
description: description.to_string(),
..RequestResponseInteraction::default()
};
inner.interactions.push(interaction);
handles::InteractionHandle::new(pact.clone(), inner.interactions.len())
}).unwrap_or_else(|| handles::InteractionHandle::new(pact.clone(), 0))
} else {
handles::InteractionHandle::new(pact.clone(), 0)
}
}
#[no_mangle]
pub extern fn upon_receiving(interaction: handles::InteractionHandle, description: *const c_char) {
if let Some(description) = convert_cstr("description", description) {
interaction.with_interaction(&|_, inner| {
inner.description = description.to_string();
});
}
}
#[no_mangle]
pub extern fn given(interaction: handles::InteractionHandle, description: *const c_char) {
if let Some(description) = convert_cstr("description", description) {
interaction.with_interaction(&|_, inner| {
inner.provider_states.push(ProviderState::default(&description.to_string()));
});
}
}
#[no_mangle]
pub extern fn given_with_param(interaction: handles::InteractionHandle, description: *const c_char,
name: *const c_char, value: *const c_char) {
if let Some(description) = convert_cstr("description", description) {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, inner| {
let value = match serde_json::from_str(value) {
Ok(json) => json,
Err(_) => json!(value)
};
match inner.provider_states.iter().find_position(|state| state.name == description) {
Some((index, _)) => {
inner.provider_states.get_mut(index).unwrap().params.insert(name.to_string(), value);
},
None => inner.provider_states.push(ProviderState {
name: description.to_string(),
params: hashmap!{ name.to_string() => value }
})
};
});
}
}
}
#[no_mangle]
pub extern fn with_request(interaction: handles::InteractionHandle, method: *const c_char, path: *const c_char) {
let method = convert_cstr("method", method).unwrap_or_else(|| "GET");
let path = convert_cstr("path", path).unwrap_or_else(|| "/");
interaction.with_interaction(&|_, inner| {
inner.request.method = method.to_string();
inner.request.path = path.to_string();
});
}
#[no_mangle]
pub extern fn with_query_parameter(interaction: handles::InteractionHandle,
name: *const c_char, index: size_t, value: *const c_char) {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, inner| {
inner.request.query = inner.request.query.clone().map(|mut q| {
if q.contains_key(name) {
let values = q.get_mut(name).unwrap();
if index >= values.len() {
values.resize_with(index + 1, Default::default);
}
values[index] = value.to_string();
} else {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.to_string();
q.insert(name.to_string(), values);
};
q
}).or_else(|| {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.to_string();
Some(hashmap!{ name.to_string() => values })
});
});
} else {
warn!("Ignoring query parameter with empty or null name");
}
}
#[no_mangle]
pub extern fn with_header(interaction: handles::InteractionHandle, part: InteractionPart,
name: *const c_char, index: size_t, value: *const c_char) {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, inner| {
let headers = match part {
InteractionPart::Request => inner.request.headers.clone(),
InteractionPart::Response => inner.response.headers.clone()
};
let updated_headers = headers.map(|mut h| {
if h.contains_key(name) {
let values = h.get_mut(name).unwrap();
if index >= values.len() {
values.resize_with(index + 1, Default::default);
}
values[index] = value.to_string();
} else {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.to_string();
h.insert(name.to_string(), values);
};
h
}).or_else(|| {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.to_string();
Some(hashmap!{ name.to_string() => values })
});
match part {
InteractionPart::Request => inner.request.headers = updated_headers,
InteractionPart::Response => inner.response.headers = updated_headers
};
});
} else {
warn!("Ignoring header with empty or null name");
}
}
#[no_mangle]
pub extern fn response_status(interaction: handles::InteractionHandle, status: c_ushort) {
interaction.with_interaction(&|_, inner| {
inner.response.status = status;
});
}
#[no_mangle]
pub extern fn with_body(interaction: handles::InteractionHandle, part: InteractionPart,
content_type: *const c_char, body: *const c_char) {
let content_type = convert_cstr("content_type", content_type).unwrap_or_else(|| "text/plain");
let body = convert_cstr("body", body).unwrap_or_default();
let content_type_header = "Content-Type".to_string();
interaction.with_interaction(&|_, inner| {
match part {
InteractionPart::Request => {
if !inner.request.has_header(&content_type_header) {
match inner.request.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec![ content_type.to_string() ]);
},
None => {
inner.request.headers = Some(hashmap! { content_type_header.clone() => vec![ content_type.to_string() ]});
}
}
}
let body = if inner.request.content_type().unwrap_or_default().is_json() {
let category = inner.request.matching_rules.add_category("body");
OptionalBody::from(process_json(body.to_string(), category, &mut inner.request.generators))
} else {
OptionalBody::from(body)
};
inner.request.body = body;
},
InteractionPart::Response => {
if !inner.response.has_header(&content_type_header) {
match inner.response.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec![ content_type.to_string() ]);
},
None => {
inner.response.headers = Some(hashmap! { content_type_header.clone() => vec![ content_type.to_string() ]});
}
}
}
let body = if inner.response.content_type().unwrap_or_default().is_json() {
let category = inner.response.matching_rules.add_category("body");
OptionalBody::from(process_json(body.to_string(), category, &mut inner.response.generators))
} else {
OptionalBody::from(body)
};
inner.response.body = body;
}
};
});
}
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)
}
}
fn convert_cstr(name: &str, value: *const c_char) -> Option<&str> {
unsafe {
if value.is_null() {
warn!("{} is NULL!", name);
None
} else {
let c_str = CStr::from_ptr(value);
match c_str.to_str() {
Ok(str) => Some(str),
Err(err) => {
warn!("Failed to parse {} name as a UTF-8 string: {}", name, err);
None
}
}
}
}
}
#[repr(C)]
#[derive(Debug, Clone)]
pub enum StringResult {
Ok(*mut c_char),
Failed(*mut c_char)
}
#[no_mangle]
pub unsafe extern fn 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 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 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]
pub unsafe extern fn free_string(s: *mut c_char) {
if s.is_null() {
return;
}
CString::from_raw(s);
}
#[no_mangle]
pub extern fn with_binary_file(interaction: handles::InteractionHandle, part: InteractionPart,
content_type: *const c_char, body: *const c_char , size: size_t) {
let content_type_header = "Content-Type".to_string();
match convert_cstr("content_type", content_type) {
Some(content_type) => {
interaction.with_interaction(&|_, inner| {
match part {
InteractionPart::Request => {
inner.request.body = convert_ptr_to_body(body, size);
if !inner.request.has_header(&content_type_header) {
match inner.request.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec!["application/octet-stream".to_string()]);
},
None => {
inner.request.headers = Some(hashmap! { content_type_header.clone() => vec!["application/octet-stream".to_string()]});
}
}
};
inner.request.matching_rules.add_category("body").add_rule("$", MatchingRule::ContentType(content_type.into()), &RuleLogic::And);
},
InteractionPart::Response => {
inner.response.body = convert_ptr_to_body(body, size);
if !inner.response.has_header(&content_type_header) {
match inner.response.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec!["application/octet-stream".to_string()]);
},
None => {
inner.response.headers = Some(hashmap! { content_type_header.clone() => vec!["application/octet-stream".to_string()]});
}
}
}
inner.response.matching_rules.add_category("body").add_rule("$", MatchingRule::ContentType(content_type.into()), &RuleLogic::And);
}
};
});
},
None => warn!("with_binary_file: Content type value is not valid (NULL or non-UTF-8)")
}
}
#[no_mangle]
pub extern fn with_multipart_file(
interaction: handles::InteractionHandle,
part: InteractionPart,
content_type: *const c_char,
file: *const c_char,
part_name: *const c_char
) -> StringResult {
let part_name = convert_cstr("part_name", part_name).unwrap_or_else(|| "file");
match convert_cstr("content_type", content_type) {
Some(content_type) => {
match interaction.with_interaction(&|_, inner| {
let boundary = inner.description.replace(" ", "_");
match convert_ptr_to_mime_part_body(file, part_name, boundary.as_str()) {
Ok(body) => {
match part {
InteractionPart::Request => request_multipart(&mut inner.request, &boundary, body, &content_type, part_name),
InteractionPart::Response => response_multipart(&mut inner.response, &boundary, body, &content_type, part_name)
};
Ok(())
},
Err(err) => Err(format!("with_multipart_file: failed to generate multipart body - {}", err))
}
}) {
Some(result) => match result {
Ok(_) => StringResult::Ok(null_mut()),
Err(err) => {
let error = CString::new(err).unwrap();
StringResult::Failed(error.into_raw())
}
},
None => {
let error = CString::new("with_multipart_file: Interaction handle is invalid").unwrap();
StringResult::Failed(error.into_raw())
}
}
},
None => {
warn!("with_multipart_file: Content type value is not valid (NULL or non-UTF-8)");
let error = CString::new("with_multipart_file: Content type value is not valid (NULL or non-UTF-8)").unwrap();
StringResult::Failed(error.into_raw())
}
}
}
fn convert_ptr_to_body(body: *const c_char, size: size_t) -> OptionalBody {
if body.is_null() {
OptionalBody::Null
} else if size == 0 {
OptionalBody::Empty
} else {
OptionalBody::Present(unsafe { std::slice::from_raw_parts(body as *const u8, size) }.to_vec(), None)
}
}
fn convert_ptr_to_mime_part_body(file: *const c_char, part_name: &str, boundary: &str) -> Result<OptionalBody, String> {
if file.is_null() {
Ok(OptionalBody::Null)
} else {
let c_str = unsafe { CStr::from_ptr(file) };
let file = match c_str.to_str() {
Ok(str) => Ok(str),
Err(err) => {
warn!("convert_ptr_to_mime_part_body: Failed to parse file name as a UTF-8 string: {}", err);
Err(format!("convert_ptr_to_mime_part_body: Failed to parse file name as a UTF-8 string: {}", err))
}
}?;
file_as_multipart_body(file, part_name, boundary)
}
}