use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use rsipstack::{
dialog::dialog_layer::DialogLayer,
transaction::{
endpoint::{EndpointBuilder, EndpointInnerRef},
TransactionReceiver,
},
transport::{udp::UdpConnection, SipAddr, SipConnection, TransportLayer},
};
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use crate::account::{SipAccount, Transport};
pub struct SipEndpoint {
pub inner: EndpointInnerRef,
pub dialog_layer: Arc<DialogLayer>,
pub sip_addr: SipAddr,
transport_cancel: CancellationToken,
}
impl SipEndpoint {
pub async fn new(
account: &SipAccount,
_cancel: CancellationToken,
) -> Result<(Self, TransactionReceiver), Box<dyn std::error::Error + Send + Sync>> {
let local_ip = detect_local_ip(account)?;
let bind_addr: SocketAddr = SocketAddr::new(local_ip, 0);
info!("Binding SIP transport to {bind_addr}");
let transport_cancel = CancellationToken::new();
let transport_layer = TransportLayer::new(transport_cancel.clone());
match account.transport {
Transport::Udp => {
let udp = UdpConnection::create_connection(
bind_addr,
None,
Some(transport_cancel.clone()),
)
.await?;
transport_layer.add_transport(SipConnection::Udp(udp));
}
Transport::Tcp => {
}
}
let user_agent = build_user_agent(
env!("CARGO_PKG_VERSION"),
crate::GIT_HASH,
&os_version(),
std::env::consts::ARCH,
&hostname::get()
.map(|h| h.to_string_lossy().into_owned())
.unwrap_or_default(),
);
info!("User-Agent: {user_agent}");
let endpoint = EndpointBuilder::new()
.with_user_agent(&user_agent)
.with_transport_layer(transport_layer)
.with_cancel_token(transport_cancel.clone())
.build();
let inner = endpoint.inner.clone();
tokio::spawn({
let inner = inner.clone();
async move {
if let Err(e) = inner.serve().await {
warn!("endpoint serve error: {e}");
}
}
});
let sip_addr = endpoint
.get_addrs()
.into_iter()
.next()
.ok_or("No SIP address bound")?;
let dialog_layer = Arc::new(DialogLayer::new(inner.clone()));
let incoming = endpoint.incoming_transactions()?;
Ok((
Self {
inner,
dialog_layer,
sip_addr,
transport_cancel,
},
incoming,
))
}
pub fn local_ip(&self) -> IpAddr {
self.sip_addr
.addr
.to_string()
.parse::<SocketAddr>()
.map(|a| a.ip())
.unwrap_or(IpAddr::from([127, 0, 0, 1]))
}
pub fn shutdown(&self) {
self.transport_cancel.cancel();
}
}
fn build_user_agent(version: &str, git_hash: &str, os: &str, arch: &str, host: &str) -> String {
format!("wavekat-sip/{version} ({git_hash}) ({os}/{arch}) {host}")
}
fn os_version() -> String {
#[cfg(target_os = "macos")]
{
if let Ok(out) = std::process::Command::new("sw_vers")
.arg("-productVersion")
.output()
{
let ver = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !ver.is_empty() {
return format!("macOS {ver}");
}
}
}
#[cfg(target_os = "linux")]
{
if let Ok(contents) = std::fs::read_to_string("/etc/os-release") {
for line in contents.lines() {
if let Some(name) = line.strip_prefix("PRETTY_NAME=") {
return name.trim_matches('"').to_string();
}
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(out) = std::process::Command::new("cmd")
.args(["/C", "ver"])
.output()
{
let ver = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !ver.is_empty() {
return ver;
}
}
}
std::env::consts::OS.to_string()
}
fn detect_local_ip(
account: &SipAccount,
) -> Result<IpAddr, Box<dyn std::error::Error + Send + Sync>> {
let dest = format!("{}:{}", account.server(), account.port());
let sock = std::net::UdpSocket::bind("0.0.0.0:0")?;
sock.connect(&dest)?;
let local = sock.local_addr()?;
Ok(local.ip())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_account(server: Option<&str>, port: Option<u16>) -> SipAccount {
SipAccount {
display_name: "Test".to_string(),
username: "1001".to_string(),
password: "secret".to_string(),
domain: "localhost".to_string(),
auth_username: None,
server: server.map(|s| s.to_string()),
port,
transport: Transport::default(),
}
}
#[test]
fn build_user_agent_format() {
let ua = build_user_agent("0.0.1", "abc1234", "macOS 15.5", "aarch64", "myhost.local");
assert_eq!(
ua,
"wavekat-sip/0.0.1 (abc1234) (macOS 15.5/aarch64) myhost.local"
);
}
#[test]
fn build_user_agent_empty_host() {
let ua = build_user_agent("1.0.0", "def5678", "Linux", "x86_64", "");
assert_eq!(ua, "wavekat-sip/1.0.0 (def5678) (Linux/x86_64) ");
}
#[test]
fn os_version_returns_non_empty() {
let version = os_version();
assert!(!version.is_empty());
#[cfg(target_os = "macos")]
assert!(version.starts_with("macOS"), "got: {version}");
}
#[test]
fn detect_local_ip_returns_non_unspecified() {
let account = make_account(Some("127.0.0.1"), Some(5060));
let ip = detect_local_ip(&account).unwrap();
assert!(!ip.is_unspecified(), "detected IP should not be 0.0.0.0");
assert_eq!(ip, IpAddr::from([127, 0, 0, 1]));
}
#[test]
fn detect_local_ip_uses_server_field() {
let account = make_account(Some("127.0.0.1"), None);
let ip = detect_local_ip(&account).unwrap();
assert_eq!(ip, IpAddr::from([127, 0, 0, 1]));
}
#[test]
fn detect_local_ip_falls_back_to_domain() {
let account = make_account(None, None);
let ip = detect_local_ip(&account).unwrap();
assert_eq!(ip, IpAddr::from([127, 0, 0, 1]));
}
}