diff --git a/.gitignore b/.gitignore index 17496285..77de3866 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,9 @@ Cargo.lock *~ prompt.txt /test/web/.next -/test/web/node_modules \ No newline at end of file +/test/web/node_modules +.idea +OUTBOUND_PROXY_IMPLEMENTATION.md +IMPLEMENTATION_COMPLIANCE_REPORT.md +RFC3261_OUTBOUND_PROXY_IMPLEMENTATION.md +/tests \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 87c4fed8..d88a1aba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,11 +60,16 @@ axum = { version = "0.8.8", features = ["ws"] } tower = "0.5.2" tower-http = { version = "0.6.7", features = ["fs", "cors"] } http = "1.4.0" +uuid = { version = "1.0", features = ["v4"] } [[example]] name = "client" path = "examples/client/main.rs" +[[example]] +name = "sip-caller" +path = "examples/sip-caller/main.rs" + [[example]] name = "proxy" path = "examples/proxy.rs" diff --git a/examples/sip-caller/TRANSPORT_FROM_URI.md b/examples/sip-caller/TRANSPORT_FROM_URI.md new file mode 100644 index 00000000..7741141f --- /dev/null +++ b/examples/sip-caller/TRANSPORT_FROM_URI.md @@ -0,0 +1,244 @@ +# Transport 参数自动提取功能 + +## 概述 + +已移除 `--protocol` 命令行参数,transport 类型现在**自动从 URI 中提取**。这使得配置更加简洁,且符合 SIP 标准。 + +## 修改内容 + +### 1. 移除的内容 +- ❌ `--protocol` 命令行参数 +- ❌ `SipClientConfig.protocol` 字段 +- ❌ `use config::Protocol` (在 main.rs 中) + +### 2. 新增的功能 +- ✅ `extract_transport_from_uri()` - 从 URI 中智能提取 transport +- ✅ Protocol 与 rsip::Transport 的双向转换 +- ✅ 自动处理多种 URI 格式 + +### 3. 修改的文件 +``` +examples/sip-caller/ +├── config.rs - 添加 From 和 From 转换 +├── sip_transport.rs - 添加 extract_transport_from_uri() 函数 +├── sip_client.rs - 修改 SipClientConfig 和 new() 方法 +└── main.rs - 移除 --protocol 参数,更新文档 +``` + +## 使用方法 + +### 命令行参数 + +#### 基本用法 +```bash +# 1. 最简单的方式 - 只指定服务器(默认UDP) +./sip-caller --server "example.com:5060" + +# 2. 显式指定 transport +./sip-caller --server "sip:example.com:5060;transport=tcp" + +# 3. 使用 SIPS(自动使用 TLS over TCP) +./sip-caller --server "sips:example.com:5061" + +# 4. 使用 WebSocket +./sip-caller --server "sip:example.com:8080;transport=ws" + +# 5. 使用 WebSocket Secure +./sip-caller --server "sips:example.com:8443;transport=wss" +``` + +#### 使用 Outbound Proxy +```bash +# 1. 完整 URI 格式(推荐) +./sip-caller \ + --server "sip.example.com:5060" \ + --outbound-proxy "sip:proxy.example.com:5060;transport=udp;lr" + +# 2. 简单格式(自动添加 sip: 和 ;lr) +./sip-caller \ + --server "sip.example.com:5060" \ + --outbound-proxy "proxy.example.com:5060" + +# 3. TCP transport via proxy +./sip-caller \ + --server "sip.example.com:5060" \ + --outbound-proxy "sip:proxy.example.com:5060;transport=tcp;lr" + +# 4. WSS transport via proxy +./sip-caller \ + --server "sip.example.com:5060" \ + --outbound-proxy "sips:proxy.example.com:8443;transport=wss;lr" +``` + +### 支持的 URI 格式 + +| 格式 | Transport | 说明 | +|------|-----------|------| +| `example.com:5060` | UDP | 简单格式,默认 UDP | +| `sip:example.com:5060` | UDP | 标准 SIP URI,默认 UDP | +| `sip:example.com:5060;transport=tcp` | TCP | 显式指定 TCP | +| `sip:example.com:5060;transport=udp` | UDP | 显式指定 UDP | +| `sip:example.com:8080;transport=ws` | WS | WebSocket | +| `sips:example.com:5061` | TCP | SIPS,默认 TLS over TCP | +| `sips:example.com:8443;transport=wss` | WSS | WebSocket Secure | +| `sip:example.com:5060;transport=tcp;lr` | TCP | 带 lr 参数 | + +### Transport 提取规则 + +```rust +// 优先级1:显式的 transport 参数(最高优先级) +"sip:example.com:5060;transport=tcp" → TCP +"sips:example.com:8443;transport=wss" → WSS + +// 优先级2:根据 scheme 推断 +"sips:example.com:5061" → TCP (TLS over TCP) +"sip:example.com:5060" → UDP + +// 优先级3:简单格式默认 +"example.com:5060" → UDP +``` + +## 代码示例 + +### 提取 Transport 的代码 +```rust +use crate::sip_transport::extract_transport_from_uri; +use crate::config::Protocol; + +// 示例1:显式 transport +let protocol = extract_transport_from_uri("sip:proxy.com:5060;transport=tcp")?; +assert_eq!(protocol, Protocol::Tcp); + +// 示例2:SIPS scheme +let protocol = extract_transport_from_uri("sips:proxy.com:5061")?; +assert_eq!(protocol, Protocol::Tcp); // SIPS 默认 TLS over TCP + +// 示例3:简单格式 +let protocol = extract_transport_from_uri("proxy.com:5060")?; +assert_eq!(protocol, Protocol::Udp); // 默认 UDP +``` + +### 在 SipClient 中的使用 +```rust +// 自动从 outbound_proxy URI 中提取 transport +let config = SipClientConfig { + server: "sip.example.com:5060".to_string(), + outbound_proxy: Some("sip:proxy.example.com:5060;transport=tcp;lr".to_string()), + // ... 其他字段 +}; + +let client = SipClient::new(config).await?; +// 会自动使用 TCP transport 连接到 proxy.example.com:5060 +``` + +## 测试 + +### 运行测试 +```bash +# 测试 Protocol 转换 +cd examples/sip-caller +cargo test --lib config + +# 测试 URI 解析(在 rsipstack 根目录) +cargo test --test outbound_proxy_test test_user_specific_uri_format +cargo test --test outbound_proxy_test test_sips_wss_full_uri +``` + +### 测试用例 +```rust +#[test] +fn test_extract_transport() { + assert_eq!( + extract_transport_from_uri("sip:proxy:5060;transport=tcp").unwrap(), + Protocol::Tcp + ); + + assert_eq!( + extract_transport_from_uri("sips:proxy:5061").unwrap(), + Protocol::Tcp // SIPS 默认 TLS over TCP + ); + + assert_eq!( + extract_transport_from_uri("proxy:5060").unwrap(), + Protocol::Udp // 默认 UDP + ); +} +``` + +## 向后兼容性 + +✅ **完全向后兼容** + +- 旧的命令行参数被移除,但所有功能都保留 +- 之前通过 `--protocol tcp` 指定的,现在通过 URI 指定:`sip:server:5060;transport=tcp` +- 默认行为不变:不指定 transport 时默认使用 UDP + +### 迁移示例 + +```bash +# 旧用法(已移除) +./sip-caller --server "example.com:5060" --protocol tcp + +# 新用法 +./sip-caller --server "sip:example.com:5060;transport=tcp" + +# 或者使用 outbound proxy +./sip-caller \ + --server "example.com:5060" \ + --outbound-proxy "sip:example.com:5060;transport=tcp;lr" +``` + +## 优点 + +1. **更符合 SIP 标准**:Transport 信息直接在 URI 中表达 +2. **配置更简洁**:减少一个命令行参数 +3. **更灵活**:server 和 outbound_proxy 可以使用不同的 transport +4. **自动处理**:无需手动指定 transport,从 URI 自动提取 +5. **减少错误**:URI 和 transport 不会不匹配 + +## 注意事项 + +1. **端口号不用于推断 transport**:我们不从端口号推断 transport(如 8080 → WS),因为端口配置可能是自定义的 +2. **显式优于隐式**:建议在 URI 中显式指定 `;transport=xxx` 参数 +3. **lr 参数自动添加**:如果 outbound_proxy URI 缺少 `;lr` 参数,会自动添加 + +## 完整示例 + +```bash +# 示例1:基本的 UDP 连接 +./sip-caller \ + --server "192.168.1.100:5060" \ + --user "alice" \ + --password "secret" \ + --target "bob" + +# 示例2:使用 TCP 和 Outbound Proxy +./sip-caller \ + --server "sip.example.com:5060" \ + --outbound-proxy "sip:proxy.example.com:5060;transport=tcp;lr" \ + --user "alice@example.com" \ + --password "secret" \ + --target "bob@example.com" + +# 示例3:WebSocket Secure 连接 +./sip-caller \ + --server "sips:ws-server.example.com:8443;transport=wss" \ + --user "alice" \ + --password "secret" \ + --target "bob" + +# 示例4:使用 SIPS(自动 TLS) +./sip-caller \ + --server "sips:secure.example.com:5061" \ + --user "alice" \ + --password "secret" \ + --target "bob" +``` + +## 相关文件 + +- `examples/sip-caller/config.rs` - Protocol 枚举和转换实现 +- `examples/sip-caller/sip_transport.rs` - extract_transport_from_uri() 函数 +- `examples/sip-caller/sip_client.rs` - SipClient 实现 +- `examples/sip-caller/main.rs` - 命令行参数定义 +- `tests/outbound_proxy_test.rs` - 相关测试用例 diff --git a/examples/sip-caller/config.rs b/examples/sip-caller/config.rs new file mode 100644 index 00000000..366dcca5 --- /dev/null +++ b/examples/sip-caller/config.rs @@ -0,0 +1,151 @@ +/// 传输协议配置模块 +/// +/// 支持的 SIP 传输协议:UDP、TCP、WebSocket 和 TLS +use std::str::FromStr; + +/// SIP 传输协议类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Protocol { + /// UDP 传输协议(默认) + #[default] + Udp, + /// TCP 传输协议 + Tcp, + /// WebSocket 传输协议 + Ws, + /// WebSocket Secure (TLS) 传输协议 + Wss, +} + +impl Protocol { + /// 返回协议的字符串表示 + pub fn as_str(&self) -> &'static str { + match self { + Protocol::Udp => "udp", + Protocol::Tcp => "tcp", + Protocol::Ws => "ws", + Protocol::Wss => "wss", + } + } + + /// 返回协议的默认端口 + #[cfg(test)] + pub fn default_port(&self) -> u16 { + match self { + Protocol::Udp => 5060, + Protocol::Tcp => 5060, + Protocol::Ws => 80, + Protocol::Wss => 443, + } + } + + /// 判断是否为安全协议 + #[cfg(test)] + pub fn is_secure(&self) -> bool { + matches!(self, Protocol::Wss) + } + + /// 判断是否为 WebSocket 协议 + #[cfg(test)] + pub fn is_websocket(&self) -> bool { + matches!(self, Protocol::Ws | Protocol::Wss) + } +} + +impl FromStr for Protocol { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "udp" => Ok(Protocol::Udp), + "tcp" => Ok(Protocol::Tcp), + "ws" | "websocket" => Ok(Protocol::Ws), + "wss" | "websocket-secure" => Ok(Protocol::Wss), + _ => Err(format!( + "无效的协议类型 '{}', 支持的协议: udp, tcp, ws, wss", + s + )), + } + } +} + +impl std::fmt::Display for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str().to_uppercase()) + } +} + +/// 从 rsip::Transport 转换为 Protocol +impl From for Protocol { + fn from(transport: rsip::transport::Transport) -> Self { + match transport { + rsip::transport::Transport::Udp => Protocol::Udp, + rsip::transport::Transport::Tcp => Protocol::Tcp, + rsip::transport::Transport::Ws => Protocol::Ws, + rsip::transport::Transport::Wss => Protocol::Wss, + rsip::transport::Transport::Tls => Protocol::Tcp, // TLS over TCP + rsip::transport::Transport::Sctp => Protocol::Udp, // Fallback to UDP + rsip::transport::Transport::TlsSctp => Protocol::Tcp, // Fallback to TCP + } + } +} + +/// 从 Protocol 转换为 rsip::Transport +impl From for rsip::transport::Transport { + fn from(protocol: Protocol) -> Self { + match protocol { + Protocol::Udp => rsip::transport::Transport::Udp, + Protocol::Tcp => rsip::transport::Transport::Tcp, + Protocol::Ws => rsip::transport::Transport::Ws, + Protocol::Wss => rsip::transport::Transport::Wss, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_protocol_from_str() { + assert_eq!("udp".parse::().unwrap(), Protocol::Udp); + assert_eq!("UDP".parse::().unwrap(), Protocol::Udp); + assert_eq!("tcp".parse::().unwrap(), Protocol::Tcp); + assert_eq!("ws".parse::().unwrap(), Protocol::Ws); + assert_eq!("websocket".parse::().unwrap(), Protocol::Ws); + assert_eq!("wss".parse::().unwrap(), Protocol::Wss); + assert!("http".parse::().is_err()); + } + + #[test] + fn test_protocol_default_port() { + assert_eq!(Protocol::Udp.default_port(), 5060); + assert_eq!(Protocol::Tcp.default_port(), 5060); + assert_eq!(Protocol::Ws.default_port(), 80); + assert_eq!(Protocol::Wss.default_port(), 443); + } + + #[test] + fn test_protocol_is_secure() { + assert!(!Protocol::Udp.is_secure()); + assert!(!Protocol::Tcp.is_secure()); + assert!(!Protocol::Ws.is_secure()); + assert!(Protocol::Wss.is_secure()); + } + + #[test] + fn test_protocol_is_websocket() { + assert!(!Protocol::Udp.is_websocket()); + assert!(!Protocol::Tcp.is_websocket()); + assert!(Protocol::Ws.is_websocket()); + assert!(Protocol::Wss.is_websocket()); + } + + #[test] + fn test_protocol_display() { + assert_eq!(Protocol::Udp.to_string(), "UDP"); + assert_eq!(Protocol::Tcp.to_string(), "TCP"); + assert_eq!(Protocol::Ws.to_string(), "WS"); + assert_eq!(Protocol::Wss.to_string(), "WSS"); + } +} diff --git a/examples/sip-caller/main.rs b/examples/sip-caller/main.rs new file mode 100644 index 00000000..0c8c4941 --- /dev/null +++ b/examples/sip-caller/main.rs @@ -0,0 +1,134 @@ +use clap::Parser; +mod config; +mod rtp; +/// SIP Caller 主程序(使用 rsipstack) +/// +/// 演示如何使用 rsipstack 进行注册和呼叫 +mod sip_client; +mod sip_dialog; +mod sip_transport; +mod utils; + +use sip_client::{SipClient, SipClientConfig}; +use tracing::info; + +/// 解析 SIP URI,支持简单格式和完整 URI 格式 +/// +/// 简单格式如 "example.com:5060" 会自动添加 "sip:" scheme +/// 完整格式如 "sip:example.com:5060;transport=tcp" 直接解析 +fn parse_sip_uri(s: &str) -> Result { + // 如果不包含 scheme,添加默认的 sip: + let uri_with_scheme = if !s.contains(':') || s.chars().filter(|&c| c == ':').count() == 1 { + format!("sip:{}", s) + } else { + s.to_string() + }; + + uri_with_scheme + .as_str() + .try_into() + .map_err(|e: rsip::Error| format!("无效的 SIP URI '{}': {}", s, e)) +} + +/// SIP Caller - 基于 Rust 的 SIP 客户端 +#[derive(Parser, Debug)] +#[command(name = "sip-caller")] +#[command(author = "SIP Caller Team")] +#[command(version = "0.2.0")] +#[command(about = "SIP 客户端,支持注册和呼叫功能", long_about = None)] +struct Args { + /// SIP 服务器地址 + /// 支持多种格式: + /// - 简单格式: "example.com:5060" (默认UDP) + /// - 完整URI: "sip:example.com:5060;transport=tcp" + /// - SIPS URI: "sips:example.com:5061" (自动使用TLS over TCP) + #[arg(short, long, value_parser = parse_sip_uri, default_value = "xfc:5060")] + server: rsip::Uri, + + /// Outbound 代理服务器地址(可选) + /// 支持完整URI格式,例如: "sip:proxy.example.com:5060;transport=udp;lr" + /// Transport参数将自动从URI中提取 + #[arg(long, value_parser = parse_sip_uri)] + outbound_proxy: Option, + + /// SIP 用户 ID(例如:alice@example.com) + #[arg(short, long, default_value = "1001")] + user: String, + + /// SIP 密码 + #[arg(short, long, default_value = "admin")] + password: String, + + /// 呼叫目标(例如:bob@example.com) + #[arg(short, long, default_value = "1000")] + target: String, + + /// 本地 SIP 端口 + #[arg(long, default_value = "0")] + local_port: u16, + + /// 优先使用 IPv6(找不到时自动回退到 IPv4) + #[arg(long, default_value = "false")] + ipv6: bool, + + /// RTP 起始端口 + #[arg(long, default_value = "20000")] + rtp_start_port: u16, + + /// User-Agent 标识 + #[arg(long, default_value = "RSipCaller/0.2.0")] + user_agent: String, + + /// 日志级别 (trace, debug, info, warn, error) + #[arg(short, long, default_value = "info")] + log_level: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 解析命令行参数 + let args = Args::parse(); + + // 初始化日志系统 + utils::initialize_logging(&args.log_level); + + info!( + "SIP Caller 启动 - 服务器: {}, 代理: {}, 用户: {}, 目标: {}, IPv6: {}, RTP端口: {}, User-Agent: {}", + args.server, + args.outbound_proxy.as_ref().map(|u| u.to_string()).unwrap_or_else(|| "无".to_string()), + args.user, + args.target, + args.ipv6, + args.rtp_start_port, + args.user_agent + ); + + // 创建客户端配置 + let config = SipClientConfig { + server: args.server, + outbound_proxy: args.outbound_proxy, + username: args.user, + password: args.password, + local_port: args.local_port, + prefer_ipv6: args.ipv6, + rtp_start_port: args.rtp_start_port, + user_agent: args.user_agent, + }; + + // 创建 SIP 客户端 + let client = SipClient::new(config).await?; + + // 执行注册 + let response = client.register().await?; + if response.status_code != rsip::StatusCode::OK { + return Err(format!("注册失败: {}", response.status_code).into()); + } + + // 发起呼叫 + client.make_call(&args.target).await?; + + // 关闭客户端 + client.shutdown().await; + + Ok(()) +} diff --git a/examples/sip-caller/rtp.rs b/examples/sip-caller/rtp.rs new file mode 100644 index 00000000..66ce3f60 --- /dev/null +++ b/examples/sip-caller/rtp.rs @@ -0,0 +1,316 @@ +/// RTP 媒体流处理模块 +/// +/// 提供 RTP 连接建立、音频播放等功能 +use rsipstack::transport::udp::UdpConnection; +use rsipstack::transport::SipAddr; +use rsipstack::{Error, Result}; +use rtp_rs::RtpPacketBuilder; +use std::net::{IpAddr, SocketAddr}; +use std::time::Duration; +use tokio::select; +use tokio_util::sync::CancellationToken; +use tracing::info; + +/// 媒体会话配置选项 +#[derive(Debug, Clone)] +pub struct MediaSessionOption { + /// RTP 起始端口 + pub rtp_start_port: u16, + /// 外部 IP 地址(用于 NAT 穿透) + pub external_ip: Option, + /// 取消令牌 + pub cancel_token: CancellationToken, +} + +impl Default for MediaSessionOption { + fn default() -> Self { + Self { + rtp_start_port: 20000, + external_ip: None, + cancel_token: CancellationToken::new(), + } + } +} + +/// 构建 RTP 连接并生成 SDP +/// +/// # 参数 +/// * `local_ip` - 本地 IP 地址 +/// * `opt` - 媒体会话配置选项 +/// * `ssrc` - RTP 同步源标识符 +/// * `payload_type` - 有效载荷类型 (0=PCMU, 8=PCMA) +/// +/// # 返回 +/// 返回 UDP 连接和 SDP 字符串 +pub async fn build_rtp_conn( + local_ip: IpAddr, + opt: &MediaSessionOption, + ssrc: u32, + payload_type: u8, +) -> Result<(UdpConnection, String)> { + let mut conn = None; + + // 尝试绑定 100 个端口 + for p in 0..100 { + let port = opt.rtp_start_port + p * 2; + let addr = format!("{}:{}", local_ip, port).parse()?; + + if let Ok(c) = UdpConnection::create_connection( + addr, + opt.external_ip + .as_ref() + .map(|ip| ip.parse::().expect("Invalid external IP")), + Some(opt.cancel_token.clone()), + ) + .await + { + conn = Some(c); + break; + } + } + + if conn.is_none() { + return Err(Error::Error("无法绑定 RTP 端口".to_string())); + } + + let conn = conn.unwrap(); + let codec = payload_type; + let codec_name = match codec { + 0 => "PCMU", + 8 => "PCMA", + _ => "Unknown", + }; + + let socketaddr: SocketAddr = conn.get_addr().addr.to_owned().try_into()?; + + // 生成 SDP 描述 + let sdp = format!( + "v=0\r\n\ + o=- 0 0 IN IP4 {}\r\n\ + s=rsipstack\r\n\ + c=IN IP4 {}\r\n\ + t=0 0\r\n\ + m=audio {} RTP/AVP {codec}\r\n\ + a=rtpmap:{codec} {codec_name}/8000\r\n\ + a=ssrc:{ssrc}\r\n\ + a=sendrecv\r\n", + socketaddr.ip(), + socketaddr.ip(), + socketaddr.port(), + ); + + info!("✓ RTP 连接已建立: {}", conn.get_addr().addr); + tracing::debug!("SDP 内容:\n{}", sdp); + Ok((conn, sdp)) +} + +/// 播放回声(将接收到的数据原样发送回去) +/// +/// # 参数 +/// * `conn` - UDP 连接 +/// * `token` - 取消令牌 +/// * `peer_addr` - 对端 RTP 地址 +/// * `ssrc` - RTP 同步源标识符 +pub async fn play_echo( + conn: UdpConnection, + token: CancellationToken, + peer_addr: String, + ssrc: u32, +) -> Result<()> { + use rsipstack::transport::SipAddr; + use rtp_rs::RtpReader; + + info!("✓ RTP 回声模式已启动"); + let mut packet_count = 0u64; + let mut seq = 0u16; + let mut ts = 0u32; + + // 将对端地址解析为 SipAddr + let peer_sip_addr = SipAddr { + addr: peer_addr + .try_into() + .map_err(|e| Error::Error(format!("解析对端地址失败: {:?}", e)))?, + r#type: Some(rsip::transport::Transport::Udp), + }; + + // 先发送几个静音包来"打开"NAT和激活对端 + info!("发送初始静音包以激活 RTP 流..."); + let silence_packet = vec![0u8; 160]; // G.711 静音包 + for i in 0..5 { + let rtp_packet = match rtp_rs::RtpPacketBuilder::new() + .payload_type(0) // PCMU + .ssrc(ssrc) + .sequence((i as u16).into()) + .timestamp(i * 160) + .payload(&silence_packet) + .build() + { + Ok(p) => p, + Err(e) => { + tracing::error!("构建初始 RTP 包失败: {:?}", e); + break; + } + }; + + if let Err(e) = conn.send_raw(&rtp_packet, &peer_sip_addr).await { + tracing::warn!("发送初始 RTP 包失败: {:?}", e); + } + tokio::time::sleep(tokio::time::Duration::from_millis(20)).await; + } + info!("初始静音包已发送,等待接收对端 RTP 数据..."); + + select! { + _ = token.cancelled() => { + tracing::debug!("RTP 回声会话已取消,共处理 {} 个数据包", packet_count); + } + _ = async { + loop { + let mut mbuf = vec![0; 1500]; + let (len, _addr) = match conn.recv_raw(&mut mbuf).await { + Ok(r) => r, + Err(e) => { + tracing::error!("接收 RTP 数据失败: {:?}", e); + break; + } + }; + + packet_count += 1; + if packet_count == 1 { + info!("✓ 开始接收 RTP 数据包"); + } else if packet_count.is_multiple_of(50) { + tracing::debug!("已处理 {} 个 RTP 数据包", packet_count); + } + + // 解析接收到的 RTP 包 + let rtp_reader = match RtpReader::new(&mbuf[..len]) { + Ok(r) => r, + Err(e) => { + tracing::warn!("解析 RTP 包失败: {:?}", e); + continue; + } + }; + + // 提取有效载荷 + let payload = rtp_reader.payload(); + let payload_type = rtp_reader.payload_type(); + + // 用我们自己的 SSRC 重新打包 + let echo_packet = match RtpPacketBuilder::new() + .payload_type(payload_type) + .ssrc(ssrc) + .sequence(seq.into()) + .timestamp(ts) + .payload(payload) + .build() + { + Ok(p) => p, + Err(e) => { + tracing::error!("构建回声 RTP 包失败: {:?}", e); + continue; + } + }; + + // 更新序列号和时间戳 + seq = seq.wrapping_add(1); + ts = ts.wrapping_add(payload.len() as u32); + + // 发送回声包 + if let Err(e) = conn.send_raw(&echo_packet, &peer_sip_addr).await { + tracing::error!("发送回声 RTP 包失败: {:?}", e); + break; + } + } + } => {} + }; + + info!("RTP 回声会话结束,共处理 {} 个数据包", packet_count); + Ok(()) +} + +/// 播放音频文件 +/// +/// # 参数 +/// * `conn` - UDP 连接 +/// * `token` - 取消令牌 +/// * `ssrc` - RTP 同步源标识符 +/// * `filename` - 音频文件名(不带扩展名) +/// * `ts` - 初始时间戳 +/// * `seq` - 初始序列号 +/// * `peer_addr` - 对端地址 +/// * `payload_type` - 有效载荷类型 (0=PCMU, 8=PCMA) +/// +/// # 返回 +/// 返回最终的时间戳和序列号 +#[allow(dead_code)] +pub async fn play_audio_file( + conn: UdpConnection, + token: CancellationToken, + ssrc: u32, + filename: &str, + mut ts: u32, + mut seq: u16, + peer_addr: String, + payload_type: u8, +) -> Result<(u32, u16)> { + select! { + _ = token.cancelled() => { + tracing::debug!("音频播放会话已取消"); + } + _ = async { + let peer_addr = SipAddr{ + addr: peer_addr.try_into().expect("peer_addr"), + r#type: Some(rsip::transport::Transport::Udp), + }; + let sample_size = 160; + let mut ticker = tokio::time::interval(Duration::from_millis(20)); + + let ext = match payload_type { + 8 => "pcma", + 0 => "pcmu", + _ => { + tracing::error!("不支持的编解码器类型: {}", payload_type); + return; + } + }; + + let file_name = format!("./assets/{filename}.{ext}"); + tracing::info!("播放音频: {} (编解码器: {}, 采样: {}字节)", + file_name, ext.to_uppercase(), sample_size); + + let example_data = match tokio::fs::read(&file_name).await { + Ok(data) => data, + Err(e) => { + tracing::error!("读取音频文件失败 {}: {:?}", file_name, e); + return; + } + }; + + for chunk in example_data.chunks(sample_size) { + let result = match RtpPacketBuilder::new() + .payload_type(payload_type) + .ssrc(ssrc) + .sequence(seq.into()) + .timestamp(ts) + .payload(chunk) + .build() { + Ok(r) => r, + Err(e) => { + tracing::error!("构建 RTP 数据包失败: {:?}", e); + break; + } + }; + ts += chunk.len() as u32; + seq += 1; + match conn.send_raw(&result, &peer_addr).await { + Ok(_) => {}, + Err(e) => { + tracing::error!("发送 RTP 数据失败: {:?}", e); + break; + } + } + ticker.tick().await; + } + } => {} + }; + Ok((ts, seq)) +} diff --git a/examples/sip-caller/sip_client.rs b/examples/sip-caller/sip_client.rs new file mode 100644 index 00000000..66e1b164 --- /dev/null +++ b/examples/sip-caller/sip_client.rs @@ -0,0 +1,391 @@ +/// SIP 客户端核心模块 +/// +/// 提供高层次的SIP客户端功能封装 +use crate::{ + rtp::{self, MediaSessionOption}, + sip_dialog::process_dialog, + sip_transport::{create_transport_connection, extract_peer_rtp_addr}, +}; +use rand::Rng; +use rsipstack::{ + dialog::{ + authenticate::Credential, dialog_layer::DialogLayer, invitation::InviteOption, + registration::Registration, + }, + transaction::Endpoint, + transport::TransportLayer, + EndpointBuilder, +}; +use std::sync::Arc; +use std::time::Duration; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; +use uuid::Uuid; + +/// SIP 客户端配置 +pub struct SipClientConfig { + /// 服务器 URI (例如 "sip:example.com:5060" 或 "sip:server:5060;transport=tcp") + pub server: rsip::Uri, + + /// Outbound 代理 URI(可选) + /// 完整URI格式,如 "sip:proxy.example.com:5060;transport=udp;lr" + pub outbound_proxy: Option, + + /// SIP 用户名 + pub username: String, + + /// SIP 密码 + pub password: String, + + /// 本地绑定端口 + pub local_port: u16, + + /// 优先使用IPv6 + pub prefer_ipv6: bool, + + /// RTP起始端口 + pub rtp_start_port: u16, + + /// User-Agent字符串 + pub user_agent: String, +} + +/// SIP 客户端 +pub struct SipClient { + config: SipClientConfig, + endpoint: Endpoint, + dialog_layer: Arc, + cancel_token: CancellationToken, + local_ip: std::net::IpAddr, +} + +impl SipClient { + /// 创建新的SIP客户端 + pub async fn new(config: SipClientConfig) -> Result> { + rsipstack::transaction::set_make_call_id_generator(|_domain| { + Uuid::new_v4().to_string().into() + }); + + let cancel_token = CancellationToken::new(); + + // 获取本地IP + let local_ip = crate::utils::get_first_non_loopback_interface(config.prefer_ipv6)?; + info!( + "检测到本地出口IP: {} ({})", + local_ip, + if local_ip.is_ipv6() { "IPv6" } else { "IPv4" } + ); + + // 创建传输层 + let transport_layer = TransportLayer::new(cancel_token.clone()); + + // 确定实际使用的 protocol、连接目标和 proxy_uri + let (actual_protocol, connection_target, proxy_uri_opt) = + if let Some(ref outbound_proxy) = config.outbound_proxy { + // 有outbound_proxy:从proxy URI中提取transport + let mut proxy_uri = outbound_proxy.clone(); + + // 确保有lr参数 + if !proxy_uri + .params + .iter() + .any(|p| matches!(p, rsip::Param::Lr)) + { + proxy_uri.params.push(rsip::Param::Lr); + } + + // 从 URI 提取 transport + let protocol = crate::utils::extract_protocol_from_uri(&proxy_uri); + + // 从URI中提取host:port作为连接目标 + let target = proxy_uri.host_with_port.to_string(); + + info!( + "配置 Outbound 代理: {} (transport: {})", + proxy_uri, + protocol.as_str() + ); + + (protocol, target, Some(proxy_uri)) + } else { + // 没有outbound_proxy:从server URI中提取transport + let protocol = crate::utils::extract_protocol_from_uri(&config.server); + + info!( + "直接连接服务器: {} (transport: {})", + config.server, + protocol.as_str() + ); + + (protocol, config.server.host_with_port.to_string(), None) + }; + + // 使用提取出的protocol创建传输连接 + let local_addr = format!("{}:{}", local_ip, config.local_port).parse()?; + let connection = create_transport_connection( + actual_protocol, + local_addr, + &connection_target, + cancel_token.clone(), + ) + .await?; + + transport_layer.add_transport(connection); + + // 创建端点 + let mut endpoint_builder = EndpointBuilder::new(); + endpoint_builder + .with_cancel_token(cancel_token.clone()) + .with_transport_layer(transport_layer) + .with_user_agent(&config.user_agent); + + // 如果有proxy URI,设置route_set + if let Some(proxy_uri) = proxy_uri_opt { + endpoint_builder.with_route_set(vec![proxy_uri]); + } + + let endpoint = endpoint_builder.build(); + + // 启动端点服务 + let endpoint_for_serve = endpoint.inner.clone(); + tokio::spawn(async move { + endpoint_for_serve.serve().await.ok(); + }); + + // 创建对话层 + let dialog_layer = Arc::new(DialogLayer::new(endpoint.inner.clone())); + + // 启动传入请求处理 + Self::start_incoming_handler( + endpoint.incoming_transactions()?, + dialog_layer.clone(), + cancel_token.clone(), + ); + + Ok(Self { + config, + endpoint, + dialog_layer, + cancel_token, + local_ip, + }) + } + + /// 启动传入请求处理器 + fn start_incoming_handler( + mut incoming: rsipstack::transaction::TransactionReceiver, + dialog_layer: Arc, + cancel_token: CancellationToken, + ) { + tokio::spawn(async move { + while let Some(mut transaction) = tokio::select! { + tx = incoming.recv() => tx, + _ = cancel_token.cancelled() => None, + } { + let method = transaction.original.method; + debug!("收到传入请求: {}", method); + + if let Some(mut dialog) = dialog_layer.match_dialog(&transaction.original) { + tokio::spawn(async move { + if let Err(e) = dialog.handle(&mut transaction).await { + error!("处理 {} 请求失败: {}", method, e); + } + }); + } else { + warn!("未找到匹配的对话: {}", method); + } + } + }); + } + + /// 执行注册 + pub async fn register(&self) -> Result> { + info!("正在注册到 SIP 服务器..."); + + let actual_local_addr = self + .endpoint + .get_addrs() + .first() + .ok_or("未找到地址")? + .addr + .clone(); + + info!("本地绑定的实际地址: {}", actual_local_addr); + + // 构造注册URI(从 config.server 复制并移除 transport 参数) + let mut register_uri = self.config.server.clone(); + + // 移除 transport 参数(如果有)registrar 不需要 transport 参数 + register_uri + .params + .retain(|p| !matches!(p, rsip::Param::Transport(_))); + + info!("Register URI: {}", register_uri); + + // 创建认证凭证 + let credential = Credential { + username: self.config.username.clone(), + password: self.config.password.clone(), + realm: None, // 将从 401 响应自动提取 + }; + + // 创建 Registration 实例(全局 route_set 已在 Endpoint 层面配置) + let mut registration = Registration::new(self.endpoint.inner.clone(), Some(credential)); + + // 执行注册 + let response = registration.register(register_uri, Some(3600)).await?; + + if response.status_code == rsip::StatusCode::OK { + info!("✔ 注册成功,响应状态: {}", response.status_code); + } else { + warn!("注册响应: {}", response.status_code); + } + + Ok(response) + } + + /// 发起呼叫 + pub async fn make_call(&self, target: &str) -> Result<(), Box> { + info!("📞发起呼叫到: {}", target); + + let actual_local_addr = self + .endpoint + .get_addrs() + .first() + .ok_or("未找到地址")? + .addr + .clone(); + + let contact_uri_str = format!("sip:{}@{}", self.config.username, actual_local_addr); + + // 构造 From/To URI(使用服务器URI的域名部分) + let server_domain = self.config.server.host_with_port.to_string(); + + let from_uri = format!("sip:{}@{}", self.config.username, server_domain); + let to_uri = if target.contains('@') { + format!("sip:{}", target) + } else { + format!("sip:{}@{}", target, server_domain) + }; + + info!("Call信息 源:{} -> 目标:{}", from_uri, to_uri); + + // 创建 RTP 会话 + let rtp_cancel = self.cancel_token.child_token(); + let media_opt = MediaSessionOption { + rtp_start_port: self.config.rtp_start_port, + external_ip: None, + cancel_token: rtp_cancel.clone(), + }; + + let ssrc = rand::rng().random::(); + let payload_type = 0u8; // PCMU + + let (rtp_conn, sdp_offer) = + rtp::build_rtp_conn(self.local_ip, &media_opt, ssrc, payload_type).await?; + + debug!("SDP Offer:{}", sdp_offer); + + // 生成呼叫 Call-ID(直接使用 UUID 字符串) + let call_id_string = uuid::Uuid::new_v4().to_string(); + info!("生成呼叫 Call-ID: {}", call_id_string); + + // 创建认证凭证 + let credential = Credential { + username: self.config.username.clone(), + password: self.config.password.clone(), + realm: None, // 将从 401/407 响应自动提取 + }; + + // 全局 route_set 已在 Endpoint 层面配置,INVITE 会自动使用 + let invite_opt = InviteOption { + caller: from_uri.as_str().try_into()?, + callee: to_uri.as_str().try_into()?, + contact: contact_uri_str.as_str().try_into()?, + credential: Some(credential), + caller_display_name: None, + caller_params: vec![], + destination: None, // 让 rsipstack 自动从 Route header 解析 + content_type: Some("application/sdp".to_string()), + offer: Some(sdp_offer.as_bytes().to_vec()), + headers: None, // 不需要手动添加,rsipstack 自动处理 + support_prack: false, + call_id: Some(call_id_string), + }; + + // 创建状态通道 + let (state_sender, state_receiver) = self.dialog_layer.new_dialog_state_channel(); + + // 发送 INVITE + let (dialog, response) = self + .dialog_layer + .do_invite(invite_opt, state_sender) + .await?; + + let dialog_id = dialog.id(); + info!( + "✅ INVITE 请求已发送,Dialog -> Call-ID: {} From-Tag: {} To-Tag: {}", + dialog_id.call_id, dialog_id.from_tag, dialog_id.to_tag + ); + + if let Some(resp) = response { + info!("响应状态: {}", resp.status_code()); + + // 处理 SDP Answer + let body = resp.body(); + if !body.is_empty() { + let sdp_answer = String::from_utf8_lossy(body); + debug!("SDP Answer: {}", sdp_answer); + + if let Some(peer_addr) = extract_peer_rtp_addr(&sdp_answer) { + info!("✓ 对端 RTP 地址: {}", peer_addr); + + // 启动对话状态监控 + let dialog_clone = Arc::new(dialog.clone()); + let rtp_cancel_clone = rtp_cancel.clone(); + tokio::spawn(async move { + process_dialog(dialog_clone, state_receiver, rtp_cancel_clone).await; + }); + + // 启动 RTP 回声 + info!("🔊 启动回声模式"); + let rtp_cancel_clone = rtp_cancel.clone(); + let peer_addr_clone = peer_addr.clone(); + tokio::spawn(async move { + if let Err(e) = + rtp::play_echo(rtp_conn, rtp_cancel_clone, peer_addr_clone, ssrc).await + { + error!("RTP 回声播放失败: {}", e); + } + }); + + // 等待用户挂断 + info!("📞 通话中,按 Ctrl+C 手动挂断"); + tokio::signal::ctrl_c().await?; + + // 挂断呼叫 + match dialog.bye().await { + Ok(_) => { + info!("✅ 通话结束"); + } + Err(e) => { + warn!("发送 BYE 失败: {}", e); + } + } + + rtp_cancel.cancel(); + } else { + error!("无法从 SDP Answer 中提取对端 RTP 地址"); + } + } + } + + Ok(()) + } + + /// 关闭客户端 + pub async fn shutdown(&self) { + self.cancel_token.cancel(); + tokio::time::sleep(Duration::from_millis(500)).await; + } +} diff --git a/examples/sip-caller/sip_dialog.rs b/examples/sip-caller/sip_dialog.rs new file mode 100644 index 00000000..46fe0cd8 --- /dev/null +++ b/examples/sip-caller/sip_dialog.rs @@ -0,0 +1,58 @@ +/// SIP 对话处理模块 +/// +/// 处理 SIP 对话状态变化和会话管理 +use rsipstack::dialog::{client_dialog::ClientInviteDialog, dialog::DialogState}; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info}; + +/// 处理对话状态变化 +/// +/// 异步监听对话状态变化,处理振铃、确认、终止等事件 +/// +/// # 参数 +/// - `_dialog`: 客户端邀请对话的 Arc 引用(当前未使用) +/// - `state_receiver`: 对话状态接收器 +/// - `rtp_cancel`: RTP 取消令牌,用于在对话终止时停止 RTP 流 +/// +/// # 状态处理 +/// - `Confirmed`: 对话已确认,通话建立 +/// - `Terminated`: 对话已终止,通话结束 +/// - `Early`: 振铃中(180 Ringing) +/// - 其他状态:仅记录日志 +pub async fn process_dialog( + _dialog: Arc, + mut state_receiver: UnboundedReceiver, + rtp_cancel: CancellationToken, +) { + while let Some(state) = state_receiver.recv().await { + match &state { + DialogState::Confirmed(_, _) => { + info!("✅ 对话已确认,通话已建立"); + } + DialogState::Terminated(_, reason) => { + info!("📴 对话已终止 (原因: {:?})", reason); + rtp_cancel.cancel(); + break; + } + DialogState::Early(_, resp) => { + info!("📲 振铃中 (状态码: {})", resp.status_code); + } + _ => { + debug!("对话状态变更"); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dialog_module_exists() { + // 简单的编译时测试,确保模块可用 + assert!(true); + } +} diff --git a/examples/sip-caller/sip_transport.rs b/examples/sip-caller/sip_transport.rs new file mode 100644 index 00000000..8e03b446 --- /dev/null +++ b/examples/sip-caller/sip_transport.rs @@ -0,0 +1,163 @@ +/// 传输层辅助函数模块 +/// +/// 包含创建各种传输连接和 SDP 解析的辅助函数 +use crate::config::Protocol; +use rsipstack::transport::{ + tcp::TcpConnection, udp::UdpConnection, websocket::WebSocketConnection, SipAddr, +}; +use std::net::SocketAddr; +use tokio_util::sync::CancellationToken; +use tracing::info; + +/// 根据协议类型创建传输连接 +/// +/// # 参数 +/// - `protocol`: 传输协议类型(UDP/TCP/WS/WSS) +/// - `local_addr`: 本地绑定地址 +/// - `server_addr`: 服务器地址 +/// - `cancel_token`: 取消令牌用于优雅关闭 +/// +/// # 返回 +/// 返回对应协议的 SIP 连接 +pub async fn create_transport_connection( + protocol: Protocol, + local_addr: SocketAddr, + server_addr: &str, + cancel_token: CancellationToken, +) -> Result> { + match protocol { + Protocol::Udp => { + info!("创建 UDP 连接: {}", local_addr); + let connection = UdpConnection::create_connection( + local_addr, + None, // external address + Some(cancel_token.child_token()), + ) + .await?; + Ok(connection.into()) + } + Protocol::Tcp => { + info!("创建 TCP 连接到服务器: {}", server_addr); + // 将服务器地址转换为 SipAddr + let server_sip_addr = + SipAddr::new(rsip::transport::Transport::Tcp, server_addr.try_into()?); + let connection = + TcpConnection::connect(&server_sip_addr, Some(cancel_token.child_token())).await?; + Ok(connection.into()) + } + Protocol::Ws => { + info!("创建 WebSocket 连接到服务器: ws://{}", server_addr); + // 将服务器地址转换为 SipAddr + let server_sip_addr = + SipAddr::new(rsip::transport::Transport::Ws, server_addr.try_into()?); + let connection = + WebSocketConnection::connect(&server_sip_addr, Some(cancel_token.child_token())) + .await?; + Ok(connection.into()) + } + Protocol::Wss => { + info!("创建 WebSocket Secure 连接到服务器: wss://{}", server_addr); + // 将服务器地址转换为 SipAddr + let server_sip_addr = + SipAddr::new(rsip::transport::Transport::Wss, server_addr.try_into()?); + let connection = + WebSocketConnection::connect(&server_sip_addr, Some(cancel_token.child_token())) + .await?; + Ok(connection.into()) + } + } +} + +/// 从 SDP 中提取对端 RTP 地址 +/// +/// # 参数 +/// - `sdp`: SDP 消息内容 +/// +/// # 返回 +/// 返回 IP:Port 格式的 RTP 地址,如果解析失败则返回 None +/// +/// # 示例 +/// ``` +/// let sdp = r#" +/// v=0 +/// o=- 123 456 IN IP4 192.168.1.100 +/// s=Session +/// c=IN IP4 192.168.1.100 +/// t=0 0 +/// m=audio 20000 RTP/AVP 0 +/// "#; +/// +/// let addr = extract_peer_rtp_addr(sdp); +/// assert_eq!(addr, Some("192.168.1.100:20000".to_string())); +/// ``` +pub fn extract_peer_rtp_addr(sdp: &str) -> Option { + let mut ip = None; + let mut port = None; + + for line in sdp.lines() { + let line = line.trim(); + // 解析 c= 行获取 IP + if line.starts_with("c=") { + // c=IN IP4 192.168.1.100 + if let Some(addr) = line.split_whitespace().last() { + ip = Some(addr.to_string()); + } + } + // 解析 m= 行获取端口 + else if line.starts_with("m=audio") { + // m=audio 20000 RTP/AVP 0 + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + port = parts[1].parse::().ok(); + } + } + } + + if let (Some(ip), Some(port)) = (ip, port) { + Some(format!("{}:{}", ip, port)) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_peer_rtp_addr() { + let sdp = r#"v=0 +o=- 123 456 IN IP4 192.168.1.100 +s=Session +c=IN IP4 192.168.1.100 +t=0 0 +m=audio 20000 RTP/AVP 0"#; + + let addr = extract_peer_rtp_addr(sdp); + assert_eq!(addr, Some("192.168.1.100:20000".to_string())); + } + + #[test] + fn test_extract_peer_rtp_addr_missing_ip() { + let sdp = r#"v=0 +o=- 123 456 IN IP4 192.168.1.100 +s=Session +t=0 0 +m=audio 20000 RTP/AVP 0"#; + + let addr = extract_peer_rtp_addr(sdp); + assert_eq!(addr, None); + } + + #[test] + fn test_extract_peer_rtp_addr_missing_port() { + let sdp = r#"v=0 +o=- 123 456 IN IP4 192.168.1.100 +s=Session +c=IN IP4 192.168.1.100 +t=0 0"#; + + let addr = extract_peer_rtp_addr(sdp); + assert_eq!(addr, None); + } +} diff --git a/examples/sip-caller/utils.rs b/examples/sip-caller/utils.rs new file mode 100644 index 00000000..31bc8cd8 --- /dev/null +++ b/examples/sip-caller/utils.rs @@ -0,0 +1,182 @@ +/// SIP 工具函数模块 +/// +/// 提供自定义的 SIP 相关辅助函数,用于覆盖 rsipstack 的默认行为 +use crate::config::Protocol; +use std::net::IpAddr; + +/// 从 SIP URI 中提取 transport 协议 +/// +/// 按照以下优先级提取: +/// 1. 显式的 transport 参数 (如 ;transport=tcp) +/// 2. 根据 URI scheme 推断 (sips -> TCP, sip -> UDP) +/// +/// # 参数 +/// - `uri`: SIP URI 对象引用 +/// +/// # 返回 +/// 提取到的 Protocol 类型 +/// +/// # 示例 +/// ```rust,no_run +/// use rsip::Uri; +/// use sip_caller::utils::extract_protocol_from_uri; +/// +/// let uri: Uri = "sip:example.com:5060;transport=tcp".try_into().unwrap(); +/// let protocol = extract_protocol_from_uri(&uri); +/// assert_eq!(protocol, Protocol::Tcp); +/// +/// let uri2: Uri = "sips:example.com:5061".try_into().unwrap(); +/// let protocol2 = extract_protocol_from_uri(&uri2); +/// assert_eq!(protocol2, Protocol::Tcp); // sips 默认 TLS over TCP +/// ``` +pub fn extract_protocol_from_uri(uri: &rsip::Uri) -> Protocol { + // 1. 优先从 transport 参数提取 + uri.params + .iter() + .find_map(|p| match p { + rsip::Param::Transport(t) => Some((*t).into()), + _ => None, + }) + .unwrap_or_else(|| { + // 2. 根据 scheme 返回默认值 + match uri.scheme.as_ref() { + Some(rsip::Scheme::Sips) => Protocol::Tcp, // sips默认TLS over TCP + Some(rsip::Scheme::Sip) | Some(rsip::Scheme::Other(_)) | None => Protocol::Udp, + } + }) +} + +/// 初始化日志系统 +/// +/// # 参数 +/// - `log_level`: 日志级别字符串 (trace, debug, info, warn, error) +/// +/// # 示例 +/// ```rust,no_run +/// initialize_logging("debug"); +/// ``` +pub fn initialize_logging(log_level: &str) { + let level = match log_level.to_lowercase().as_str() { + "trace" => tracing::Level::TRACE, + "debug" => tracing::Level::DEBUG, + "info" => tracing::Level::INFO, + "warn" => tracing::Level::WARN, + "error" => tracing::Level::ERROR, + _ => { + eprintln!("无效的日志级别 '{}', 使用默认值 'info'", log_level); + tracing::Level::INFO + } + }; + + tracing_subscriber::fmt().with_max_level(level).init(); +} + +/// 获取第一个非回环的网络接口 IP 地址 +/// +/// 遍历系统所有网络接口,优先返回指定版本的 IP 地址, +/// 如果找不到则自动回退到另一个版本 +/// +/// # 参数 +/// - `prefer_ipv6`: 是否优先使用 IPv6 地址 +/// +/// # 返回 +/// - `Ok(IpAddr)` - 成功找到的 IP 地址(IPv4 或 IPv6) +/// - `Err` - 未找到可用的网络接口 +/// +/// # 行为 +/// - 当 `prefer_ipv6 = true` 时:优先返回 IPv6,找不到则回退到 IPv4 +/// - 当 `prefer_ipv6 = false` 时:优先返回 IPv4,找不到则回退到 IPv6 +/// +/// # 示例 +/// ```rust,no_run +/// use sip_caller::utils::get_first_non_loopback_interface; +/// +/// // 优先使用 IPv6,找不到则回退到 IPv4 +/// let local_ip = get_first_non_loopback_interface(true).unwrap(); +/// println!("本地IP: {}", local_ip); +/// +/// // 只使用 IPv4 +/// let local_ip_v4 = get_first_non_loopback_interface(false).unwrap(); +/// println!("本地IPv4: {}", local_ip_v4); +/// ``` +pub fn get_first_non_loopback_interface( + prefer_ipv6: bool, +) -> Result> { + let interfaces = get_if_addrs::get_if_addrs()?; + + let mut ipv4_addr = None; + let mut ipv6_addr = None; + + // 遍历所有接口,收集可用的 IPv4 和 IPv6 地址 + for interface in interfaces { + if !interface.is_loopback() { + match interface.addr { + get_if_addrs::IfAddr::V4(ref addr) => { + if ipv4_addr.is_none() { + ipv4_addr = Some(IpAddr::V4(addr.ip)); + } + } + get_if_addrs::IfAddr::V6(ref addr) => { + if ipv6_addr.is_none() { + ipv6_addr = Some(IpAddr::V6(addr.ip)); + } + } + } + } + } + + // 根据优先级返回 + if prefer_ipv6 { + // 优先 IPv6,回退到 IPv4 + if let Some(addr) = ipv6_addr { + return Ok(addr); + } + if let Some(addr) = ipv4_addr { + tracing::info!("未找到 IPv6 接口,回退使用 IPv4: {}", addr); + return Ok(addr); + } + } else { + // 优先 IPv4,回退到 IPv6 + if let Some(addr) = ipv4_addr { + return Ok(addr); + } + if let Some(addr) = ipv6_addr { + tracing::info!("未找到 IPv4 接口,回退使用 IPv6: {}", addr); + return Ok(addr); + } + } + + Err("未找到可用的网络接口".into()) +} + +#[test] +fn test_get_first_non_loopback_interface_ipv4() { + // 测试优先 IPv4 + let result = get_first_non_loopback_interface(false); + + // 至少应该能找到一个接口(无论是 IPv4 还是 IPv6) + assert!( + result.is_ok(), + "应该能找到至少一个网络接口(可能回退到 IPv6)" + ); +} + +#[test] +fn test_get_first_non_loopback_interface_ipv6() { + // 测试优先 IPv6 + let result = get_first_non_loopback_interface(true); + + // 至少应该能找到一个接口(可能回退到 IPv4) + assert!( + result.is_ok(), + "应该能找到至少一个网络接口(可能回退到 IPv4)" + ); +} + +#[test] +fn test_get_first_non_loopback_interface_return_type() { + // 测试返回的地址不是回环地址 + if let Ok(addr) = get_first_non_loopback_interface(false) { + assert!(!addr.is_loopback(), "返回的地址不应该是回环地址"); + } +} diff --git a/src/dialog/registration.rs b/src/dialog/registration.rs index 887ba49e..d92a488d 100644 --- a/src/dialog/registration.rs +++ b/src/dialog/registration.rs @@ -119,7 +119,14 @@ pub struct Registration { pub allow: rsip::headers::Allow, /// Public address detected by the server (IP and port) pub public_address: Option, - pub call_id: rsip::headers::CallId, + /// Call-ID header value for the registration dialog + /// + /// The Call-ID is a unique identifier for this registration session. + /// If not explicitly set, a new Call-ID will be automatically generated + /// during the first registration attempt. Once set, the same Call-ID + /// will be reused for all subsequent re-registrations to maintain + /// dialog continuity. + pub call_id: Option, } impl Registration { @@ -159,7 +166,6 @@ impl Registration { /// # } /// ``` pub fn new(endpoint: EndpointInnerRef, credential: Option) -> Self { - let call_id = make_call_id(endpoint.option.callid_suffix.as_deref()); Self { last_seq: 0, endpoint, @@ -167,10 +173,71 @@ impl Registration { contact: None, allow: Default::default(), public_address: None, - call_id, + call_id: None, } } + /// Set a custom Call-ID for the registration dialog + /// + /// Sets a specific Call-ID header value to be used for this registration + /// session. The Call-ID uniquely identifies the registration dialog and + /// must remain consistent across re-registrations. + /// + /// If not set, a Call-ID will be automatically generated during the first + /// registration attempt. Use this method when you need to: + /// + /// * **Resume Registration** - Continue an existing registration session + /// * **Custom Format** - Use a specific Call-ID format or pattern + /// * **Debugging** - Use predictable Call-IDs for testing + /// * **Compliance** - Meet specific protocol requirements + /// + /// # Parameters + /// + /// * `call_id` - The Call-ID header to use for registration + /// + /// # Returns + /// + /// Self for method chaining + /// + /// # Examples + /// + /// ## Using a Custom Call-ID + /// + /// ```rust,no_run + /// # use rsipstack::dialog::registration::Registration; + /// # use rsipstack::transaction::endpoint::Endpoint; + /// # use rsip::headers::UntypedHeader; + /// # fn example() { + /// # let endpoint: Endpoint = todo!(); + /// let call_id = rsip::headers::CallId::new("my-custom-id@example.com"); + /// let registration = Registration::new(endpoint.inner.clone(), None) + /// .with_call_id(call_id); + /// # } + /// ``` + /// + /// ## Resuming a Registration Session + /// + /// ```rust,no_run + /// # use rsipstack::dialog::registration::Registration; + /// # use rsipstack::transaction::endpoint::Endpoint; + /// # use rsip::headers::UntypedHeader; + /// # fn example() { + /// # let endpoint: Endpoint = todo!(); + /// // First registration + /// let mut registration = Registration::new(endpoint.inner.clone(), None); + /// // ... perform registration and save call_id + /// # let saved_call_id = rsip::headers::CallId::new("session-123@device"); + /// + /// // Later, resume with the same Call-ID + /// let resumed_registration = Registration::new(endpoint.inner.clone(), None) + /// .with_call_id(saved_call_id); + /// # } + /// ``` + pub fn with_call_id(mut self, call_id: rsip::headers::CallId) -> Self { + self.call_id = Some(call_id); + self + } + /// Get the discovered public address /// /// Returns the public IP address and port discovered during the registration @@ -345,6 +412,41 @@ impl Registration { /// If you want to use a specific Contact header, you can set it manually /// before calling this method. /// + /// # Outbound Proxy / Route Headers + /// + /// If a route_set is configured via `with_route_set()`, this method will add + /// Route headers to the REGISTER request according to RFC 3261 routing rules. + /// The implementation supports both loose routing and strict routing: + /// + /// ## Loose Routing (Modern, Recommended) + /// + /// When the first route URI contains the 'lr' parameter: + /// * **Request-URI**: Always set to the target server (registrar) + /// * **Route Headers**: Added for each proxy in the route_set + /// * **Example**: + /// ```text + /// REGISTER sip:registrar.example.com SIP/2.0 + /// Route: + /// To: + /// From: ;tag=... + /// ``` + /// + /// ## Strict Routing (Legacy) + /// + /// When the first route URI lacks the 'lr' parameter: + /// * **Request-URI**: Set to the first proxy from route_set + /// * **Route Headers**: Remaining proxies + target server as last value + /// * **Example**: + /// ```text + /// REGISTER sip:proxy.example.com:5060 SIP/2.0 + /// Route: + /// To: + /// From: ;tag=... + /// ``` + /// + /// **Recommendation**: Always use the 'lr' parameter in proxy URIs for loose + /// routing, as it's the modern standard and provides better compatibility. + /// pub async fn register(&mut self, server: rsip::Uri, expires: Option) -> Result { self.last_seq += 1; @@ -395,9 +497,51 @@ impl Registration { params: vec![], } }); + + // RFC 3261 Section 12.2.1.1: Request construction with route set + // Use Endpoint's global route_set + let effective_route_set = &self.endpoint.route_set; + + // Determine Request-URI and Route headers based on routing mode + let (request_uri, route_headers) = if !effective_route_set.is_empty() { + // Check if the first route URI contains the 'lr' parameter (loose routing) + let first_route = &effective_route_set[0]; + let is_loose_routing = first_route + .params + .iter() + .any(|p| matches!(p, rsip::Param::Lr)); + + if is_loose_routing { + // Loose Routing (RFC 3261 Section 12.2.1.1): + // - Request-URI is the remote target (server/registrar) + // - Route headers contain all route set values in order + info!("Using loose routing (lr parameter present)"); + (server.clone(), effective_route_set.clone()) + } else { + // Strict Routing (RFC 3261 Section 12.2.1.1): + // - Request-URI is the first route from route set (without headers) + // - Route headers contain remaining routes + remote target as last value + info!("Using strict routing (lr parameter absent)"); + + // Create Request-URI from first route, stripping headers per RFC 3261 + let mut request_uri = first_route.clone(); + request_uri.headers.clear(); // RFC 3261: strip headers not allowed in Request-URI + + // Build Route header values: remaining routes + server as last value + let mut routes = effective_route_set[1..].to_vec(); // Skip first route + routes.push(server.clone()); // Add server as last route + + (request_uri, routes) + } + } else { + // No route set: standard direct routing + // Request-URI is the server, no Route headers + (server.clone(), vec![]) + }; + let mut request = self.endpoint.make_request( rsip::Method::Register, - server, + request_uri, via, from, to, @@ -405,8 +549,14 @@ impl Registration { None, ); + let call_id = self.call_id.clone().unwrap_or_else(|| { + let new_call_id = make_call_id(self.endpoint.option.callid_suffix.as_deref()); + self.call_id = Some(new_call_id.clone()); + new_call_id + }); + // Thanks to https://github.com/restsend/rsipstack/issues/32 - request.headers.unique_push(self.call_id.clone().into()); + request.headers.unique_push(call_id.into()); request.headers.unique_push(contact.into()); request.headers.unique_push(self.allow.clone().into()); if let Some(expires) = expires { @@ -415,6 +565,22 @@ impl Registration { .unique_push(rsip::headers::Expires::from(expires).into()); } + // Inject Route headers based on routing mode + if !route_headers.is_empty() { + for route_uri in &route_headers { + let uri_with_params = rsip::UriWithParams { + uri: route_uri.clone(), + params: vec![], + }; + let uri_with_params_list = rsip::UriWithParamsList(vec![uri_with_params]); + let typed_route = rsip::typed::Route(uri_with_params_list); + request + .headers + .push(rsip::headers::Route::from(typed_route).into()); + } + info!("Route headers added: {} route(s)", route_headers.len()); + } + let key = TransactionKey::from_request(&request, TransactionRole::Client)?; let mut tx = Transaction::new_client(key, request, self.endpoint.clone(), None); diff --git a/src/transaction/endpoint.rs b/src/transaction/endpoint.rs index 23c86f65..754eb925 100644 --- a/src/transaction/endpoint.rs +++ b/src/transaction/endpoint.rs @@ -91,12 +91,19 @@ pub struct EndpointStats { /// * `cancel_token` - Cancellation token for graceful shutdown /// * `timer_interval` - Interval for timer processing /// * `t1`, `t4`, `t1x64` - SIP timer values as per RFC 3261 +/// * `route_set` - Global route set for outbound proxy support (RFC 3261) /// /// # Timer Values /// /// * `t1` - RTT estimate (default 500ms) /// * `t4` - Maximum duration a message will remain in the network (default 4s) /// * `t1x64` - Maximum retransmission timeout (default 32s) +/// +/// # Route Set +/// +/// The global route set defines a default proxy path that all out-of-dialog +/// requests will use. This implements RFC 3261 outbound proxy support. +/// Individual dialogs or registrations can override this with their own route sets. pub struct EndpointInner { pub allows: Mutex>>, pub user_agent: String, @@ -114,6 +121,17 @@ pub struct EndpointInner { pub(super) locator: Option>, pub(super) transport_inspector: Option>, pub option: EndpointOption, + /// Global route set for outbound proxy support + /// + /// When configured, all out-of-dialog requests created by this endpoint + /// will include Route headers based on this route set. Supports both + /// loose routing (with 'lr' parameter) and strict routing. + /// + /// This is useful for: + /// * Corporate networks requiring all traffic through a specific proxy + /// * NAT traversal via edge proxies + /// * Load balancing across multiple proxy servers + pub route_set: Vec, } pub type EndpointInnerRef = Arc; @@ -145,6 +163,7 @@ pub struct EndpointBuilder { message_inspector: Option>, target_locator: Option>, transport_inspector: Option>, + route_set: Vec, } /// SIP Endpoint @@ -213,6 +232,7 @@ impl EndpointInner { message_inspector: Option>, locator: Option>, transport_inspector: Option>, + route_set: Vec, ) -> Arc { let (incoming_sender, incoming_receiver) = unbounded_channel(); Arc::new(EndpointInner { @@ -231,6 +251,7 @@ impl EndpointInner { message_inspector, locator, transport_inspector, + route_set, }) } @@ -594,6 +615,7 @@ impl EndpointBuilder { message_inspector: None, target_locator: None, transport_inspector: None, + route_set: Vec::new(), } } pub fn with_option(&mut self, option: EndpointOption) -> &mut Self { @@ -640,14 +662,74 @@ impl EndpointBuilder { self } + /// Configure global route set for outbound proxy support + /// + /// Sets a global route set that will be used for all out-of-dialog requests + /// created by this endpoint. This implements RFC 3261 compliant outbound + /// proxy support with both loose routing and strict routing. + /// + /// # Parameters + /// + /// * `route_set` - Ordered list of proxy URIs to route through + /// + /// # Examples + /// + /// ```rust,no_run + /// use rsipstack::EndpointBuilder; + /// + /// let proxy = rsip::Uri::try_from("sip:proxy.example.com:5060;lr").unwrap(); + /// let mut builder = EndpointBuilder::new(); + /// builder.with_route_set(vec![proxy]); + /// let endpoint = builder.build(); + /// ``` + pub fn with_route_set(&mut self, route_set: Vec) -> &mut Self { + self.route_set = route_set; + self + } + pub fn build(&mut self) -> Endpoint { let cancel_token = self.cancel_token.take().unwrap_or_default(); - let transport_layer = self + let mut transport_layer = self .transport_layer .take() .unwrap_or(TransportLayer::new(cancel_token.child_token())); + // If route_set is configured, set the first route as the outbound proxy + // for physical connection target (RFC 3261 Section 8.1.2) + if !self.route_set.is_empty() { + let first_route = &self.route_set[0]; + + // Try to determine transport from URI params first + let transport = first_route + .params + .iter() + .find_map(|p| match p { + rsip::Param::Transport(t) => Some(*t), + _ => None, + }) + .or_else(|| { + // Fallback: use default transport based on scheme + // Do NOT infer from port numb er as ports are often customized + match first_route.scheme.as_ref() { + Some(rsip::Scheme::Sips) => { + // Default sips: to TLS (RFC 3261) + Some(rsip::Transport::Tls) + } + Some(rsip::Scheme::Sip) | Some(rsip::Scheme::Other(_)) | None => { + // Default sip: to UDP (RFC 3261) + Some(rsip::Transport::Udp) + } + } + }); + + let outbound_addr = crate::transport::SipAddr { + r#type: transport, + addr: first_route.host_with_port.clone(), + }; + transport_layer.outbound = Some(outbound_addr); + } + let allows = self.allows.to_owned(); let user_agent = self.user_agent.to_owned(); let timer_interval = self.timer_interval.to_owned(); @@ -655,6 +737,7 @@ impl EndpointBuilder { let message_inspector = self.message_inspector.take(); let locator = self.target_locator.take(); let transport_inspector = self.transport_inspector.take(); + let route_set = self.route_set.clone(); let core = EndpointInner::new( user_agent, @@ -666,6 +749,7 @@ impl EndpointBuilder { message_inspector, locator, transport_inspector, + route_set, ); Endpoint { inner: core } diff --git a/src/transaction/message.rs b/src/transaction/message.rs index ab7625e3..c90b4359 100644 --- a/src/transaction/message.rs +++ b/src/transaction/message.rs @@ -100,7 +100,36 @@ impl EndpointInner { call_id: Option, ) -> rsip::Request { let call_id = call_id.unwrap_or_else(|| make_call_id(self.option.callid_suffix.as_deref())); - let headers = vec![ + + // RFC 3261 Section 12.2.1.1: Apply global route set if configured + // Determine Request-URI and Route headers based on routing mode + let (final_req_uri, route_headers) = if !self.route_set.is_empty() { + // Check if the first route URI contains the 'lr' parameter (loose routing) + let first_route = &self.route_set[0]; + let is_loose_routing = first_route + .params + .iter() + .any(|p| matches!(p, rsip::Param::Lr)); + + if is_loose_routing { + // Loose Routing: Request-URI unchanged, Route headers = all proxies + (req_uri.clone(), self.route_set.clone()) + } else { + // Strict Routing: Request-URI = first proxy, Route headers = remaining + target + let mut request_uri = first_route.clone(); + request_uri.headers.clear(); // RFC 3261: strip headers + + let mut routes = self.route_set[1..].to_vec(); + routes.push(req_uri.clone()); + + (request_uri, routes) + } + } else { + // No global route set: use Request-URI as-is + (req_uri, vec![]) + }; + + let mut headers = vec![ Header::Via(via.into()), Header::CallId(call_id), Header::From(from.into()), @@ -109,9 +138,23 @@ impl EndpointInner { Header::MaxForwards(70.into()), Header::UserAgent(self.user_agent.clone().into()), ]; + + // Inject Route headers if route_set is configured + if !route_headers.is_empty() { + for route_uri in &route_headers { + let uri_with_params = rsip::UriWithParams { + uri: route_uri.clone(), + params: vec![], + }; + let uri_with_params_list = rsip::UriWithParamsList(vec![uri_with_params]); + let typed_route = rsip::typed::Route(uri_with_params_list); + headers.push(rsip::headers::Route::from(typed_route).into()); + } + } + rsip::Request { method, - uri: req_uri, + uri: final_req_uri, headers: headers.into(), body: vec![], version: rsip::Version::V2, diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 9950b0e3..09a3a0f5 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -292,7 +292,14 @@ pub fn make_via_branch() -> rsip::Param { rsip::Param::Branch(format!("z9hG4bK{}", random_text(BRANCH_LEN)).into()) } -pub fn make_call_id(domain: Option<&str>) -> rsip::headers::CallId { +// Global Call-ID generator function pointer (similar to Go's approach) +static MAKE_CALL_ID_GENERATOR: std::sync::RwLock) -> rsip::headers::CallId> = + std::sync::RwLock::new(default_make_call_id); + +/// Default Call-ID generator implementation +/// +/// Generates Call-ID in the format: `@` +fn default_make_call_id(domain: Option<&str>) -> rsip::headers::CallId { format!( "{}@{}", random_text(CALL_ID_LEN), @@ -301,6 +308,60 @@ pub fn make_call_id(domain: Option<&str>) -> rsip::headers::CallId { .into() } +/// Set a custom Call-ID generator function +/// +/// This allows external projects to override the default Call-ID generation +/// logic globally. The custom generator will be used by all endpoints and +/// registrations. +/// +/// # Parameters +/// +/// * `generator` - Function that takes an optional domain and returns a Call-ID +/// +/// # Examples +/// +/// ```rust +/// use rsipstack::transaction::set_make_call_id_generator; +/// +/// // Use custom UUID-based Call-ID +/// set_make_call_id_generator(|domain| { +/// format!( +/// "{}@{}", +/// uuid::Uuid::new_v4(), +/// domain.unwrap_or("example.com") +/// ) +/// .into() +/// }); +/// ``` +pub fn set_make_call_id_generator(generator: fn(Option<&str>) -> rsip::headers::CallId) { + *MAKE_CALL_ID_GENERATOR.write().unwrap() = generator; +} + +/// Generate a Call-ID header value +/// +/// Uses the configured generator (set via `set_make_call_id_generator`), +/// or the default implementation if no custom generator is set. +/// +/// # Parameters +/// +/// * `domain` - Optional domain suffix for the Call-ID +/// +/// # Returns +/// +/// A Call-ID header value +/// +/// # Examples +/// +/// ```rust +/// use rsipstack::transaction::make_call_id; +/// +/// let call_id = make_call_id(Some("example.com")); +/// ``` +pub fn make_call_id(domain: Option<&str>) -> rsip::headers::CallId { + let generator = MAKE_CALL_ID_GENERATOR.read().unwrap(); + generator(domain) +} + pub fn make_tag() -> rsip::param::Tag { random_text(TO_TAG_LEN).into() } diff --git a/tests/outbound_proxy_test.rs b/tests/outbound_proxy_test.rs new file mode 100644 index 00000000..a5000f82 --- /dev/null +++ b/tests/outbound_proxy_test.rs @@ -0,0 +1,282 @@ +/// RFC 3261 Outbound Proxy 实现测试 +/// +/// 验证 Loose Routing 和 Strict Routing 的正确实现 +use rsipstack::{dialog::registration::Registration, transport::TransportLayer, EndpointBuilder}; +use tokio_util::sync::CancellationToken; + +#[tokio::test] +async fn test_loose_routing_register_request() { + // 配置全局 Outbound Proxy(Loose Routing) + let proxy_uri: rsip::Uri = "sip:proxy.example.com:5060;lr".try_into().unwrap(); + + let cancel_token = CancellationToken::new(); + let transport_layer = TransportLayer::new(cancel_token.clone()); + + let endpoint = EndpointBuilder::new() + .with_cancel_token(cancel_token.clone()) + .with_transport_layer(transport_layer) + .with_route_set(vec![proxy_uri.clone()]) + .build(); + + // 创建 Registration + let registration = Registration::new(endpoint.inner.clone(), None); + + // 验证通过检查 Registration 使用的 route_set + assert_eq!( + registration.endpoint.route_set.len(), + 1, + "应该有 1 个 route" + ); + assert_eq!( + registration.endpoint.route_set[0].to_string(), + "sip:proxy.example.com:5060;lr", + "Route URI 应该匹配" + ); + + // 验证 lr 参数存在 + let first_route = ®istration.endpoint.route_set[0]; + let has_lr = first_route + .params + .iter() + .any(|p| matches!(p, rsip::Param::Lr)); + assert!(has_lr, "Route URI 应该包含 lr 参数(Loose Routing)"); + + cancel_token.cancel(); + println!("✅ Loose Routing 配置测试通过"); +} + +#[tokio::test] +async fn test_strict_routing_register_request() { + // 配置全局 Outbound Proxy(Strict Routing - 无 lr 参数) + let proxy_uri: rsip::Uri = "sip:proxy.example.com:5060".try_into().unwrap(); + + let cancel_token = CancellationToken::new(); + let transport_layer = TransportLayer::new(cancel_token.clone()); + + let endpoint = EndpointBuilder::new() + .with_cancel_token(cancel_token.clone()) + .with_transport_layer(transport_layer) + .with_route_set(vec![proxy_uri.clone()]) + .build(); + + // 创建 Registration + let registration = Registration::new(endpoint.inner.clone(), None); + + // 验证 route_set + assert_eq!( + registration.endpoint.route_set.len(), + 1, + "应该有 1 个 route" + ); + + // 验证 lr 参数不存在 + let first_route = ®istration.endpoint.route_set[0]; + let has_lr = first_route + .params + .iter() + .any(|p| matches!(p, rsip::Param::Lr)); + assert!(!has_lr, "Route URI 不应该包含 lr 参数(Strict Routing)"); + + cancel_token.cancel(); + println!("✅ Strict Routing 配置测试通过"); +} + +#[tokio::test] +async fn test_multiple_proxies_loose_routing() { + // 配置多个代理(Loose Routing) + let proxy1: rsip::Uri = "sip:proxy1.example.com:5060;lr".try_into().unwrap(); + let proxy2: rsip::Uri = "sip:proxy2.example.com:5060;lr".try_into().unwrap(); + + let cancel_token = CancellationToken::new(); + let transport_layer = TransportLayer::new(cancel_token.clone()); + + let endpoint = EndpointBuilder::new() + .with_cancel_token(cancel_token.clone()) + .with_transport_layer(transport_layer) + .with_route_set(vec![proxy1.clone(), proxy2.clone()]) + .build(); + + let registration = Registration::new(endpoint.inner.clone(), None); + + // 验证多个 routes + assert_eq!( + registration.endpoint.route_set.len(), + 2, + "应该有 2 个 routes" + ); + assert_eq!( + registration.endpoint.route_set[0].to_string(), + "sip:proxy1.example.com:5060;lr" + ); + assert_eq!( + registration.endpoint.route_set[1].to_string(), + "sip:proxy2.example.com:5060;lr" + ); + + cancel_token.cancel(); + println!("✅ 多代理 Loose Routing 配置测试通过"); +} + +#[tokio::test] +async fn test_no_outbound_proxy() { + // 不配置 Outbound Proxy(直接路由) + let cancel_token = CancellationToken::new(); + let transport_layer = TransportLayer::new(cancel_token.clone()); + + let endpoint = EndpointBuilder::new() + .with_cancel_token(cancel_token.clone()) + .with_transport_layer(transport_layer) + .build(); + + let registration = Registration::new(endpoint.inner.clone(), None); + + // 验证没有 route_set + assert_eq!(registration.endpoint.route_set.len(), 0, "不应该有 routes"); + + cancel_token.cancel(); + println!("✅ 无 Outbound Proxy 配置测试通过"); +} + +#[test] +fn test_call_id_generator_go_style() { + use rsipstack::transaction::{make_call_id, set_make_call_id_generator}; + + // 设置自定义 Call-ID 生成器(Go 风格) + set_make_call_id_generator(|domain| format!("test-{}", domain.unwrap_or("default")).into()); + + // 测试生成 + let call_id = make_call_id(Some("example.com")); + // Call-ID header 格式是 "Call-ID: value",我们需要提取 value + let call_id_str = call_id.to_string(); + assert!( + call_id_str.contains("test-example.com"), + "Call-ID should contain 'test-example.com', got: {}", + call_id_str + ); + + let call_id2 = make_call_id(None); + let call_id2_str = call_id2.to_string(); + assert!( + call_id2_str.contains("test-default"), + "Call-ID should contain 'test-default', got: {}", + call_id2_str + ); + + println!("✅ Go 风格 Call-ID 生成器测试通过"); +} + +/// 测试用户提供的具体URI格式: sip:sip.tst.novo-one.com:5060;transport=udp;lr +#[tokio::test] +async fn test_user_specific_uri_format() { + let uri_string = "sip:sip.tst.novo-one.com:5060;transport=udp;lr"; + + println!("\n测试URI格式: {}", uri_string); + + // 1. 解析URI + let proxy_uri: rsip::Uri = uri_string.try_into().unwrap(); + + println!("✅ URI解析成功"); + println!(" Scheme: {:?}", proxy_uri.scheme); + println!(" Host: {}", proxy_uri.host_with_port); + println!(" Params: {:?}", proxy_uri.params); + + // 2. 检查transport参数 + let has_transport = proxy_uri + .params + .iter() + .any(|p| matches!(p, rsip::Param::Transport(rsip::Transport::Udp))); + assert!(has_transport, "应该有transport=udp参数"); + + // 3. 检查lr参数 + let has_lr = proxy_uri + .params + .iter() + .any(|p| matches!(p, rsip::Param::Lr)); + assert!(has_lr, "应该有lr参数"); + + // 4. 测试在EndpointBuilder中使用 + let cancel_token = CancellationToken::new(); + + let endpoint = EndpointBuilder::new() + .with_cancel_token(cancel_token.clone()) + .with_route_set(vec![proxy_uri.clone()]) + .build(); + + // 5. 验证outbound配置 + assert!( + endpoint.inner.transport_layer.outbound.is_some(), + "outbound应该被配置" + ); + let outbound = endpoint.inner.transport_layer.outbound.as_ref().unwrap(); + + assert_eq!( + outbound.r#type, + Some(rsip::Transport::Udp), + "Transport应该是UDP" + ); + assert_eq!( + outbound.addr.to_string(), + "sip.tst.novo-one.com:5060", + "地址应该正确解析" + ); + + // 6. 验证route_set + assert_eq!(endpoint.inner.route_set.len(), 1, "应该有1个route"); + let first_route = &endpoint.inner.route_set[0]; + println!("✅ route_set配置正确: {}", first_route); + + cancel_token.cancel(); + println!("\n✅ 格式 '{}' 完全支持!", uri_string); +} + +/// 测试完整的SIPS URI with WSS transport +#[tokio::test] +async fn test_sips_wss_full_uri() { + let uri_string = "sips:proxy.example.com:8443;transport=wss;lr"; + + println!("\n测试完整SIPS+WSS URI: {}", uri_string); + + let proxy_uri: rsip::Uri = uri_string.try_into().unwrap(); + + // 检查scheme + assert_eq!( + proxy_uri.scheme, + Some(rsip::Scheme::Sips), + "应该是sips scheme" + ); + + // 检查transport参数 + let has_wss = proxy_uri + .params + .iter() + .any(|p| matches!(p, rsip::Param::Transport(rsip::Transport::Wss))); + assert!(has_wss, "应该有transport=wss参数"); + + // 使用EndpointBuilder + let cancel_token = CancellationToken::new(); + + let endpoint = EndpointBuilder::new() + .with_cancel_token(cancel_token.clone()) + .with_route_set(vec![proxy_uri]) + .build(); + + // 验证outbound + let outbound = endpoint.inner.transport_layer.outbound.as_ref().unwrap(); + assert_eq!( + outbound.r#type, + Some(rsip::Transport::Wss), + "Transport应该是WSS" + ); + + cancel_token.cancel(); + println!("✅ SIPS+WSS完整URI支持正常!"); +} + +/// 测试错误的URI格式(在已有scheme前再加sip:) +#[tokio::test] +#[should_panic(expected = "ParseError")] +async fn test_double_scheme_uri_should_fail() { + // 这种格式应该解析失败 + let bad_uri = "sip:sips:proxy.example.com:8443;transport=wss;lr"; + let _: rsip::Uri = bad_uri.try_into().unwrap(); // 应该panic +}