use std::ffi::{c_char, c_int, c_uint, c_void, CStr, CString};
use std::ptr;
use std::sync::Arc;
use libloading::{Library, Symbol};
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub error_code: i32,
pub error_message: Option<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
error_code: 0,
error_message: None,
}
}
pub fn failure(code: i32, message: impl Into<String>) -> Self {
Self {
valid: false,
error_code: code,
error_message: Some(message.into()),
}
}
}
#[derive(Debug, Clone)]
pub struct TokenPayload {
pub subject: String,
pub scopes: String,
pub audience: String,
pub expiration: u64,
}
type GopherAuthInitFn = unsafe extern "C" fn() -> c_int;
type GopherAuthClientCreateFn = unsafe extern "C" fn(
out: *mut *mut c_void,
jwks_uri: *const c_char,
issuer: *const c_char,
) -> c_int;
type GopherAuthClientDestroyFn = unsafe extern "C" fn(client: *mut c_void);
type GopherAuthSetOptionFn =
unsafe extern "C" fn(client: *mut c_void, key: *const c_char, value: *const c_char) -> c_int;
type GopherAuthValidateTokenFn = unsafe extern "C" fn(
client: *mut c_void,
token: *const c_char,
clock_skew: c_uint,
out_valid: *mut c_int,
out_error: *mut *mut c_char,
) -> c_int;
type GopherAuthExtractPayloadFn =
unsafe extern "C" fn(client: *mut c_void, token: *const c_char, out: *mut *mut c_void) -> c_int;
type GopherAuthPayloadGetSubjectFn = unsafe extern "C" fn(payload: *mut c_void) -> *const c_char;
type GopherAuthPayloadGetScopesFn = unsafe extern "C" fn(payload: *mut c_void) -> *const c_char;
type GopherAuthPayloadGetAudienceFn = unsafe extern "C" fn(payload: *mut c_void) -> *const c_char;
type GopherAuthPayloadGetExpirationFn = unsafe extern "C" fn(payload: *mut c_void) -> u64;
type GopherAuthPayloadDestroyFn = unsafe extern "C" fn(payload: *mut c_void);
type GopherAuthFreeStringFn = unsafe extern "C" fn(s: *mut c_char);
pub struct GopherAuthClient {
handle: *mut c_void,
library: Arc<Library>,
}
unsafe impl Send for GopherAuthClient {}
unsafe impl Sync for GopherAuthClient {}
impl GopherAuthClient {
pub fn new(jwks_uri: &str, issuer: &str) -> Result<Self, Error> {
let library = Self::load_library()?;
let library = Arc::new(library);
unsafe {
let init: Symbol<GopherAuthInitFn> = library
.get(b"gopher_auth_init\0")
.map_err(|e| Error::auth(format!("Failed to load gopher_auth_init: {}", e)))?;
let result = init();
if result != 0 {
return Err(Error::auth(format!(
"gopher_auth_init failed with code {}",
result
)));
}
}
let jwks_uri_c =
CString::new(jwks_uri).map_err(|e| Error::auth(format!("Invalid jwks_uri: {}", e)))?;
let issuer_c =
CString::new(issuer).map_err(|e| Error::auth(format!("Invalid issuer: {}", e)))?;
let handle = unsafe {
let create: Symbol<GopherAuthClientCreateFn> =
library.get(b"gopher_auth_client_create\0").map_err(|e| {
Error::auth(format!("Failed to load gopher_auth_client_create: {}", e))
})?;
let mut handle: *mut c_void = ptr::null_mut();
let result = create(&mut handle, jwks_uri_c.as_ptr(), issuer_c.as_ptr());
if result != 0 || handle.is_null() {
return Err(Error::auth(format!(
"gopher_auth_client_create failed with code {}",
result
)));
}
handle
};
Ok(Self { handle, library })
}
fn load_library() -> Result<Library, Error> {
let lib_names = if cfg!(target_os = "macos") {
vec![
"libgopher-orch.dylib",
"libgopher-orch.0.dylib",
"libgopher_orch.dylib",
]
} else if cfg!(target_os = "windows") {
vec!["gopher-orch.dll", "libgopher-orch.dll", "gopher_orch.dll"]
} else {
vec![
"libgopher-orch.so",
"libgopher-orch.so.0",
"libgopher_orch.so",
]
};
let mut search_paths = vec![
String::new(), String::from("./"),
String::from("./native/lib/"),
String::from("../native/lib/"),
];
if let Ok(lib_path) = std::env::var("DYLD_LIBRARY_PATH") {
for path in lib_path.split(':') {
if !path.is_empty() {
let mut p = path.to_string();
if !p.ends_with('/') {
p.push('/');
}
search_paths.push(p);
}
}
}
if let Ok(lib_path) = std::env::var("LD_LIBRARY_PATH") {
for path in lib_path.split(':') {
if !path.is_empty() {
let mut p = path.to_string();
if !p.ends_with('/') {
p.push('/');
}
search_paths.push(p);
}
}
}
search_paths.push(String::from("/usr/local/lib/"));
search_paths.push(String::from("/usr/lib/"));
for path in &search_paths {
for name in &lib_names {
let full_path = format!("{}{}", path, name);
if let Ok(lib) = unsafe { Library::new(&full_path) } {
return Ok(lib);
}
}
}
Err(Error::auth(format!(
"Failed to load gopher-auth library. Tried: {:?}",
lib_names
)))
}
pub fn is_available() -> bool {
Self::load_library().is_ok()
}
pub fn validate_token(&self, token: &str, clock_skew: u32) -> ValidationResult {
let token_c = match CString::new(token) {
Ok(s) => s,
Err(e) => return ValidationResult::failure(-1, format!("Invalid token string: {}", e)),
};
unsafe {
let validate: Symbol<GopherAuthValidateTokenFn> =
match self.library.get(b"gopher_auth_validate_token\0") {
Ok(f) => f,
Err(e) => {
return ValidationResult::failure(
-1,
format!("Failed to load validate function: {}", e),
)
}
};
let mut valid: c_int = 0;
let mut error: *mut c_char = ptr::null_mut();
let result = validate(
self.handle,
token_c.as_ptr(),
clock_skew,
&mut valid,
&mut error,
);
if result != 0 {
let error_msg = if !error.is_null() {
let msg = CStr::from_ptr(error).to_string_lossy().into_owned();
self.free_string(error);
msg
} else {
format!("Validation failed with code {}", result)
};
return ValidationResult::failure(result, error_msg);
}
if valid != 0 {
ValidationResult::success()
} else {
let error_msg = if !error.is_null() {
let msg = CStr::from_ptr(error).to_string_lossy().into_owned();
self.free_string(error);
msg
} else {
"Token validation failed".to_string()
};
ValidationResult::failure(-2, error_msg)
}
}
}
pub fn extract_payload(&self, token: &str) -> Result<TokenPayload, Error> {
let token_c =
CString::new(token).map_err(|e| Error::auth(format!("Invalid token string: {}", e)))?;
unsafe {
let extract: Symbol<GopherAuthExtractPayloadFn> = self
.library
.get(b"gopher_auth_extract_payload\0")
.map_err(|e| Error::auth(format!("Failed to load extract function: {}", e)))?;
let mut payload: *mut c_void = ptr::null_mut();
let result = extract(self.handle, token_c.as_ptr(), &mut payload);
if result != 0 || payload.is_null() {
return Err(Error::auth(format!(
"Failed to extract payload, code {}",
result
)));
}
let subject = self.get_payload_string(payload, b"gopher_auth_payload_get_subject\0")?;
let scopes = self.get_payload_string(payload, b"gopher_auth_payload_get_scopes\0")?;
let audience =
self.get_payload_string(payload, b"gopher_auth_payload_get_audience\0")?;
let expiration = self.get_payload_expiration(payload)?;
self.destroy_payload(payload);
Ok(TokenPayload {
subject,
scopes,
audience,
expiration,
})
}
}
unsafe fn get_payload_string(
&self,
payload: *mut c_void,
fn_name: &[u8],
) -> Result<String, Error> {
let get_fn: Symbol<GopherAuthPayloadGetSubjectFn> = self
.library
.get(fn_name)
.map_err(|e| Error::auth(format!("Failed to load getter function: {}", e)))?;
let ptr = get_fn(payload);
if ptr.is_null() {
return Ok(String::new());
}
Ok(CStr::from_ptr(ptr).to_string_lossy().into_owned())
}
unsafe fn get_payload_expiration(&self, payload: *mut c_void) -> Result<u64, Error> {
let get_fn: Symbol<GopherAuthPayloadGetExpirationFn> = self
.library
.get(b"gopher_auth_payload_get_expiration\0")
.map_err(|e| Error::auth(format!("Failed to load expiration getter: {}", e)))?;
Ok(get_fn(payload))
}
unsafe fn destroy_payload(&self, payload: *mut c_void) {
if let Ok(destroy) = self
.library
.get::<GopherAuthPayloadDestroyFn>(b"gopher_auth_payload_destroy\0")
{
destroy(payload);
}
}
unsafe fn free_string(&self, s: *mut c_char) {
if let Ok(free) = self
.library
.get::<GopherAuthFreeStringFn>(b"gopher_auth_free_string\0")
{
free(s);
}
}
pub fn set_option(&self, key: &str, value: &str) -> Result<(), Error> {
let key_c =
CString::new(key).map_err(|e| Error::auth(format!("Invalid option key: {}", e)))?;
let value_c =
CString::new(value).map_err(|e| Error::auth(format!("Invalid option value: {}", e)))?;
unsafe {
let set_option: Symbol<GopherAuthSetOptionFn> = self
.library
.get(b"gopher_auth_client_set_option\0")
.map_err(|e| Error::auth(format!("Failed to load set_option: {}", e)))?;
let result = set_option(self.handle, key_c.as_ptr(), value_c.as_ptr());
if result != 0 {
return Err(Error::auth(format!(
"Failed to set option '{}', code {}",
key, result
)));
}
}
Ok(())
}
pub fn destroy(&mut self) {
if !self.handle.is_null() {
unsafe {
if let Ok(destroy) = self
.library
.get::<GopherAuthClientDestroyFn>(b"gopher_auth_client_destroy\0")
{
destroy(self.handle);
}
}
self.handle = ptr::null_mut();
}
}
#[cfg(test)]
pub fn dummy() -> Self {
Self {
handle: ptr::null_mut(),
library: Arc::new(unsafe {
Library::new("/dev/null").unwrap_or_else(|_| {
#[cfg(target_os = "macos")]
{
Library::new("/usr/lib/libSystem.B.dylib")
.expect("Failed to load system library for test dummy")
}
#[cfg(target_os = "linux")]
{
Library::new("/lib/x86_64-linux-gnu/libc.so.6")
.or_else(|_| Library::new("/lib/libc.so.6"))
.expect("Failed to load system library for test dummy")
}
#[cfg(target_os = "windows")]
{
Library::new("kernel32.dll")
.expect("Failed to load system library for test dummy")
}
})
}),
}
}
}
impl Drop for GopherAuthClient {
fn drop(&mut self) {
self.destroy();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_result_success() {
let result = ValidationResult::success();
assert!(result.valid);
assert_eq!(result.error_code, 0);
assert!(result.error_message.is_none());
}
#[test]
fn test_validation_result_failure() {
let result = ValidationResult::failure(-1, "Token expired");
assert!(!result.valid);
assert_eq!(result.error_code, -1);
assert_eq!(result.error_message, Some("Token expired".to_string()));
}
#[test]
fn test_token_payload_fields() {
let payload = TokenPayload {
subject: "user123".to_string(),
scopes: "openid profile".to_string(),
audience: "my-app".to_string(),
expiration: 1234567890,
};
assert_eq!(payload.subject, "user123");
assert_eq!(payload.scopes, "openid profile");
assert_eq!(payload.audience, "my-app");
assert_eq!(payload.expiration, 1234567890);
}
#[test]
fn test_token_payload_clone() {
let payload = TokenPayload {
subject: "user".to_string(),
scopes: "read write".to_string(),
audience: "api".to_string(),
expiration: 9999999999,
};
let cloned = payload.clone();
assert_eq!(payload.subject, cloned.subject);
assert_eq!(payload.scopes, cloned.scopes);
}
#[test]
fn test_is_available() {
let _ = GopherAuthClient::is_available();
}
#[test]
#[ignore]
fn test_client_creation() {
let result = GopherAuthClient::new(
"https://example.com/.well-known/jwks.json",
"https://example.com",
);
if let Err(e) = result {
println!(
"Client creation failed (expected without native lib): {}",
e
);
}
}
#[test]
#[ignore]
fn test_client_validate_token() {
let client = match GopherAuthClient::new(
"https://example.com/.well-known/jwks.json",
"https://example.com",
) {
Ok(c) => c,
Err(_) => return, };
let result = client.validate_token("invalid.token.here", 0);
assert!(!result.valid);
}
}