#![allow(unsafe_op_in_unsafe_fn)]
use gbp_stack::core::{ControlOpcode, NodeState, PayloadCodec, SignalType, StreamType};
use gbp_stack::{
CipherSuite, DeliveredPayload, ErrorObject, Event, GapAccept, GapClient, GbpFrame, GroupNode,
GspAccept, GspClient, GtpAccept, GtpClient, MlsContext, OutboundFrame, ProcessedKind,
SFrameDecryptor, SFrameEncryptor, SFrameSession, StreamLabel,
};
use openmls::prelude::tls_codec::Serialize as _;
use openmls::prelude::*;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{CString, c_char};
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, Mutex};
#[repr(C)]
pub struct GbpBuffer {
pub ptr: *mut u8,
pub len: usize,
pub cap: usize,
}
impl GbpBuffer {
fn empty() -> Self {
Self {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
}
}
fn from_vec(mut v: Vec<u8>) -> Self {
let ptr = v.as_mut_ptr();
let len = v.len();
let cap = v.capacity();
std::mem::forget(v);
Self { ptr, len, cap }
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_buffer_free(buf: GbpBuffer) {
if buf.ptr.is_null() {
return;
}
unsafe {
let _ = Vec::from_raw_parts(buf.ptr, buf.len, buf.cap);
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_string_free(ptr: *mut c_char) {
if ptr.is_null() {
return;
}
unsafe {
let _ = CString::from_raw(ptr);
}
}
fn alloc_cstring(s: &str) -> *mut c_char {
CString::new(s.as_bytes())
.unwrap_or_else(|_| CString::new(s.replace('\0', "?")).unwrap())
.into_raw()
}
thread_local! {
static LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
}
fn set_last_error(e: impl ToString) {
LAST_ERROR.with(|s| *s.borrow_mut() = e.to_string());
}
fn clear_last_error() {
LAST_ERROR.with(|s| s.borrow_mut().clear());
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_last_error() -> *mut c_char {
LAST_ERROR.with(|s| alloc_cstring(&s.borrow()))
}
macro_rules! registry {
($vis:vis $name:ident<$t:ty>) => {
$vis struct $name {
next: AtomicI32,
map: Mutex<HashMap<i32, Arc<Mutex<$t>>>>,
}
impl $name {
fn new() -> Self {
Self { next: AtomicI32::new(1), map: Mutex::new(HashMap::new()) }
}
fn insert(&self, v: $t) -> i32 {
let id = self.next.fetch_add(1, Ordering::Relaxed);
self.map.lock().unwrap().insert(id, Arc::new(Mutex::new(v)));
id
}
fn remove(&self, id: i32) {
self.map.lock().unwrap().remove(&id);
}
fn get(&self, id: i32) -> Option<Arc<Mutex<$t>>> {
self.map.lock().unwrap().get(&id).cloned()
}
}
};
}
registry!(MlsRegistry<MlsContext>);
registry!(NodeRegistry<GroupNode>);
registry!(GtpRegistry<GtpClient>);
registry!(GapRegistry<GapClient>);
registry!(GspRegistry<GspClient>);
registry!(SFrameSessionRegistry<SFrameDecryptor>);
registry!(SFrameEncryptorRegistry<SFrameEncryptor>);
struct MlsBundles {
map: Mutex<HashMap<i32, KeyPackageBundle>>,
}
impl MlsBundles {
fn new() -> Self {
Self {
map: Mutex::new(HashMap::new()),
}
}
}
fn mls() -> &'static MlsRegistry {
use std::sync::OnceLock;
static R: OnceLock<MlsRegistry> = OnceLock::new();
R.get_or_init(MlsRegistry::new)
}
fn mls_bundles() -> &'static MlsBundles {
use std::sync::OnceLock;
static R: OnceLock<MlsBundles> = OnceLock::new();
R.get_or_init(MlsBundles::new)
}
fn nodes() -> &'static NodeRegistry {
use std::sync::OnceLock;
static R: OnceLock<NodeRegistry> = OnceLock::new();
R.get_or_init(NodeRegistry::new)
}
fn gtps() -> &'static GtpRegistry {
use std::sync::OnceLock;
static R: OnceLock<GtpRegistry> = OnceLock::new();
R.get_or_init(GtpRegistry::new)
}
fn gaps() -> &'static GapRegistry {
use std::sync::OnceLock;
static R: OnceLock<GapRegistry> = OnceLock::new();
R.get_or_init(GapRegistry::new)
}
fn gsps() -> &'static GspRegistry {
use std::sync::OnceLock;
static R: OnceLock<GspRegistry> = OnceLock::new();
R.get_or_init(GspRegistry::new)
}
fn sframe_sessions() -> &'static SFrameSessionRegistry {
use std::sync::OnceLock;
static R: OnceLock<SFrameSessionRegistry> = OnceLock::new();
R.get_or_init(SFrameSessionRegistry::new)
}
fn sframe_encryptors() -> &'static SFrameEncryptorRegistry {
use std::sync::OnceLock;
static R: OnceLock<SFrameEncryptorRegistry> = OnceLock::new();
R.get_or_init(SFrameEncryptorRegistry::new)
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_version() -> *mut c_char {
alloc_cstring(&format!(
"group-protocol-stack {} (gbp + gtp + gap + gsp)",
env!("CARGO_PKG_VERSION")
))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_create(identity_ptr: *const u8, identity_len: usize) -> i32 {
clear_last_error();
let ident = unsafe { std::slice::from_raw_parts(identity_ptr, identity_len) };
match MlsContext::new_member(ident) {
Ok((ctx, kp)) => {
let id = mls().insert(ctx);
mls_bundles().map.lock().unwrap().insert(id, kp);
id
}
Err(e) => {
set_last_error(e);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_destroy(h: i32) {
mls().remove(h);
mls_bundles().map.lock().unwrap().remove(&h);
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_epoch(h: i32) -> u64 {
mls().get(h).map(|c| c.lock().unwrap().epoch()).unwrap_or(0)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_group_id(h: i32, out16: *mut u8) -> bool {
clear_last_error();
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return false;
};
let ctx = ctx_arc.lock().unwrap();
let gid = ctx.group_id_16();
unsafe { std::ptr::copy_nonoverlapping(gid.as_ptr(), out16, 16) };
true
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_export_key_package(h: i32) -> GbpBuffer {
clear_last_error();
let bundles = mls_bundles().map.lock().unwrap();
let Some(b) = bundles.get(&h) else {
set_last_error("invalid MLS handle");
return GbpBuffer::empty();
};
match b.key_package().tls_serialize_detached() {
Ok(b) => GbpBuffer::from_vec(b),
Err(e) => {
set_last_error(format!("kp serialize: {e:?}"));
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_invite(h: i32, kp_ptr: *const u8, kp_len: usize) -> GbpBuffer {
clear_last_error();
let bytes = unsafe { std::slice::from_raw_parts(kp_ptr, kp_len) };
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return GbpBuffer::empty();
};
let mut ctx = ctx_arc.lock().unwrap();
let kp_in = match KeyPackageIn::tls_deserialize_exact_bytes(bytes) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("kp parse: {e:?}"));
return GbpBuffer::empty();
}
};
let validated = match kp_in.validate(ctx.provider.crypto(), ProtocolVersion::Mls10) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("kp validate: {e:?}"));
return GbpBuffer::empty();
}
};
match ctx.invite(&[validated]) {
Ok(welcome) => GbpBuffer::from_vec(welcome),
Err(e) => {
set_last_error(e);
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_invite_full(
h: i32,
kp_ptr: *const u8,
kp_len: usize,
) -> GbpBuffer {
clear_last_error();
let bytes = unsafe { std::slice::from_raw_parts(kp_ptr, kp_len) };
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return GbpBuffer::empty();
};
let mut ctx = ctx_arc.lock().unwrap();
let kp_in = match KeyPackageIn::tls_deserialize_exact_bytes(bytes) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("kp parse: {e:?}"));
return GbpBuffer::empty();
}
};
let validated = match kp_in.validate(ctx.provider.crypto(), ProtocolVersion::Mls10) {
Ok(v) => v,
Err(e) => {
set_last_error(format!("kp validate: {e:?}"));
return GbpBuffer::empty();
}
};
match ctx.invite_full(&[validated]) {
Ok((commit, welcome)) => {
let mut out = Vec::with_capacity(4 + commit.len() + welcome.len());
out.extend_from_slice(&(commit.len() as u32).to_le_bytes());
out.extend_from_slice(&commit);
out.extend_from_slice(&welcome);
GbpBuffer::from_vec(out)
}
Err(e) => {
set_last_error(e);
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_remove(h: i32, leaf_index: u32) -> GbpBuffer {
clear_last_error();
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return GbpBuffer::empty();
};
let mut ctx = ctx_arc.lock().unwrap();
match ctx.remove_members(&[leaf_index]) {
Ok(commit) => GbpBuffer::from_vec(commit),
Err(e) => {
set_last_error(e);
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_process_message(
h: i32,
msg_ptr: *const u8,
msg_len: usize,
) -> u32 {
clear_last_error();
let bytes = unsafe { std::slice::from_raw_parts(msg_ptr, msg_len) };
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return 0;
};
let mut ctx = ctx_arc.lock().unwrap();
match ctx.process_message(bytes) {
Ok(ProcessedKind::Commit) => 1,
Ok(ProcessedKind::Application) => 2,
Ok(ProcessedKind::Proposal) => 3,
Ok(ProcessedKind::External) => 4,
Err(e) => {
set_last_error(e);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_finalize_commit(h: i32) -> bool {
clear_last_error();
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return false;
};
let mut ctx = ctx_arc.lock().unwrap();
match ctx.finalize_pending_commit() {
Ok(()) => true,
Err(e) => {
set_last_error(e);
false
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_clear_pending_commit(h: i32) -> bool {
clear_last_error();
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return false;
};
let mut ctx = ctx_arc.lock().unwrap();
match ctx.clear_pending_commit() {
Ok(()) => true,
Err(e) => {
set_last_error(e);
false
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_accept_welcome(
h: i32,
welcome_ptr: *const u8,
welcome_len: usize,
) -> bool {
clear_last_error();
let bytes = unsafe { std::slice::from_raw_parts(welcome_ptr, welcome_len) };
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return false;
};
let mut ctx = ctx_arc.lock().unwrap();
match ctx.accept_welcome(bytes) {
Ok(()) => true,
Err(e) => {
set_last_error(e);
false
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_mls_export_state(h: i32) -> GbpBuffer {
clear_last_error();
let Some(ctx_arc) = mls().get(h) else {
set_last_error("invalid MLS handle");
return GbpBuffer::empty();
};
let ctx = ctx_arc.lock().unwrap();
match ctx.export_state() {
Ok(bytes) => GbpBuffer::from_vec(bytes),
Err(e) => {
set_last_error(e);
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_mls_restore_state(ptr: *const u8, len: usize) -> i32 {
clear_last_error();
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
match MlsContext::restore_state(bytes) {
Ok(ctx) => mls().insert(ctx),
Err(e) => {
set_last_error(e);
0
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_node_create(member_id: u32, group_id_16: *const u8) -> i32 {
clear_last_error();
let mut gid = [0u8; 16];
unsafe { std::ptr::copy_nonoverlapping(group_id_16, gid.as_mut_ptr(), 16) };
nodes().insert(GroupNode::new(member_id, gid))
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_destroy(h: i32) {
nodes().remove(h);
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_bootstrap_creator(h: i32, epoch: u64) -> bool {
let Some(n_arc) = nodes().get(h) else {
return false;
};
n_arc.lock().unwrap().bootstrap_as_creator(epoch);
true
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_bootstrap_joiner(h: i32, epoch: u64, expected_first_tid: u32) -> bool {
let Some(n_arc) = nodes().get(h) else {
return false;
};
n_arc
.lock()
.unwrap()
.bootstrap_as_joiner(epoch, expected_first_tid);
true
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_state(h: i32) -> u32 {
nodes()
.get(h)
.map(|n| n.lock().unwrap().state as u32)
.unwrap_or(u32::MAX)
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_epoch(h: i32) -> u64 {
nodes()
.get(h)
.map(|n| n.lock().unwrap().current_epoch)
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_last_transition_id(h: i32) -> u32 {
nodes()
.get(h)
.map(|n| n.lock().unwrap().last_transition_id)
.unwrap_or(0)
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_set_epoch(h: i32, epoch: u64) -> bool {
let Some(n_arc) = nodes().get(h) else {
return false;
};
n_arc.lock().unwrap().current_epoch = epoch;
true
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_apply_transition(h: i32, tid: u32) -> bool {
let Some(n_arc) = nodes().get(h) else {
return false;
};
n_arc.lock().unwrap().apply_transition(tid);
true
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_node_send_control(
nh: i32,
mh: i32,
target: u32,
opcode: u16,
transition_id: u32,
request_id: u32,
args_ptr: *const u8,
args_len: usize,
) -> GbpBuffer {
clear_last_error();
let op = match ControlOpcode::try_from(opcode) {
Ok(o) => o,
Err(_) => {
set_last_error(format!("bad opcode 0x{opcode:04X}"));
return GbpBuffer::empty();
}
};
let args = if args_len == 0 {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(args_ptr, args_len) }.to_vec()
};
let (n_arc, m_arc) = (nodes().get(nh), mls().get(mh));
let (Some(n_arc), Some(m_arc)) = (n_arc, m_arc) else {
set_last_error("bad node/mls handle");
return GbpBuffer::empty();
};
let mut n = n_arc.lock().unwrap();
let mut m = m_arc.lock().unwrap();
match n.send_control(&mut *m, target, op, transition_id, request_id, args) {
Ok(of) => outbound_to_buffer(of),
Err(e) => {
set_last_error(e.to_string());
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_node_on_wire(
nh: i32,
mh: i32,
wire_ptr: *const u8,
wire_len: usize,
) -> *mut c_char {
clear_last_error();
let wire = unsafe { std::slice::from_raw_parts(wire_ptr, wire_len) };
let (n_arc, m_arc) = (nodes().get(nh), mls().get(mh));
let (Some(n_arc), Some(m_arc)) = (n_arc, m_arc) else {
set_last_error("bad node/mls handle");
return alloc_cstring("[]");
};
let mut n = n_arc.lock().unwrap();
let mut m = m_arc.lock().unwrap();
let events = match n.on_wire(&mut *m, wire) {
Ok(e) => e,
Err(e) => {
set_last_error(e.to_string());
return alloc_cstring("[]");
}
};
alloc_cstring(&events_to_json(&events))
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_node_drain_events(nh: i32) -> *mut c_char {
let Some(n_arc) = nodes().get(nh) else {
return alloc_cstring("[]");
};
alloc_cstring(&events_to_json(&n_arc.lock().unwrap().drain_events()))
}
fn outbound_to_buffer(of: OutboundFrame) -> GbpBuffer {
let mut out = Vec::with_capacity(4 + of.wire.len());
out.extend_from_slice(&of.to.to_le_bytes());
out.extend_from_slice(&of.wire);
GbpBuffer::from_vec(out)
}
#[unsafe(no_mangle)]
pub extern "C" fn gtp_client_create() -> i32 {
gtps().insert(GtpClient::new())
}
#[unsafe(no_mangle)]
pub extern "C" fn gtp_client_destroy(h: i32) {
gtps().remove(h);
}
#[unsafe(no_mangle)]
pub extern "C" fn gtp_client_reset(h: i32) {
if let Some(c) = gtps().get(h) {
c.lock().unwrap().reset();
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gtp_client_send(
ch: i32,
nh: i32,
mh: i32,
target: u32,
message_id: u64,
text_ptr: *const u8,
text_len: usize,
codec: u8,
) -> GbpBuffer {
clear_last_error();
let text = unsafe { std::slice::from_raw_parts(text_ptr, text_len) };
let text = match std::str::from_utf8(text) {
Ok(s) => s,
Err(e) => {
set_last_error(format!("utf8: {e}"));
return GbpBuffer::empty();
}
};
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let (c_arc, n_arc, m_arc) = (gtps().get(ch), nodes().get(nh), mls().get(mh));
let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
set_last_error("bad handle");
return GbpBuffer::empty();
};
let mut c = c_arc.lock().unwrap();
let mut n = n_arc.lock().unwrap();
let mut m = m_arc.lock().unwrap();
match c.send(&mut *n, &mut *m, target, message_id, text, codec) {
Ok(of) => outbound_to_buffer(of),
Err(e) => {
set_last_error(e.to_string());
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gtp_client_accept(
ch: i32,
current_epoch: u64,
pt_ptr: *const u8,
pt_len: usize,
codec: u8,
) -> *mut c_char {
clear_last_error();
let pt = unsafe { std::slice::from_raw_parts(pt_ptr, pt_len) };
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let Some(c_arc) = gtps().get(ch) else {
return alloc_cstring(r#"{"status":"error","reason":"bad client"}"#);
};
let mut c = c_arc.lock().unwrap();
#[derive(Serialize)]
struct Out<'a> {
status: &'a str,
sender: Option<u32>,
message_id: Option<u64>,
text: Option<String>,
reason: Option<String>,
}
let out = match c.accept(pt, current_epoch, codec) {
Ok(GtpAccept::New(m)) => Out {
status: "new",
sender: Some(m.sender_id),
message_id: Some(m.message_id),
text: Some(m.text().unwrap_or("<binary>").to_string()),
reason: None,
},
Ok(GtpAccept::Duplicate(m)) => Out {
status: "duplicate",
sender: Some(m.sender_id),
message_id: Some(m.message_id),
text: Some(m.text().unwrap_or("<binary>").to_string()),
reason: None,
},
Err(e) => Out {
status: "error",
sender: None,
message_id: None,
text: None,
reason: Some(e.to_string()),
},
};
alloc_cstring(&serde_json::to_string(&out).unwrap_or_default())
}
#[unsafe(no_mangle)]
pub extern "C" fn gap_client_create() -> i32 {
gaps().insert(GapClient::new())
}
#[unsafe(no_mangle)]
pub extern "C" fn gap_client_destroy(h: i32) {
gaps().remove(h);
}
#[unsafe(no_mangle)]
pub extern "C" fn gap_client_reset(h: i32) {
if let Some(c) = gaps().get(h) {
c.lock().unwrap().reset();
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gap_client_send(
ch: i32,
nh: i32,
mh: i32,
target: u32,
media_source_id: u32,
rtp_timestamp: u64,
opus_ptr: *const u8,
opus_len: usize,
codec: u8,
) -> GbpBuffer {
clear_last_error();
let opus = unsafe { std::slice::from_raw_parts(opus_ptr, opus_len) }.to_vec();
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let (c_arc, n_arc, m_arc) = (gaps().get(ch), nodes().get(nh), mls().get(mh));
let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
set_last_error("bad handle");
return GbpBuffer::empty();
};
let mut c = c_arc.lock().unwrap();
let mut n = n_arc.lock().unwrap();
let mut m = m_arc.lock().unwrap();
match c.send(
&mut *n,
&mut *m,
target,
media_source_id,
rtp_timestamp,
opus,
codec,
) {
Ok(of) => outbound_to_buffer(of),
Err(e) => {
set_last_error(e.to_string());
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gap_client_accept(
ch: i32,
current_epoch: u64,
pt_ptr: *const u8,
pt_len: usize,
codec: u8,
) -> *mut c_char {
clear_last_error();
let pt = unsafe { std::slice::from_raw_parts(pt_ptr, pt_len) };
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let Some(c_arc) = gaps().get(ch) else {
return alloc_cstring(r#"{"status":"error","reason":"bad client"}"#);
};
let mut c = c_arc.lock().unwrap();
#[derive(Serialize)]
struct Out<'a> {
status: &'a str,
source: Option<u32>,
seq: Option<u32>,
bytes: Option<usize>,
reason: Option<String>,
}
let out = match c.accept(pt, current_epoch, codec) {
Ok(GapAccept::New(p)) => Out {
status: "new",
source: Some(p.media_source_id),
seq: Some(p.rtp_sequence),
bytes: Some(p.opus_frame.len()),
reason: None,
},
Ok(GapAccept::Late(p)) => Out {
status: "late",
source: Some(p.media_source_id),
seq: Some(p.rtp_sequence),
bytes: Some(p.opus_frame.len()),
reason: None,
},
Err(e) => Out {
status: "error",
source: None,
seq: None,
bytes: None,
reason: Some(e.to_string()),
},
};
alloc_cstring(&serde_json::to_string(&out).unwrap_or_default())
}
#[unsafe(no_mangle)]
pub extern "C" fn gsp_client_create() -> i32 {
gsps().insert(GspClient::new())
}
#[unsafe(no_mangle)]
pub extern "C" fn gsp_client_destroy(h: i32) {
gsps().remove(h);
}
#[unsafe(no_mangle)]
pub extern "C" fn gsp_client_reset(h: i32) {
if let Some(c) = gsps().get(h) {
c.lock().unwrap().reset();
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gsp_client_send(
ch: i32,
nh: i32,
mh: i32,
target: u32,
signal_type: u32,
role_claim: u32,
request_id: u32,
codec: u8,
) -> GbpBuffer {
clear_last_error();
let sig = match SignalType::try_from(signal_type) {
Ok(s) => s,
Err(_) => {
set_last_error(format!("bad signal {signal_type}"));
return GbpBuffer::empty();
}
};
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let (c_arc, n_arc, m_arc) = (gsps().get(ch), nodes().get(nh), mls().get(mh));
let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
set_last_error("bad handle");
return GbpBuffer::empty();
};
let mut c = c_arc.lock().unwrap();
let mut n = n_arc.lock().unwrap();
let mut m = m_arc.lock().unwrap();
match c.send(&mut *n, &mut *m, target, sig, role_claim, request_id, codec) {
Ok(of) => outbound_to_buffer(of),
Err(e) => {
set_last_error(e.to_string());
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gsp_client_send_with_args(
ch: i32,
nh: i32,
mh: i32,
target: u32,
signal_type: u32,
role_claim: u32,
request_id: u32,
args_ptr: *const u8,
args_len: usize,
codec: u8,
) -> GbpBuffer {
clear_last_error();
let args: &[u8] = if args_len == 0 || args_ptr.is_null() {
&[]
} else {
unsafe { std::slice::from_raw_parts(args_ptr, args_len) }
};
let sig = match SignalType::try_from(signal_type) {
Ok(s) => s,
Err(_) => {
set_last_error(format!("bad signal {signal_type}"));
return GbpBuffer::empty();
}
};
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let (c_arc, n_arc, m_arc) = (gsps().get(ch), nodes().get(nh), mls().get(mh));
let (Some(c_arc), Some(n_arc), Some(m_arc)) = (c_arc, n_arc, m_arc) else {
set_last_error("bad handle");
return GbpBuffer::empty();
};
let mut c = c_arc.lock().unwrap();
let mut n = n_arc.lock().unwrap();
let mut m = m_arc.lock().unwrap();
match c.send_with_args(&mut *n, &mut *m, target, sig, role_claim, request_id, args, codec) {
Ok(of) => outbound_to_buffer(of),
Err(e) => {
set_last_error(e.to_string());
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gsp_client_accept(
ch: i32,
current_epoch: u64,
pt_ptr: *const u8,
pt_len: usize,
codec: u8,
) -> *mut c_char {
clear_last_error();
let pt = unsafe { std::slice::from_raw_parts(pt_ptr, pt_len) };
let codec = PayloadCodec::from_u8(codec).unwrap_or(PayloadCodec::Cbor);
let Some(c_arc) = gsps().get(ch) else {
return alloc_cstring(r#"{"status":"error","reason":"bad client"}"#);
};
let mut c = c_arc.lock().unwrap();
#[derive(Serialize)]
struct Out<'a> {
status: &'a str,
signal: Option<&'a str>,
signal_code: Option<u32>,
sender: Option<u32>,
role_claim: Option<u32>,
request_id: Option<u32>,
reason: Option<String>,
}
let out = match c.accept(pt, current_epoch, codec) {
Ok(GspAccept {
signal,
sender_id,
role_claim,
request_id,
}) => Out {
status: "new",
signal: Some(signal.name()),
signal_code: Some(signal as u32),
sender: Some(sender_id),
role_claim: Some(role_claim),
request_id: Some(request_id),
reason: None,
},
Err(gbp_stack::GspError::DuplicateRequest(rid)) => Out {
status: "duplicate",
signal: None,
signal_code: None,
sender: None,
role_claim: None,
request_id: Some(rid),
reason: None,
},
Err(e) => Out {
status: "error",
signal: None,
signal_code: None,
sender: None,
role_claim: None,
request_id: None,
reason: Some(e.to_string()),
},
};
alloc_cstring(&serde_json::to_string(&out).unwrap_or_default())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_frame_encode_v(
version: u8,
group_id_16: *const u8,
epoch: u64,
transition_id: u32,
stream_type: u32,
stream_id: u32,
flags: u16,
sequence_no: u32,
payload_ptr: *const u8,
payload_len: usize,
) -> GbpBuffer {
clear_last_error();
let mut gid = [0u8; 16];
unsafe { std::ptr::copy_nonoverlapping(group_id_16, gid.as_mut_ptr(), 16) };
let st_u8 = StreamType::try_from(stream_type)
.map(|s| s as u8)
.unwrap_or(stream_type as u8);
let payload: Vec<u8> = if payload_len == 0 || payload_ptr.is_null() {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len) }.to_vec()
};
let frame = gbp_stack::gbp::GbpFrame {
version,
group_id: serde_bytes::ByteBuf::from(gid.to_vec()),
epoch,
transition_id,
stream_type: st_u8,
stream_id,
flags,
sequence_no,
payload_format: 0u8,
payload_size: payload.len() as u32,
encrypted_payload: serde_bytes::ByteBuf::from(payload),
};
GbpBuffer::from_vec(frame.to_cbor())
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_error_lookup(code: u16) -> GbpBuffer {
use gbp_stack::core::errors::ErrorSpec;
match ErrorSpec::lookup(code) {
Some(spec) => GbpBuffer::from_vec(ErrorObject::from_spec(spec, spec.name).to_cbor()),
None => {
set_last_error(format!("unknown error code 0x{code:04X}"));
GbpBuffer::empty()
}
}
}
#[allow(dead_code)]
fn _link(_f: &GbpFrame, _l: StreamLabel) {}
#[derive(Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum EventDto<'a> {
StateChanged {
from: String,
to: String,
},
PayloadReceived {
stream_type: &'a str,
stream_type_code: u32,
stream_id: u32,
sequence_no: u32,
flags: u16,
codec: u8,
plaintext_b64: String,
},
Control {
from: u32,
opcode: &'a str,
opcode_code: u16,
transition_id: u32,
request_id: u32,
args_b64: String,
},
Error {
code: u16,
code_hex: String,
class: u8,
retryable: bool,
fatal: bool,
reason: String,
},
EpochAdvanced {
epoch: u64,
transition_id: u32,
},
CoordinatorElectionNeeded {},
BecameCoordinator {},
CoordinatorClaim {
claimant: u32,
},
}
fn b64(b: &[u8]) -> String {
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
let mut i = 0;
while i + 3 <= b.len() {
let n = ((b[i] as u32) << 16) | ((b[i + 1] as u32) << 8) | (b[i + 2] as u32);
out.push(A[(n >> 18) as usize & 0x3F] as char);
out.push(A[(n >> 12) as usize & 0x3F] as char);
out.push(A[(n >> 6) as usize & 0x3F] as char);
out.push(A[n as usize & 0x3F] as char);
i += 3;
}
let rem = b.len() - i;
if rem == 1 {
let n = (b[i] as u32) << 16;
out.push(A[(n >> 18) as usize & 0x3F] as char);
out.push(A[(n >> 12) as usize & 0x3F] as char);
out.push('=');
out.push('=');
} else if rem == 2 {
let n = ((b[i] as u32) << 16) | ((b[i + 1] as u32) << 8);
out.push(A[(n >> 18) as usize & 0x3F] as char);
out.push(A[(n >> 12) as usize & 0x3F] as char);
out.push(A[(n >> 6) as usize & 0x3F] as char);
out.push('=');
}
out
}
fn dto<'a>(e: &'a Event) -> EventDto<'a> {
match e {
Event::StateChanged { from, to } => EventDto::StateChanged {
from: from.to_string(),
to: to.to_string(),
},
Event::PayloadReceived(DeliveredPayload {
stream_type,
stream_id,
sequence_no,
flags,
plaintext,
codec,
}) => EventDto::PayloadReceived {
stream_type: match stream_type {
StreamType::Control => "control",
StreamType::Audio => "audio",
StreamType::Text => "text",
StreamType::Signal => "signal",
},
stream_type_code: *stream_type as u32,
stream_id: *stream_id,
sequence_no: *sequence_no,
flags: *flags,
codec: codec.as_u8(),
plaintext_b64: b64(plaintext),
},
Event::Control {
from,
opcode,
transition_id,
request_id,
args,
} => EventDto::Control {
from: *from,
opcode: opcode.name(),
opcode_code: *opcode as u16,
transition_id: *transition_id,
request_id: *request_id,
args_b64: b64(args),
},
Event::Error {
code,
class,
retryable,
fatal,
reason,
} => EventDto::Error {
code: *code,
code_hex: format!("0x{code:04X}"),
class: *class as u8,
retryable: *retryable,
fatal: *fatal,
reason: reason.clone(),
},
Event::EpochAdvanced {
epoch,
transition_id,
} => EventDto::EpochAdvanced {
epoch: *epoch,
transition_id: *transition_id,
},
Event::CoordinatorElectionNeeded => EventDto::CoordinatorElectionNeeded {},
Event::BecameCoordinator => EventDto::BecameCoordinator {},
Event::CoordinatorClaim { claimant } => EventDto::CoordinatorClaim {
claimant: *claimant,
},
}
}
fn events_to_json(events: &[Event]) -> String {
let dtos: Vec<EventDto> = events.iter().map(dto).collect();
serde_json::to_string(&dtos).unwrap_or_else(|_| "[]".to_string())
}
#[allow(dead_code)]
const _STATES: [NodeState; 7] = [
NodeState::Idle,
NodeState::Connecting,
NodeState::EstablishingGroup,
NodeState::Active,
NodeState::Resyncing,
NodeState::Failed,
NodeState::Closed,
];
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_sframe_session_create(
mls_handle: i32,
suite: u8,
label_ptr: *const u8,
label_len: usize,
) -> i32 {
clear_last_error();
let suite = match CipherSuite::from_u8(suite) {
Some(s) => s,
None => {
set_last_error(format!("unknown ciphersuite {suite}"));
return 0;
}
};
let label = unsafe {
match std::str::from_utf8(std::slice::from_raw_parts(label_ptr, label_len)) {
Ok(s) => s,
Err(e) => {
set_last_error(e);
return 0;
}
}
};
let Some(mls_arc) = mls().get(mls_handle) else {
set_last_error("invalid MLS handle");
return 0;
};
let mls = mls_arc.lock().unwrap();
match SFrameSession::from_mls(&mls, label, suite) {
Ok(session) => sframe_sessions().insert(session.decryptor()),
Err(e) => {
set_last_error(e);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_sframe_session_free(handle: i32) {
sframe_sessions().remove(handle);
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_sframe_encryptor_create(
mls_handle: i32,
session_handle: i32,
leaf_index: u32,
suite: u8,
label_ptr: *const u8,
label_len: usize,
) -> i32 {
clear_last_error();
let suite = match CipherSuite::from_u8(suite) {
Some(s) => s,
None => {
set_last_error(format!("unknown ciphersuite {suite}"));
return 0;
}
};
let label = unsafe {
match std::str::from_utf8(std::slice::from_raw_parts(label_ptr, label_len)) {
Ok(s) => s,
Err(e) => {
set_last_error(e);
return 0;
}
}
};
if sframe_sessions().get(session_handle).is_none() {
set_last_error("invalid session handle");
return 0;
}
let Some(mls_arc) = mls().get(mls_handle) else {
set_last_error("invalid MLS handle");
return 0;
};
let mls = mls_arc.lock().unwrap();
match SFrameSession::from_mls(&mls, label, suite) {
Ok(session) => sframe_encryptors().insert(session.encryptor(leaf_index)),
Err(e) => {
set_last_error(e);
0
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gbp_sframe_encryptor_free(handle: i32) {
sframe_encryptors().remove(handle);
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_sframe_encrypt(
enc_handle: i32,
plaintext_ptr: *const u8,
plaintext_len: usize,
aad_ptr: *const u8,
aad_len: usize,
) -> GbpBuffer {
clear_last_error();
let Some(enc_arc) = sframe_encryptors().get(enc_handle) else {
set_last_error("invalid encryptor handle");
return GbpBuffer::empty();
};
let plaintext = unsafe { std::slice::from_raw_parts(plaintext_ptr, plaintext_len) };
let aad = if aad_ptr.is_null() || aad_len == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(aad_ptr, aad_len) }
};
let mut enc = enc_arc.lock().unwrap();
match enc.encrypt(plaintext, aad) {
Ok(payload) => GbpBuffer::from_vec(payload),
Err(e) => {
set_last_error(e);
GbpBuffer::empty()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn gbp_sframe_decrypt(
session_handle: i32,
payload_ptr: *const u8,
payload_len: usize,
aad_ptr: *const u8,
aad_len: usize,
sender_leaf_out: *mut u32,
) -> GbpBuffer {
clear_last_error();
let Some(session_arc) = sframe_sessions().get(session_handle) else {
set_last_error("invalid session handle");
return GbpBuffer::empty();
};
let payload = unsafe { std::slice::from_raw_parts(payload_ptr, payload_len) };
let aad = if aad_ptr.is_null() || aad_len == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(aad_ptr, aad_len) }
};
let mut dec = session_arc.lock().unwrap();
match dec.decrypt(payload, aad) {
Ok((plaintext, leaf)) => {
if !sender_leaf_out.is_null() {
unsafe {
*sender_leaf_out = leaf;
}
}
GbpBuffer::from_vec(plaintext)
}
Err(e) => {
set_last_error(e);
GbpBuffer::empty()
}
}
}
#[cfg(test)]
mod tests {
use super::b64;
#[test]
fn b64_empty() {
assert_eq!(b64(b""), "");
}
#[test]
fn b64_single_byte() {
let s = b64(b"f");
assert_eq!(s, "Zg==");
}
#[test]
fn b64_two_bytes() {
let s = b64(b"fo");
assert_eq!(s, "Zm8=");
}
#[test]
fn b64_three_bytes() {
let s = b64(b"foo");
assert_eq!(s, "Zm9v");
}
#[test]
fn b64_known_vectors() {
assert_eq!(b64(b""), "");
assert_eq!(b64(b"f"), "Zg==");
assert_eq!(b64(b"fo"), "Zm8=");
assert_eq!(b64(b"foo"), "Zm9v");
assert_eq!(b64(b"foob"), "Zm9vYg==");
assert_eq!(b64(b"fooba"), "Zm9vYmE=");
assert_eq!(b64(b"foobar"), "Zm9vYmFy");
}
#[test]
fn b64_padding_roundtrip() {
for b in 0u8..=255 {
let input = [b];
let enc = b64(&input);
assert_eq!(enc.len(), 4, "len mismatch for 0x{b:02X}: {enc}");
assert!(enc.ends_with("=="), "missing padding for 0x{b:02X}: {enc}");
}
}
}