use crate::core::{ChaserCF, ChaserConfig};
use crate::error::ChaserError;
use crate::models::{ChaserResult as ChaserResultModel, Profile, ProxyConfig};
use once_cell::sync::OnceCell;
use std::ffi::{c_char, c_void, CStr, CString};
use std::ptr;
use std::sync::Arc;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
static CHASER: OnceCell<Arc<RwLock<Option<ChaserCF>>>> = OnceCell::new();
pub type ChaserCallback = extern "C" fn(result: *const c_char, user_data: *mut c_void);
#[repr(C)]
pub struct ChaserConfigFFI {
pub context_limit: u32,
pub timeout_ms: u64,
pub profile: u32,
pub lazy_init: u32,
pub headless: u32,
pub chrome_path: *const c_char,
}
#[repr(C)]
pub struct ProxyConfigFFI {
pub host: *const c_char,
pub port: u16,
pub username: *const c_char,
pub password: *const c_char,
}
#[no_mangle]
pub extern "C" fn chaser_config_default() -> ChaserConfigFFI {
ChaserConfigFFI {
context_limit: 20,
timeout_ms: 60000,
profile: 0, lazy_init: 0,
headless: 0,
chrome_path: ptr::null(),
}
}
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn chaser_init(config: *const ChaserConfigFFI) -> i32 {
let runtime = RUNTIME.get_or_try_init(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
});
let runtime = match runtime {
Ok(rt) => rt,
Err(_) => return -1,
};
let chaser_config = if config.is_null() {
ChaserConfig::default()
} else {
unsafe { config_from_ffi(&*config) }
};
let result = runtime.block_on(async {
let suite = ChaserCF::new(chaser_config).await?;
Ok::<_, ChaserError>(suite)
});
match result {
Ok(suite) => {
let _ = CHASER.set(Arc::new(RwLock::new(Some(suite))));
0
}
Err(e) => e.code(),
}
}
#[no_mangle]
pub extern "C" fn chaser_shutdown() {
if let Some(runtime) = RUNTIME.get() {
if let Some(chaser) = CHASER.get() {
runtime.block_on(async {
let mut guard = chaser.write().await;
if let Some(suite) = guard.take() {
suite.shutdown().await;
}
});
}
}
}
#[no_mangle]
pub extern "C" fn chaser_is_ready() -> i32 {
if let (Some(runtime), Some(chaser)) = (RUNTIME.get(), CHASER.get()) {
let ready = runtime.block_on(async {
let guard = chaser.read().await;
match guard.as_ref() {
Some(s) => s.is_ready().await,
None => false,
}
});
if ready {
1
} else {
0
}
} else {
0
}
}
#[no_mangle]
pub unsafe extern "C" fn chaser_solve_waf_async(
url: *const c_char,
proxy: *const ProxyConfigFFI,
user_data: *mut c_void,
callback: ChaserCallback,
) {
let url = match cstr_to_string(url) {
Some(s) => s,
None => {
let result = make_error_json(ChaserError::InvalidUrl("NULL url".to_string()));
callback(result, user_data);
return;
}
};
let proxy = proxy_from_ffi(proxy);
spawn_async_operation(
user_data,
callback,
AsyncOp {
url,
site_key: None,
proxy,
op_type: OpType::WafSession,
},
);
}
#[no_mangle]
pub unsafe extern "C" fn chaser_get_source_async(
url: *const c_char,
proxy: *const ProxyConfigFFI,
user_data: *mut c_void,
callback: ChaserCallback,
) {
let url = match cstr_to_string(url) {
Some(s) => s,
None => {
let result = make_error_json(ChaserError::InvalidUrl("NULL url".to_string()));
callback(result, user_data);
return;
}
};
let proxy = proxy_from_ffi(proxy);
spawn_async_operation(
user_data,
callback,
AsyncOp {
url,
site_key: None,
proxy,
op_type: OpType::Source,
},
);
}
#[no_mangle]
pub unsafe extern "C" fn chaser_solve_turnstile_async(
url: *const c_char,
proxy: *const ProxyConfigFFI,
user_data: *mut c_void,
callback: ChaserCallback,
) {
let url = match cstr_to_string(url) {
Some(s) => s,
None => {
let result = make_error_json(ChaserError::InvalidUrl("NULL url".to_string()));
callback(result, user_data);
return;
}
};
let proxy = proxy_from_ffi(proxy);
spawn_async_operation(
user_data,
callback,
AsyncOp {
url,
site_key: None,
proxy,
op_type: OpType::Turnstile,
},
);
}
#[no_mangle]
pub unsafe extern "C" fn chaser_solve_turnstile_min_async(
url: *const c_char,
site_key: *const c_char,
proxy: *const ProxyConfigFFI,
user_data: *mut c_void,
callback: ChaserCallback,
) {
let url = match cstr_to_string(url) {
Some(s) => s,
None => {
let result = make_error_json(ChaserError::InvalidUrl("NULL url".to_string()));
callback(result, user_data);
return;
}
};
let site_key = match cstr_to_string(site_key) {
Some(s) => s,
None => {
let result = make_error_json(ChaserError::MissingParameter("site_key".to_string()));
callback(result, user_data);
return;
}
};
let proxy = proxy_from_ffi(proxy);
spawn_async_operation(
user_data,
callback,
AsyncOp {
url,
site_key: Some(site_key),
proxy,
op_type: OpType::TurnstileMin,
},
);
}
#[no_mangle]
pub unsafe extern "C" fn chaser_free_string(s: *mut c_char) {
if !s.is_null() {
let _ = CString::from_raw(s);
}
}
unsafe fn cstr_to_string(s: *const c_char) -> Option<String> {
if s.is_null() {
return None;
}
CStr::from_ptr(s).to_str().ok().map(|s| s.to_owned())
}
unsafe fn config_from_ffi(ffi: &ChaserConfigFFI) -> ChaserConfig {
let mut config = ChaserConfig::default()
.with_context_limit(ffi.context_limit as usize)
.with_timeout_ms(ffi.timeout_ms)
.with_lazy_init(ffi.lazy_init != 0)
.with_headless(ffi.headless != 0);
config.profile = match ffi.profile {
1 => Profile::Linux,
2 => Profile::Macos,
_ => Profile::Windows,
};
if !ffi.chrome_path.is_null() {
if let Some(path) = cstr_to_string(ffi.chrome_path) {
config = config.with_chrome_path(path);
}
}
config
}
unsafe fn proxy_from_ffi(ffi: *const ProxyConfigFFI) -> Option<ProxyConfig> {
if ffi.is_null() {
return None;
}
let ffi = &*ffi;
let host = cstr_to_string(ffi.host)?;
let mut proxy = ProxyConfig::new(host, ffi.port);
if let (Some(username), Some(password)) =
(cstr_to_string(ffi.username), cstr_to_string(ffi.password))
{
proxy = proxy.with_auth(username, password);
}
Some(proxy)
}
fn make_error_json(error: ChaserError) -> *const c_char {
let result = ChaserResultModel::error(error.code(), error.to_string());
let json = serde_json::to_string(&result).unwrap_or_else(|_| {
r#"{"type":"Error","data":{"code":99,"message":"Serialization failed"}}"#.to_string()
});
CString::new(json).unwrap().into_raw()
}
struct AsyncOp {
url: String,
site_key: Option<String>,
proxy: Option<ProxyConfig>,
op_type: OpType,
}
#[derive(Clone, Copy)]
enum OpType {
WafSession,
Source,
Turnstile,
TurnstileMin,
}
fn spawn_async_operation(user_data: *mut c_void, callback: ChaserCallback, op: AsyncOp) {
let user_data = user_data as usize;
let runtime = match RUNTIME.get() {
Some(rt) => rt,
None => {
let result = make_error_json(ChaserError::NotInitialized);
callback(result, user_data as *mut c_void);
return;
}
};
let chaser = match CHASER.get() {
Some(g) => g.clone(),
None => {
let result = make_error_json(ChaserError::NotInitialized);
callback(result, user_data as *mut c_void);
return;
}
};
runtime.spawn(async move {
let result = {
let guard = chaser.read().await;
match guard.as_ref() {
Some(suite) => match op.op_type {
OpType::WafSession => match suite.solve_waf_session(&op.url, op.proxy).await {
Ok(session) => ChaserResultModel::waf_session(session),
Err(e) => ChaserResultModel::error(e.code(), e.to_string()),
},
OpType::Source => match suite.get_source(&op.url, op.proxy).await {
Ok(source) => ChaserResultModel::source(source),
Err(e) => ChaserResultModel::error(e.code(), e.to_string()),
},
OpType::Turnstile => match suite.solve_turnstile(&op.url, op.proxy).await {
Ok(token) => ChaserResultModel::token(token),
Err(e) => ChaserResultModel::error(e.code(), e.to_string()),
},
OpType::TurnstileMin => {
let site_key = op.site_key.as_deref().unwrap_or("");
match suite.solve_turnstile_min(&op.url, site_key, op.proxy).await {
Ok(token) => ChaserResultModel::token(token),
Err(e) => ChaserResultModel::error(e.code(), e.to_string()),
}
}
},
None => ChaserResultModel::error(
ChaserError::NotInitialized.code(),
"chaser-cf not initialized",
),
}
};
let json = serde_json::to_string(&result).unwrap_or_else(|_| {
r#"{"type":"Error","data":{"code":99,"message":"Serialization failed"}}"#.to_string()
});
let c_result = CString::new(json).unwrap().into_raw();
callback(c_result, user_data as *mut c_void);
});
}