Rust port of OmniVox, wire-compatible with the C++ original
Byte-exact cereal binary layout, RFC 8439 ChaCha20-Poly1305, RFC 7914 scrypt defaults; tokio transport preserving the original's observable behavior (contract B1-B3, B5 quirks included). Parity evidence: golden C++ vectors in tests/vectors.rs and loopback behavior tests; both cross-directions verified against the unmodified C++ binaries. Source: https://git.0xfeedc0de.com/michael/OmniVox
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
//! Port of the C++ `common.h`: shared payload type and demo constants.
|
||||
|
||||
use omnivox::{Cereal, CodecError, Reader, write_byte_vec};
|
||||
|
||||
/// Wire-identical to the C++ `DatagramPayload` (cereal field order:
|
||||
/// `id`, `v1.qword`, `payload`; the `PackedValue64` union only ever
|
||||
/// serialized its `qword`, so it ports as a plain `u64`).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DatagramPayload {
|
||||
pub id: u16,
|
||||
pub v1: u64,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Cereal for DatagramPayload {
|
||||
fn encode(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.id.to_le_bytes());
|
||||
out.extend_from_slice(&self.v1.to_le_bytes());
|
||||
write_byte_vec(out, &self.payload);
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
|
||||
let mut r = Reader::new(bytes);
|
||||
Ok(Self {
|
||||
id: r.u16()?,
|
||||
v1: r.u64()?,
|
||||
payload: r.byte_vec()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const PASSWORD: &str = "SecurePassword123";
|
||||
pub const SALT: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
pub const SERVER_PORT: u16 = 8080;
|
||||
/// Only used by the client example.
|
||||
#[allow(dead_code)]
|
||||
pub const SERVER_ADDR: &str = "127.0.0.1";
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Port of OmniVox-client.cpp: connects, sends "Hello", prints what the
|
||||
//! server echoes, then stops after 15 seconds like the C++ example.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use common::{DatagramPayload, PASSWORD, SALT, SERVER_ADDR, SERVER_PORT};
|
||||
use omnivox::{ConnectionContext, ConnectionStatus, OmniVox, derive_encrypt_key};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(false)
|
||||
.without_time()
|
||||
.init();
|
||||
|
||||
tracing::info!("Connecting to server at {SERVER_ADDR}:{SERVER_PORT}");
|
||||
|
||||
let client = Arc::new(OmniVox::<DatagramPayload>::new());
|
||||
let key = derive_encrypt_key(PASSWORD.as_bytes(), SALT, 17, 8, 1)?;
|
||||
client.set_encrypt_key(key);
|
||||
|
||||
let data_cb = Arc::new(|_data: &DatagramPayload, ctx: &ConnectionContext| {
|
||||
tracing::info!("Server ID: {}, sent data!", ctx.id);
|
||||
});
|
||||
|
||||
let connector = Arc::clone(&client);
|
||||
let connection_cb = Arc::new(move |ctx: &ConnectionContext| match ctx.connected {
|
||||
ConnectionStatus::Connected => {
|
||||
tracing::info!(
|
||||
"[onConnect] Successfully connected to: {}:{}",
|
||||
ctx.ip,
|
||||
ctx.port
|
||||
);
|
||||
let client = Arc::clone(&connector);
|
||||
tokio::spawn(async move {
|
||||
let payload = DatagramPayload {
|
||||
id: 1,
|
||||
v1: 0,
|
||||
payload: b"Hello".to_vec(),
|
||||
};
|
||||
if client.send_to_server(&payload).await {
|
||||
tracing::info!("Sent initial data to server");
|
||||
}
|
||||
});
|
||||
}
|
||||
ConnectionStatus::ConnectionFailed => {
|
||||
tracing::info!(
|
||||
"[onConnect] Connection failed or timed out to: {}:{}",
|
||||
ctx.ip,
|
||||
ctx.port
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
tracing::info!(
|
||||
"[onConnect] Unexpected connection status to: {}:{}",
|
||||
ctx.ip,
|
||||
ctx.port
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
client
|
||||
.connect_to_server(SERVER_ADDR, SERVER_PORT, Some(data_cb), connection_cb)
|
||||
.await?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
||||
client.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Port of OmniVox-server.cpp: echoes each client payload back with the
|
||||
//! client id, refusing no client that holds the key.
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use common::{DatagramPayload, PASSWORD, SALT, SERVER_PORT};
|
||||
use omnivox::{ConnectionContext, Decision, OmniVox, derive_encrypt_key};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(false)
|
||||
.without_time()
|
||||
.init();
|
||||
|
||||
tracing::info!("Starting server on port {SERVER_PORT} with 2 threads");
|
||||
|
||||
let server = Arc::new(OmniVox::<DatagramPayload>::new());
|
||||
let key = derive_encrypt_key(PASSWORD.as_bytes(), SALT, 17, 8, 1)?;
|
||||
server.set_encrypt_key(key);
|
||||
|
||||
// The C++ example's g_clientHalt map, kept for behavioral parity.
|
||||
let client_halt: Arc<Mutex<HashMap<u32, bool>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let responder = Arc::clone(&server);
|
||||
let received = Arc::new(move |data: &DatagramPayload, ctx: &ConnectionContext| {
|
||||
tracing::info!(
|
||||
"Received data from client {}: Payload size={}",
|
||||
ctx.id,
|
||||
data.payload.len()
|
||||
);
|
||||
if !data.payload.is_empty() {
|
||||
tracing::info!(
|
||||
"Payload content: {}",
|
||||
String::from_utf8_lossy(&data.payload)
|
||||
);
|
||||
}
|
||||
let response = DatagramPayload {
|
||||
id: ctx.id as u16,
|
||||
v1: 0,
|
||||
payload: data.payload.clone(),
|
||||
};
|
||||
let server = Arc::clone(&responder);
|
||||
let id = ctx.id;
|
||||
// C++ echoed synchronously inside the callback; the Rust callback is
|
||||
// sync, so the echo rides a task — per-connection socket order is
|
||||
// still serialized by the client's write lock (contract B3).
|
||||
tokio::spawn(async move {
|
||||
if server.send_to_client(&response, id).await {
|
||||
tracing::info!("Sent response to client {id}");
|
||||
} else {
|
||||
tracing::info!("Error sending data to client {id}");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let halt = Arc::clone(&client_halt);
|
||||
let disconnect = Arc::new(move |ctx: &ConnectionContext| {
|
||||
tracing::info!(
|
||||
"Client {} {}:{} is disconnecting from the server cleaning up objects.",
|
||||
ctx.id,
|
||||
ctx.ip,
|
||||
ctx.port
|
||||
);
|
||||
*halt.lock().unwrap().entry(ctx.id).or_default() = true;
|
||||
});
|
||||
|
||||
let halt = Arc::clone(&client_halt);
|
||||
let connect = Arc::new(move |ctx: &ConnectionContext| {
|
||||
*halt.lock().unwrap().entry(ctx.id).or_default() = false;
|
||||
tracing::info!("New client has connected - {}:{}", ctx.ip, ctx.port);
|
||||
Decision::Allow
|
||||
});
|
||||
|
||||
server
|
||||
.start_server(SERVER_PORT, 2, 15000, received, disconnect, connect)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Server started successfully");
|
||||
|
||||
// C++ slept u32::MAX hours; we serve until killed, like the reference.
|
||||
std::future::pending::<()>().await;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user