pub mod actions;
use anyhow::{Context, Result};
use futures::{SinkExt, StreamExt};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex};
use tokio_tungstenite::{
accept_async_with_config,
tungstenite::{protocol::WebSocketConfig, Message},
};
use tracing::{debug, error, info, trace, warn};
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::WebRtcSignalingProtocol;
use crate::state::app_state::AppState;
use crate::state::ServerId;
use actions::{
WEBRTC_SIGNALING_MESSAGE_RECEIVED_EVENT, WEBRTC_SIGNALING_PEER_CONNECTED_EVENT,
WEBRTC_SIGNALING_PEER_DISCONNECTED_EVENT,
};
pub type PeerId = String;
const SIGNALING_HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
pub const SIGNALING_MAX_MESSAGE_BYTES: usize = 256 * 1024;
fn message_too_big_close() -> Message {
Message::Close(Some(tokio_tungstenite::tungstenite::protocol::CloseFrame {
code: tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Size,
reason: "message too big".into(),
}))
}
pub const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
const SIGNALING_MAX_PEERS: usize = 1024;
const MAX_PEER_ID_BYTES: usize = 128;
const MAX_CONNECTIONS: usize = SIGNALING_MAX_PEERS;
const CONNECTION_CAP_BODY: &str = "Too many signaling connections, try again later.\n";
static CONNECTION_CAP_REFUSAL: std::sync::LazyLock<Vec<u8>> = std::sync::LazyLock::new(|| {
format!(
"HTTP/1.1 503 Service Unavailable\r\nRetry-After: 30\r\nContent-Type: \
text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
CONNECTION_CAP_BODY.len(),
CONNECTION_CAP_BODY
)
.into_bytes()
});
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
pub enum SignalingMessage {
#[serde(rename = "register")]
Register { peer_id: String },
#[serde(rename = "offer")]
Offer {
from: String,
to: String,
sdp: serde_json::Value,
},
#[serde(rename = "answer")]
Answer {
from: String,
to: String,
sdp: serde_json::Value,
},
#[serde(rename = "ice_candidate")]
IceCandidate {
from: String,
to: String,
candidate: serde_json::Value,
},
#[serde(rename = "error")]
Error { message: String },
#[serde(rename = "registered")]
Registered { peer_id: String },
#[serde(rename = "relay")]
Relay {
from: String,
to: String,
data: serde_json::Value,
},
}
struct PeerConnection {
#[allow(dead_code)]
peer_id: PeerId,
out_tx: mpsc::UnboundedSender<Message>,
#[allow(dead_code)]
remote_addr: SocketAddr,
#[allow(dead_code)]
connection_id: ConnectionId,
}
pub struct WebRtcSignalingServerData {
peers: Arc<Mutex<HashMap<PeerId, PeerConnection>>>,
}
impl Default for WebRtcSignalingServerData {
fn default() -> Self {
Self::new()
}
}
impl WebRtcSignalingServerData {
pub fn new() -> Self {
Self {
peers: Arc::new(Mutex::new(HashMap::new())),
}
}
async fn register_peer(
&self,
peer_id: PeerId,
out_tx: mpsc::UnboundedSender<Message>,
remote_addr: SocketAddr,
connection_id: ConnectionId,
) -> Result<()> {
if peer_id.is_empty() || peer_id.len() > MAX_PEER_ID_BYTES {
anyhow::bail!(
"Peer ID must be 1-{} bytes, got {}",
MAX_PEER_ID_BYTES,
peer_id.len()
);
}
let mut peers = self.peers.lock().await;
if peers.len() >= SIGNALING_MAX_PEERS {
anyhow::bail!(
"Signaling server is full ({} peers registered)",
SIGNALING_MAX_PEERS
);
}
if peers.contains_key(&peer_id) {
anyhow::bail!("Peer ID {} already registered", peer_id);
}
peers.insert(
peer_id.clone(),
PeerConnection {
peer_id: peer_id.clone(),
out_tx,
remote_addr,
connection_id,
},
);
info!("Registered signaling peer: {}", peer_id);
Ok(())
}
pub async fn unregister_peer(&self, peer_id: &str) {
let mut peers = self.peers.lock().await;
peers.remove(peer_id);
info!("Unregistered signaling peer: {}", peer_id);
}
pub async fn forward_message(&self, to: &str, message: &SignalingMessage) -> Result<()> {
let msg_json = serde_json::to_string(message)?;
let peers = self.peers.lock().await;
let peer_conn = peers.get(to).context(format!("Peer {} not found", to))?;
peer_conn
.out_tx
.send(Message::Text(msg_json))
.context("Peer's writer task has stopped")?;
drop(peers);
trace!("Forwarded message to peer {}: {:?}", to, message);
Ok(())
}
pub async fn send_to_peer(&self, peer_id: &str, text: String) -> Result<()> {
let peers = self.peers.lock().await;
let peer_conn = peers
.get(peer_id)
.context(format!("Peer {} not found", peer_id))?;
peer_conn
.out_tx
.send(Message::Text(text))
.context("Peer's writer task has stopped")?;
Ok(())
}
pub async fn list_peers(&self) -> Vec<String> {
self.peers.lock().await.keys().cloned().collect()
}
pub async fn peer_count(&self) -> usize {
self.peers.lock().await.len()
}
}
pub struct WebRtcSignalingServer;
impl WebRtcSignalingServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: ServerId,
idle_timeout: std::time::Duration,
) -> Result<SocketAddr> {
let listener = TcpListener::bind(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"WebRTC Signaling server listening on {}",
local_addr
));
let server_data = Arc::new(WebRtcSignalingServerData::new());
let protocol = Arc::new(WebRtcSignalingProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let cap_status_tx = status_tx.clone();
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
&CONNECTION_CAP_REFUSAL,
"WEBRTC_SIGNALING",
Some(&cap_status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
info!("Signaling server accepted connection from {}", remote_addr);
let server_data_clone = Arc::clone(&server_data);
let app_state_clone = Arc::clone(&app_state);
let status_tx_clone = status_tx.clone();
let llm_client_clone = llm_client.clone();
let protocol_clone = Arc::clone(&protocol);
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
remote_addr,
server_data_clone,
app_state_clone,
status_tx_clone,
llm_client_clone,
server_id,
protocol_clone,
idle_timeout,
)
.await
{
error!(
"Error handling signaling connection from {}: {}",
remote_addr, e
);
}
})
.await;
}
Err(e) => {
error!("Error accepting signaling connection: {}", e);
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection(
stream: TcpStream,
remote_addr: SocketAddr,
server_data: Arc<WebRtcSignalingServerData>,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
llm_client: OllamaClient,
server_id: ServerId,
protocol: Arc<WebRtcSignalingProtocol>,
idle_timeout: std::time::Duration,
) -> Result<()> {
let config = WebSocketConfig {
max_message_size: Some(SIGNALING_MAX_MESSAGE_BYTES),
max_frame_size: Some(SIGNALING_MAX_MESSAGE_BYTES),
..Default::default()
};
let ws_stream = tokio::time::timeout(
SIGNALING_HANDSHAKE_TIMEOUT,
accept_async_with_config(stream, Some(config)),
)
.await
.map_err(|_| {
anyhow::anyhow!(
"WebSocket handshake from {} did not complete within {:?}",
remote_addr,
SIGNALING_HANDSHAKE_TIMEOUT
)
})??;
info!("WebSocket connection established with {}", remote_addr);
let (mut ws_tx, mut ws_rx) = ws_stream.split();
let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Message>();
let writer = tokio::spawn(async move {
while let Some(msg) = out_rx.recv().await {
if let Err(e) = ws_tx.send(msg).await {
debug!("Signaling writer stopped: {}", e);
break;
}
}
let _ = ws_tx.close().await;
});
let mut peer_id: Option<PeerId> = None;
let mut connection_id: Option<ConnectionId> = None;
let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
loop {
let keepalive_tx = out_tx.clone();
let msg_result = tokio::select! {
msg = ws_rx.next() => msg,
_ = crate::server::accept_bounded::watch_idle_with_probe(
Arc::clone(&activity),
idle_timeout,
move || {
let _ = keepalive_tx.send(Message::Ping(
crate::server::accept_bounded::KEEPALIVE_PING_PAYLOAD.to_vec(),
));
},
) => {
info!(
"WebRTC signaling peer {} sent no frame for {}s, not even a Pong; \
closing decision=idle_timeout",
remote_addr,
idle_timeout.as_secs()
);
let _ = out_tx.send(Message::Close(Some(
tokio_tungstenite::tungstenite::protocol::CloseFrame {
code: tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Away,
reason: "idle timeout".into(),
},
)));
break;
}
};
let Some(msg_result) = msg_result else { break };
let _busy = activity.busy();
match msg_result {
Ok(Message::Text(text)) => {
trace!("Received signaling message: {}", text);
let message: SignalingMessage = match serde_json::from_str(&text) {
Ok(m) => m,
Err(e) => {
warn!(
"Invalid signaling message from {}: {}",
remote_addr,
crate::utils::truncate_for_log(&e.to_string(), 300)
);
let _ = Self::reply(
&out_tx,
&SignalingMessage::Error {
message: "invalid signaling message: expected a JSON \
object with a 'type' of register, offer, \
answer, ice_candidate or relay"
.to_string(),
},
);
continue;
}
};
match message {
SignalingMessage::Register {
peer_id: new_peer_id,
} => {
if peer_id.is_some() {
let _ = Self::reply(
&out_tx,
&SignalingMessage::Error {
message: "This connection is already registered"
.to_string(),
},
);
continue;
}
let conn_id = ConnectionId::new(app_state.get_next_unified_id().await);
match server_data
.register_peer(
new_peer_id.clone(),
out_tx.clone(),
remote_addr,
conn_id,
)
.await
{
Ok(_) => {
peer_id = Some(new_peer_id.clone());
connection_id = Some(conn_id);
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: conn_id,
remote_addr,
local_addr: "0.0.0.0:0".parse().unwrap(),
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(
serde_json::json!({
"peer_id": new_peer_id,
}),
),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let _ = Self::reply(
&out_tx,
&SignalingMessage::Registered {
peer_id: new_peer_id.clone(),
},
);
Log::new(Some(&status_tx)).info(format!(
"WebRTC signaling peer '{}' registered from {}",
new_peer_id, remote_addr
));
let event = Event::new(
&WEBRTC_SIGNALING_PEER_CONNECTED_EVENT,
serde_json::json!({
"peer_id": new_peer_id,
"remote_addr": remote_addr.to_string(),
"peer_count": server_data.peer_count().await,
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(conn_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(result) => {
let subject = format!(
"WebRTC signaling peer '{}' \
webrtc_signaling_peer_connected",
new_peer_id
);
if Self::apply_results(
result, &subject, &out_tx, &status_tx,
) {
break;
}
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!(
"WebRTC signaling peer '{}' \
webrtc_signaling_peer_connected \
decision=llm_error_peer_admitted \
category={}: LLM call failed ({}) - sent \
error frame, registration stands",
new_peer_id,
Self::failure_category(&e),
e
));
let _ = Self::reply(
&out_tx,
&SignalingMessage::Error {
message: format!(
"Server-side handler for peer '{}' \
failed; registration stands but no \
handler response follows",
new_peer_id
),
},
);
}
}
}
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"WebRTC signaling refused registration of '{}' from {} \
decision=protocol_error: {}",
new_peer_id, remote_addr, e
));
let _ = Self::reply(
&out_tx,
&SignalingMessage::Error {
message: e.to_string(),
},
);
}
}
}
SignalingMessage::Offer { .. }
| SignalingMessage::Answer { .. }
| SignalingMessage::IceCandidate { .. }
| SignalingMessage::Relay { .. } => {
let (kind, claimed_from, to) = match &message {
SignalingMessage::Offer { from, to, .. } => {
("offer", from.clone(), to.clone())
}
SignalingMessage::Answer { from, to, .. } => {
("answer", from.clone(), to.clone())
}
SignalingMessage::IceCandidate { from, to, .. } => {
("ice_candidate", from.clone(), to.clone())
}
SignalingMessage::Relay { from, to, .. } => {
("relay", from.clone(), to.clone())
}
_ => unreachable!(),
};
let Some(sender_id) = peer_id.clone() else {
let _ = Self::reply(
&out_tx,
&SignalingMessage::Error {
message: "register before sending offer, answer, \
ice_candidate or relay"
.to_string(),
},
);
Log::new(Some(&status_tx)).warn(format!(
"WebRTC signaling refused {} from unregistered {}",
kind, remote_addr
));
continue;
};
if claimed_from != sender_id {
Log::new(Some(&status_tx)).warn(format!(
"WebRTC signaling rewrote forged 'from' on a {}: peer '{}' \
claimed to be '{}'",
kind, sender_id, claimed_from
));
}
let from = sender_id;
let message = Self::with_sender(message, &from);
let delivered = match server_data.forward_message(&to, &message).await {
Ok(()) => true,
Err(e) => {
warn!(
"Failed to forward {} from {} to {}: {}",
kind, from, to, e
);
let _ = Self::reply(
&out_tx,
&SignalingMessage::Error {
message: format!(
"Cannot deliver {} to {}: {}",
kind, to, e
),
},
);
false
}
};
Log::new(Some(&status_tx)).debug(format!(
"WebRTC signaling {} {} -> {} ({})",
kind,
from,
to,
if delivered {
"delivered"
} else {
"undeliverable"
}
));
let event = Event::new(
&WEBRTC_SIGNALING_MESSAGE_RECEIVED_EVENT,
serde_json::json!({
"peer_id": from,
"message_type": kind,
"target_peer": to,
"delivered": delivered,
}),
);
let llm = llm_client.clone();
let state = app_state.clone();
let proto = protocol.clone();
let status = status_tx.clone();
let observed = format!("{} {} -> {}", kind, from, to);
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
if let Err(e) = call_llm(
&llm,
&state,
server_id,
None,
&event,
proto.as_ref(),
)
.await
{
Log::new(Some(&status)).error(format!(
"WebRTC signaling {} webrtc_signaling_message_received \
decision=llm_error_notice_only: LLM call failed ({}) - \
message was already relayed, no frame sent",
observed, e
));
}
})
.await;
}
other => {
debug!("Ignoring signaling message: {:?}", other);
}
}
}
Ok(Message::Close(_)) => {
info!("Signaling connection closed by peer");
break;
}
Ok(_) => {
}
Err(tokio_tungstenite::tungstenite::Error::Capacity(e)) => {
warn!(
"Signaling message from {} over {} bytes \
decision=fail_closed_message_too_large: {}",
remote_addr, SIGNALING_MAX_MESSAGE_BYTES, e
);
let _ = out_tx.send(message_too_big_close());
break;
}
Err(e) => {
warn!("WebSocket error: {}", e);
break;
}
}
}
if let Some(pid) = peer_id {
server_data.unregister_peer(&pid).await;
let event = Event::new(
&WEBRTC_SIGNALING_PEER_DISCONNECTED_EVENT,
serde_json::json!({
"peer_id": pid,
"peer_count": server_data.peer_count().await,
}),
);
if let Err(e) = call_llm(
&llm_client,
&app_state,
server_id,
connection_id,
&event,
protocol.as_ref(),
)
.await
{
Log::new(Some(&status_tx)).error(format!(
"WebRTC signaling peer '{}' webrtc_signaling_peer_disconnected \
decision=llm_error_notice_only: LLM call failed on disconnect ({}) - peer \
already gone, nothing sent",
pid, e
));
}
if let Some(conn_id) = connection_id {
app_state
.remove_connection_from_server(server_id, conn_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
}
drop(out_tx);
let _ = writer.await;
Ok(())
}
fn failure_category(e: &anyhow::Error) -> &'static str {
match crate::utils::wire_failure::WireFailure::classify(e) {
crate::utils::wire_failure::WireFailure::Overloaded => "overloaded",
crate::utils::wire_failure::WireFailure::Unavailable => "unavailable",
}
}
fn reply(out_tx: &mpsc::UnboundedSender<Message>, message: &SignalingMessage) -> Result<()> {
out_tx.send(Message::Text(serde_json::to_string(message)?))?;
Ok(())
}
fn with_sender(message: SignalingMessage, sender: &str) -> SignalingMessage {
match message {
SignalingMessage::Offer { to, sdp, .. } => SignalingMessage::Offer {
from: sender.to_string(),
to,
sdp,
},
SignalingMessage::Answer { to, sdp, .. } => SignalingMessage::Answer {
from: sender.to_string(),
to,
sdp,
},
SignalingMessage::IceCandidate { to, candidate, .. } => {
SignalingMessage::IceCandidate {
from: sender.to_string(),
to,
candidate,
}
}
SignalingMessage::Relay { to, data, .. } => SignalingMessage::Relay {
from: sender.to_string(),
to,
data,
},
other => other,
}
}
fn apply_results(
result: crate::llm::actions::executor::ExecutionResult,
subject: &str,
out_tx: &mpsc::UnboundedSender<Message>,
status_tx: &mpsc::UnboundedSender<String>,
) -> bool {
let log = Log::new(Some(status_tx));
for message in &result.messages {
log.info(format!("{}", message));
}
let mut close = false;
let mut sent = 0usize;
let mut undecodable = 0usize;
for protocol_result in result.protocol_results {
match protocol_result {
crate::llm::ActionResult::Output(bytes) => match String::from_utf8(bytes) {
Ok(text) => {
let _ = out_tx.send(Message::Text(text));
sent += 1;
}
Err(e) => {
undecodable += 1;
error!("Signaling action produced non-UTF-8 output: {}", e);
}
},
crate::llm::ActionResult::CloseConnection => close = true,
_ => {}
}
}
if undecodable > 0 && sent == 0 && !close {
log.error(format!(
"{} decision=fail_closed_bad_action: the answer's output was not valid UTF-8, \
so no signaling frame could be sent",
subject
));
} else if close {
log.info(format!(
"{} decision=model_reject: the model disconnected the peer",
subject
));
} else if sent > 0 {
log.info(format!(
"{} decision=model_answer: {} signaling frame(s) sent",
subject, sent
));
} else if result.raw_actions.is_empty() {
log.warn(format!(
"{} decision=model_silent: no action, so nothing was sent to the peer",
subject
));
} else if !result.failures.is_empty() {
log.error(format!(
"{} decision=fail_closed_bad_action: {}",
subject,
result
.failures
.iter()
.map(|f| format!("{}: {}", f.action, f.error))
.collect::<Vec<_>>()
.join("; ")
));
} else {
log.warn(format!(
"{} decision=model_silent: the answer produced no signaling frame",
subject
));
}
close
}
}