use std::sync::atomic::AtomicUsize;
use std::{sync::Arc, time::Duration};
use super::common::{
create_auth_request, create_proxy_auth_request_with_nonce, create_test_request,
create_test_server, create_test_server_with_config, create_transaction,
extract_nonce_from_proxy_authenticate,
};
use crate::call::{SipUser, TransactionCookie};
use crate::config::{ProxyConfig, RtpConfig};
use crate::proxy::active_call_registry::ActiveProxyCallRegistry;
use crate::proxy::auth::AuthModule;
use crate::proxy::data::ProxyDataContext;
use crate::proxy::locator::{Locator, MemoryLocator};
use crate::proxy::server::SipServerInner;
use crate::proxy::user::MemoryUserBackend;
use crate::proxy::{ProxyAction, ProxyModule};
use rsipstack::EndpointBuilder;
use rsipstack::dialog::dialog_layer::DialogLayer;
use rsipstack::sip::Header;
use rsipstack::sip::prelude::{HasHeaders, HeadersExt};
use rsipstack::sip::services::DigestGenerator;
use rsipstack::transaction::endpoint::EndpointInner;
use rsipstack::transaction::key::{TransactionKey, TransactionRole};
use rsipstack::transaction::random_text;
use rsipstack::transaction::transaction::Transaction;
use rsipstack::transport::TransportLayer;
use tokio_util::sync::CancellationToken;
#[tokio::test]
async fn test_auth_module_invite_success() {
let (server_inner, _) = create_test_server().await;
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let request = create_test_request(
rsipstack::sip::Method::Invite,
"alice",
None,
"rustpbx.com",
None,
);
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
if tx.last_response.is_none() {
let mut response = rsipstack::sip::Response {
version: rsipstack::sip::Version::V2,
status_code: rsipstack::sip::StatusCode::Unauthorized,
headers: tx.original.headers().clone(),
body: vec![],
};
let www_auth = module.create_www_auth_challenge("rustpbx.com").unwrap();
response.headers.push(Header::WwwAuthenticate(www_auth));
tx.last_response = Some(response);
}
let response = tx.last_response.as_ref().unwrap();
let nonce = if let Some(Header::WwwAuthenticate(h)) = response
.headers()
.iter()
.find(|h| matches!(h, Header::WwwAuthenticate(_)))
{
let auth_str = h.value();
auth_str
.split(',')
.find_map(|part| {
let part = part.trim();
if part.starts_with("nonce=") {
Some(
part.trim_start_matches("nonce=")
.trim_matches('"')
.to_string(),
)
} else {
None
}
})
.unwrap()
} else {
panic!("No WWW-Authenticate header");
};
let request_with_auth = {
let host_with_port = rsipstack::sip::HostWithPort {
host: "rustpbx.com".parse().unwrap(),
port: Some(5060.into()),
};
let uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from = rsipstack::sip::typed::From {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let to = rsipstack::sip::typed::To {
display_name: None,
uri: uri.clone(),
params: vec![],
};
let via = rsipstack::sip::headers::Via::new(format!(
"SIP/2.0/UDP rustpbx.com:5060;branch=z9hG4bK{}",
random_text(8)
));
let call_id = rsipstack::sip::headers::CallId::new(random_text(16));
let cseq = rsipstack::sip::headers::typed::CSeq {
seq: 1u32,
method: rsipstack::sip::Method::Invite,
};
let contact_uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: Some("password".to_string()),
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let contact = rsipstack::sip::typed::Contact {
display_name: None,
uri: contact_uri,
params: vec![],
};
let mut headers = vec![
from.into(),
to.into(),
via.into(),
call_id.into(),
cseq.into(),
contact.into(),
];
let digest = DigestGenerator {
username: "alice",
password: "password",
algorithm: rsipstack::sip::headers::auth::Algorithm::Md5,
nonce: &nonce,
method: &rsipstack::sip::Method::Invite,
uri: &uri,
realm: "rustpbx.com",
qop: None,
};
let auth_header = rsipstack::sip::headers::Authorization::new(format!(
"Digest username=\"alice\", realm=\"rustpbx.com\", nonce=\"{}\", uri=\"{}\", response=\"{}\", algorithm=MD5",
nonce,
uri,
digest.compute()
));
headers.push(auth_header.into());
rsipstack::sip::Request {
method: rsipstack::sip::Method::Invite,
uri: uri.clone(),
version: rsipstack::sip::Version::V2,
headers: headers.into(),
body: vec![],
}
};
let (mut tx2, _) = create_transaction(request_with_auth).await;
let result2 = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx2,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result2, ProxyAction::Continue));
}
#[tokio::test]
async fn test_auth_module_register_success() {
let (server_inner, _) = create_test_server().await;
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let request = create_test_request(
rsipstack::sip::Method::Register,
"alice",
None,
"rustpbx.com",
None,
);
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
if tx.last_response.is_none() {
let mut response = rsipstack::sip::Response {
version: rsipstack::sip::Version::V2,
status_code: rsipstack::sip::StatusCode::Unauthorized,
headers: tx.original.headers().clone(),
body: vec![],
};
let www_auth = module.create_www_auth_challenge("rustpbx.com").unwrap();
response.headers.push(Header::WwwAuthenticate(www_auth));
tx.last_response = Some(response);
}
let response = tx.last_response.as_ref().unwrap();
let nonce = if let Some(Header::WwwAuthenticate(h)) = response
.headers()
.iter()
.find(|h| matches!(h, Header::WwwAuthenticate(_)))
{
let auth_str = h.value();
auth_str
.split(',')
.find_map(|part| {
let part = part.trim();
if part.starts_with("nonce=") {
Some(
part.trim_start_matches("nonce=")
.trim_matches('"')
.to_string(),
)
} else {
None
}
})
.unwrap()
} else {
panic!("No WWW-Authenticate header");
};
let request_with_auth = {
let host_with_port = rsipstack::sip::HostWithPort {
host: "rustpbx.com".parse().unwrap(),
port: Some(5060.into()),
};
let uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from = rsipstack::sip::typed::From {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let to = rsipstack::sip::typed::To {
display_name: None,
uri: uri.clone(),
params: vec![],
};
let via = rsipstack::sip::headers::Via::new(format!(
"SIP/2.0/UDP rustpbx.com:5060;branch=z9hG4bK{}",
random_text(8)
));
let call_id = rsipstack::sip::headers::CallId::new(random_text(16));
let cseq = rsipstack::sip::headers::typed::CSeq {
seq: 1u32,
method: rsipstack::sip::Method::Register,
};
let contact_uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: Some("password".to_string()),
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let contact = rsipstack::sip::typed::Contact {
display_name: None,
uri: contact_uri,
params: vec![],
};
let mut headers = vec![
from.into(),
to.into(),
via.into(),
call_id.into(),
cseq.into(),
contact.into(),
];
let digest = DigestGenerator {
username: "alice",
password: "password",
algorithm: rsipstack::sip::headers::auth::Algorithm::Md5,
nonce: &nonce,
method: &rsipstack::sip::Method::Register,
uri: &uri,
realm: "rustpbx.com",
qop: None,
};
let auth_header = rsipstack::sip::headers::Authorization::new(format!(
"Digest username=\"alice\", realm=\"rustpbx.com\", nonce=\"{}\", uri=\"{}\", response=\"{}\", algorithm=MD5",
nonce,
uri,
digest.compute()
));
headers.push(auth_header.into());
rsipstack::sip::Request {
method: rsipstack::sip::Method::Register,
uri: uri.clone(),
version: rsipstack::sip::Version::V2,
headers: headers.into(),
body: vec![],
}
};
let (mut tx2, _) = create_transaction(request_with_auth).await;
let result2 = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx2,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result2, ProxyAction::Continue));
}
#[tokio::test]
async fn test_auth_module_disabled_user() {
let (server_inner, _) = create_test_server().await;
let request = create_auth_request(
rsipstack::sip::Method::Invite,
"bob",
"rustpbx.com",
"password",
);
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
}
#[tokio::test]
async fn test_auth_module_unknown_user() {
let (server_inner, _) = create_test_server().await;
let request = create_auth_request(
rsipstack::sip::Method::Invite,
"unknown",
"rustpbx.com",
"123456",
);
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
}
#[tokio::test]
async fn test_auth_module_bypass_other_methods() {
let (server_inner, _) = create_test_server().await;
let request = create_auth_request(
rsipstack::sip::Method::Options,
"unknown",
"rustpbx.com",
"123456",
);
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Continue));
}
#[tokio::test]
async fn test_guest_call_allowed_extension() {
let mut proxy_config = ProxyConfig::default();
if proxy_config.realms.is_none() {
proxy_config.realms = Some(vec![]);
}
proxy_config
.realms
.as_mut()
.unwrap()
.push("rustpbx.com".to_string());
let builtin_users = vec![SipUser {
id: 2000,
username: "2000".to_string(),
enabled: true,
realm: Some("rustpbx.com".to_string()),
allow_guest_calls: true,
..Default::default()
}];
let user_backend = MemoryUserBackend::new(Some(builtin_users));
let locator = Arc::new(Box::new(MemoryLocator::new()) as Box<dyn Locator>);
let config = Arc::new(proxy_config);
let endpoint = EndpointBuilder::new().build();
let dialog_layer = Arc::new(DialogLayer::new(endpoint.inner.clone()));
let data_context = Arc::new(
ProxyDataContext::new(config.clone(), None)
.await
.expect("failed to init proxy data context for auth test"),
);
let server_inner = Arc::new(SipServerInner {
rtp_config: RtpConfig::default(),
proxy_config: config,
cancel_token: CancellationToken::new(),
data_context,
database: None,
user_backend: Box::new(user_backend),
auth_backend: Vec::new(),
call_router: None,
locator,
callrecord_sender: None,
endpoint,
dialog_layer,
dialplan_inspectors: Vec::new(),
create_route_invite: None,
ignore_out_of_dialog_request: true,
locator_events: None,
sip_flow: None,
active_call_registry: Arc::new(ActiveProxyCallRegistry::new()),
frequency_limiter: None,
call_record_hooks: Arc::new(Vec::new()),
runnings_tx: Arc::new(AtomicUsize::new(0)),
storage: None,
presence_manager: Arc::new(crate::proxy::presence::PresenceManager::new(None)),
addon_registry: None,
rwi_gateway: None,
tls_listener: None,
queue_manager: Arc::new(crate::call::runtime::QueueManager::new()),
conference_manager: Arc::new(crate::call::runtime::ConferenceManager::new()),
agent_registry: None,
transfer_notify_subscribers: Arc::new(tokio::sync::Mutex::new(Vec::new())),
cluster_event_hub: None,
cluster_peer_ips: vec![],
});
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let request = {
let realm = "rustpbx.com";
let caller = "guestuser";
let callee = "2000";
let host_with_port = rsipstack::sip::HostWithPort {
host: realm.parse().unwrap(),
port: Some(5060.into()),
};
let to_uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: callee.to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from_uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: caller.to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from = rsipstack::sip::typed::From {
display_name: None,
uri: from_uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let to = rsipstack::sip::typed::To {
display_name: None,
uri: to_uri.clone(),
params: vec![],
};
let via = rsipstack::sip::headers::Via::new(format!(
"SIP/2.0/UDP {}:5060;branch=z9hG4bK{}",
realm,
random_text(8)
));
let contact = rsipstack::sip::typed::Contact {
display_name: None,
uri: from_uri.clone(),
params: vec![],
};
let headers = vec![
from.into(),
to.into(),
via.into(),
rsipstack::sip::headers::CallId::new(random_text(16)).into(),
rsipstack::sip::headers::typed::CSeq {
seq: 1u32,
method: rsipstack::sip::Method::Invite,
}
.into(),
contact.into(),
];
rsipstack::sip::Request {
method: rsipstack::sip::Method::Invite,
uri: to_uri,
version: rsipstack::sip::Version::V2,
headers: headers.into(),
body: vec![],
}
};
let (mut tx, _) = create_transaction(request).await;
let cookie = TransactionCookie::default();
let result = module
.on_transaction_begin(CancellationToken::new(), &mut tx, cookie.clone())
.await
.unwrap();
assert!(matches!(result, ProxyAction::Continue));
assert!(tx.last_response.is_none());
let stored_user = cookie.get_user().expect("caller should be stored");
assert_eq!(stored_user.username, "guestuser");
}
fn create_sip_request(
method: rsipstack::sip::Method,
username: &str,
realm: &str,
) -> rsipstack::sip::Request {
let host_with_port = rsipstack::sip::HostWithPort {
host: realm.parse().unwrap(),
port: Some(5060.into()),
};
let uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: username.to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from = rsipstack::sip::typed::From {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
"fromtag",
))],
};
let to = rsipstack::sip::typed::To {
display_name: None,
uri: uri.clone(),
params: vec![],
};
let via = rsipstack::sip::headers::Via::new(format!(
"SIP/2.0/UDP {}:5060;branch=z9hG4bKnashds7",
realm
));
let call_id = rsipstack::sip::headers::CallId::new("test-call-id");
let cseq = rsipstack::sip::headers::typed::CSeq { seq: 1u32, method };
let mut request = rsipstack::sip::Request {
method,
uri,
version: rsipstack::sip::Version::V2,
headers: vec![
from.into(),
to.into(),
via.into(),
call_id.into(),
cseq.into(),
]
.into(),
body: vec![],
};
if username == "bob" {
let uri_str = format!("sip:{}@{}", username, realm);
request.headers_mut().push(Header::Authorization(
rsipstack::sip::headers::Authorization::new(
format!("Digest username=\"{}\", realm=\"{}\", nonce=\"random_nonce\", uri=\"{}\", response=\"invalid\"", username, realm, uri_str)
)
));
}
request
}
async fn create_issue_146_server() -> Arc<SipServerInner> {
let config = ProxyConfig {
realms: Some(vec![
"pbx.e36".to_string(),
"pbx.e36:5060".to_string(),
"pbx.e36:5061".to_string(),
]),
..Default::default()
};
let (server, _) = create_test_server_with_config(config).await;
server
.user_backend
.create_user(SipUser {
id: 111,
username: "111".to_string(),
password: Some("111".to_string()),
enabled: true,
realm: Some("pbx.e36".to_string()),
..Default::default()
})
.await
.unwrap();
server
}
fn create_issue_146_register_request(
request_uri: &str,
authorization: &str,
) -> rsipstack::sip::Request {
let request_uri = rsipstack::sip::Uri::try_from(request_uri).unwrap();
let aor_uri = rsipstack::sip::Uri::try_from("sip:111@pbx.e36").unwrap();
let contact_uri =
rsipstack::sip::Uri::try_from("sip:111@192.168.20.169:5060;transport=tls").unwrap();
let headers = vec![
rsipstack::sip::typed::From {
display_name: Some("Deskphone".into()),
uri: aor_uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
"a1a3406102",
))],
}
.into(),
rsipstack::sip::typed::To {
display_name: None,
uri: aor_uri,
params: vec![],
}
.into(),
rsipstack::sip::headers::Via::new(
"SIP/2.0/TLS 192.168.20.169:5060;branch=z9hG4bK64964f4e5cad27a81".to_string(),
)
.into(),
rsipstack::sip::headers::CallId::new("fd9623914bbc79b9").into(),
rsipstack::sip::headers::typed::CSeq {
seq: 873199510u32,
method: rsipstack::sip::Method::Register,
}
.into(),
rsipstack::sip::typed::Contact {
display_name: Some("Deskphone".into()),
uri: contact_uri,
params: vec![rsipstack::sip::Param::Expires(
rsipstack::sip::param::Expires::from("3600"),
)],
}
.into(),
rsipstack::sip::headers::Authorization::new(authorization.to_string()).into(),
];
rsipstack::sip::Request {
method: rsipstack::sip::Method::Register,
uri: request_uri,
version: rsipstack::sip::Version::V2,
headers: headers.into(),
body: vec![],
}
}
#[tokio::test]
async fn test_authenticate_request_accepts_authorization_uri_when_request_uri_differs() {
let server = create_issue_146_server().await;
let module = AuthModule::new(server.clone(), server.proxy_config.clone());
let auth_header_value = r#"Digest username="111",realm="pbx.e36",nonce="MoLk0nzBonitjdoo",uri="sip:pbx.e36:5060;transport=udp",response="5a832a648a56b95f905b8db1d28d8f5b",algorithm=MD5"#;
let request = create_issue_146_register_request("sip:pbx.e36:5060", auth_header_value);
let (tx, _) = create_transaction(request).await;
let result = module.authenticate_request(&tx).await.unwrap();
assert!(
result.is_some(),
"authentication should succeed when the digest matches the Authorization uri from issue #146"
);
}
#[tokio::test]
async fn test_authenticate_request_preserves_authorization_uri_transport_case() {
let server = create_issue_146_server().await;
let module = AuthModule::new(server.clone(), server.proxy_config.clone());
let auth_header_value = r#"Digest username="111",realm="pbx.e36",nonce="K1KmT96onZZVMvBB",uri="sip:pbx.e36:5061;transport=tls",response="0c9ba3a13fbcc4f342fd7eb9c2be6a83",algorithm=MD5"#;
let request =
create_issue_146_register_request("sip:pbx.e36:5061;transport=tls", auth_header_value);
let (tx, _) = create_transaction(request).await;
let result = module.authenticate_request(&tx).await.unwrap();
assert!(
result.is_some(),
"authentication should preserve the exact Authorization uri bytes from issue #146"
);
}
#[tokio::test]
async fn test_auth_no_credentials() {
let (server, _) = create_test_server().await;
let auth_module = AuthModule::new(server.clone(), server.proxy_config.clone());
let request = create_sip_request(rsipstack::sip::Method::Invite, "alice", "rustpbx.com");
let transport_layer = TransportLayer::new(CancellationToken::new());
let endpoint_inner = EndpointInner::new(
"RustPBX Test".to_string(),
transport_layer,
CancellationToken::new(),
Some(Duration::from_millis(20)),
vec![
rsipstack::sip::Method::Invite,
rsipstack::sip::Method::Register,
],
None,
None,
None,
None,
);
let key = TransactionKey::from_request(&request, TransactionRole::Server).unwrap();
let tx = Transaction::new_server(key, request, endpoint_inner, None);
let result = auth_module.authenticate_request(&tx).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_auth_bypass_for_non_invite_register() {
let (server, _) = create_test_server().await;
let auth_module = AuthModule::new(server.clone(), server.proxy_config.clone());
let request = create_sip_request(rsipstack::sip::Method::Bye, "alice", "rustpbx.com");
let transport_layer = TransportLayer::new(CancellationToken::new());
let endpoint_inner = EndpointInner::new(
"RustPBX Test".to_string(),
transport_layer,
CancellationToken::new(),
Some(Duration::from_millis(20)),
vec![
rsipstack::sip::Method::Invite,
rsipstack::sip::Method::Register,
],
None,
None,
None,
None,
);
let key = TransactionKey::from_request(&request, TransactionRole::Server).unwrap();
let mut tx = Transaction::new_server(key, request, endpoint_inner, None);
let result = auth_module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Continue));
}
#[tokio::test]
async fn test_auth_disabled_user() {
let (server, _) = create_test_server().await;
let auth_module = AuthModule::new(server.clone(), server.proxy_config.clone());
let request = create_sip_request(rsipstack::sip::Method::Invite, "bob", "rustpbx.com");
println!(
"Test request: method={}, uri={}",
request.method, request.uri
);
println!("From header: {}", request.from_header().unwrap());
println!(
"Has auth header: {}",
request.authorization_header().is_some()
);
let transport_layer = TransportLayer::new(CancellationToken::new());
let endpoint_inner = EndpointInner::new(
"RustPBX Test".to_string(),
transport_layer,
CancellationToken::new(),
Some(Duration::from_millis(20)),
vec![
rsipstack::sip::Method::Invite,
rsipstack::sip::Method::Register,
],
None,
None,
None,
None,
);
let key = TransactionKey::from_request(&request, TransactionRole::Server).unwrap();
let tx = Transaction::new_server(key, request, endpoint_inner, None);
let result = auth_module.authenticate_request(&tx).await.unwrap();
println!("Authentication result: {:?}", result);
assert!(result.is_none());
}
#[tokio::test]
async fn test_proxy_auth_invite_success() {
let (server_inner, _) = create_test_server().await;
let request = create_test_request(
rsipstack::sip::Method::Invite,
"alice",
None,
"rustpbx.com",
None,
);
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
if tx.last_response.is_none() {
let mut response = rsipstack::sip::Response {
version: rsipstack::sip::Version::V2,
status_code: rsipstack::sip::StatusCode::Unauthorized,
headers: tx.original.headers().clone(),
body: vec![],
};
let www_auth = module.create_www_auth_challenge("rustpbx.com").unwrap();
let proxy_auth = module.create_proxy_auth_challenge("rustpbx.com").unwrap();
response.headers.push(Header::WwwAuthenticate(www_auth));
response.headers.push(Header::ProxyAuthenticate(proxy_auth));
tx.last_response = Some(response);
}
let response = tx.last_response.as_ref().expect("Should have response");
assert_eq!(
response.status_code,
rsipstack::sip::StatusCode::ProxyAuthenticationRequired
);
let nonce = extract_nonce_from_proxy_authenticate(response)
.expect("Should have nonce in Proxy-Authenticate header");
let request_with_auth = create_proxy_auth_request_with_nonce(
rsipstack::sip::Method::Invite,
"alice",
"rustpbx.com",
Some("password"),
&nonce,
);
let (mut tx2, _) = create_transaction(request_with_auth).await;
let result2 = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx2,
TransactionCookie::default(),
)
.await
.unwrap();
let auth_result = module.authenticate_request(&tx2).await.unwrap();
assert!(
auth_result.is_some(),
"Authentication should succeed with correct credentials"
);
assert!(matches!(result2, ProxyAction::Continue));
}
#[tokio::test]
async fn test_proxy_auth_no_credentials() {
let (server_inner, _) = create_test_server().await;
let request = create_test_request(
rsipstack::sip::Method::Invite,
"alice",
None,
"rustpbx.com",
None,
);
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
if let Some(response) = &tx.last_response {
assert_eq!(
response.status_code,
rsipstack::sip::StatusCode::ProxyAuthenticationRequired
);
let has_proxy_auth = response
.headers()
.iter()
.any(|h| matches!(h, Header::ProxyAuthenticate(_)));
assert!(
has_proxy_auth,
"Response should contain Proxy-Authenticate header"
);
}
}
#[tokio::test]
async fn test_proxy_auth_wrong_credentials() {
let (server_inner, _) = create_test_server().await;
let request = create_test_request(
rsipstack::sip::Method::Invite,
"alice",
None,
"rustpbx.com",
None,
);
let module = AuthModule::new(server_inner.clone(), server_inner.proxy_config.clone());
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
if tx.last_response.is_none() {
let mut response = rsipstack::sip::Response {
version: rsipstack::sip::Version::V2,
status_code: rsipstack::sip::StatusCode::ProxyAuthenticationRequired,
headers: tx.original.headers().clone(),
body: vec![],
};
let proxy_auth = module.create_proxy_auth_challenge("rustpbx.com").unwrap();
response.headers.push(Header::ProxyAuthenticate(proxy_auth));
tx.last_response = Some(response);
}
let response = tx.last_response.as_ref().expect("Should have response");
let nonce = extract_nonce_from_proxy_authenticate(response)
.expect("Should have nonce in Proxy-Authenticate header");
let request_with_wrong_auth = create_proxy_auth_request_with_nonce(
rsipstack::sip::Method::Invite,
"alice",
"rustpbx.com",
Some("wrongpassword"),
&nonce,
);
let (mut tx2, _) = create_transaction(request_with_wrong_auth).await;
let auth_result = module.authenticate_request(&tx).await.unwrap();
println!("Direct authentication result: {:?}", auth_result);
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx2,
TransactionCookie::default(),
)
.await
.unwrap();
println!("Authentication result: {:?}", result);
assert!(matches!(result, ProxyAction::Abort));
}
#[tokio::test]
async fn test_dialog_auth_cache_skips_in_dialog_reinvite() {
let (server_inner, _) = create_test_server().await;
let mut proxy_config = (*server_inner.proxy_config).clone();
proxy_config.dialog_auth_cache = Some(crate::config::AuthCacheConfig {
enabled: true,
cache_size: 100,
ttl_seconds: 3600,
});
let proxy_config = Arc::new(proxy_config);
let module = AuthModule::new(server_inner.clone(), proxy_config.clone());
let request = create_test_request(
rsipstack::sip::Method::Invite,
"alice",
None,
"rustpbx.com",
None,
);
let (mut tx, _) = create_transaction(request).await;
let result = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(matches!(result, ProxyAction::Abort));
if tx.last_response.is_none() {
let mut response = rsipstack::sip::Response {
version: rsipstack::sip::Version::V2,
status_code: rsipstack::sip::StatusCode::Unauthorized,
headers: tx.original.headers().clone(),
body: vec![],
};
let www_auth = module.create_www_auth_challenge("rustpbx.com").unwrap();
response.headers.push(Header::WwwAuthenticate(www_auth));
tx.last_response = Some(response);
}
let response = tx.last_response.as_ref().unwrap();
let nonce = if let Some(Header::WwwAuthenticate(h)) = response
.headers()
.iter()
.find(|h| matches!(h, Header::WwwAuthenticate(_)))
{
let auth_str = h.value();
auth_str
.split(',')
.find_map(|part| {
let part = part.trim();
if part.starts_with("nonce=") {
Some(
part.trim_start_matches("nonce=")
.trim_matches('"')
.to_string(),
)
} else {
None
}
})
.unwrap()
} else {
panic!("No WWW-Authenticate header");
};
let request_with_auth = {
let host_with_port = rsipstack::sip::HostWithPort {
host: "rustpbx.com".parse().unwrap(),
port: Some(5060.into()),
};
let uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from = rsipstack::sip::typed::From {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let to = rsipstack::sip::typed::To {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let via = rsipstack::sip::headers::Via::new(format!(
"SIP/2.0/UDP rustpbx.com:5060;branch=z9hG4bK{}",
random_text(8)
));
let call_id = rsipstack::sip::headers::CallId::new(random_text(16));
let cseq = rsipstack::sip::headers::typed::CSeq {
seq: 1u32,
method: rsipstack::sip::Method::Invite,
};
let contact_uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: Some("password".to_string()),
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let contact = rsipstack::sip::typed::Contact {
display_name: None,
uri: contact_uri,
params: vec![],
};
let mut headers = vec![
from.into(),
to.into(),
via.into(),
call_id.into(),
cseq.into(),
contact.into(),
];
let digest = DigestGenerator {
username: "alice",
password: "password",
algorithm: rsipstack::sip::headers::auth::Algorithm::Md5,
nonce: &nonce,
method: &rsipstack::sip::Method::Invite,
uri: &uri,
realm: "rustpbx.com",
qop: None,
};
let auth_header = rsipstack::sip::headers::Authorization::new(format!(
"Digest username=\"alice\", realm=\"rustpbx.com\", nonce=\"{}\", uri=\"{}\", response=\"{}\", algorithm=MD5",
nonce,
uri,
digest.compute()
));
headers.push(auth_header.into());
rsipstack::sip::Request {
method: rsipstack::sip::Method::Invite,
uri: uri.clone(),
version: rsipstack::sip::Version::V2,
headers: headers.into(),
body: vec![],
}
};
let (mut tx2, _) = create_transaction(request_with_auth).await;
let result2 = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx2,
TransactionCookie::default(),
)
.await
.unwrap();
assert!(
matches!(result2, ProxyAction::Continue),
"Initial authenticated INVITE should succeed"
);
let reinvite_request = {
let host_with_port = rsipstack::sip::HostWithPort {
host: "rustpbx.com".parse().unwrap(),
port: Some(5060.into()),
};
let uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: None,
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let from = rsipstack::sip::typed::From {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let to = rsipstack::sip::typed::To {
display_name: None,
uri: uri.clone(),
params: vec![rsipstack::sip::Param::Tag(rsipstack::sip::param::Tag::new(
random_text(8),
))],
};
let via = rsipstack::sip::headers::Via::new(format!(
"SIP/2.0/UDP rustpbx.com:5060;branch=z9hG4bK{}",
random_text(8)
));
let call_id = rsipstack::sip::headers::CallId::new(random_text(16));
let cseq = rsipstack::sip::headers::typed::CSeq {
seq: 2u32,
method: rsipstack::sip::Method::Invite,
};
let contact_uri = rsipstack::sip::Uri {
scheme: Some(rsipstack::sip::Scheme::Sip),
auth: Some(rsipstack::sip::Auth {
user: "alice".to_string(),
password: Some("password".to_string()),
}),
host_with_port: host_with_port.clone(),
params: vec![],
headers: vec![],
};
let contact = rsipstack::sip::typed::Contact {
display_name: None,
uri: contact_uri,
params: vec![],
};
let headers = vec![
from.into(),
to.into(),
via.into(),
call_id.into(),
cseq.into(),
contact.into(),
];
rsipstack::sip::Request {
method: rsipstack::sip::Method::Invite,
uri: uri.clone(),
version: rsipstack::sip::Version::V2,
headers: headers.into(),
body: vec![],
}
};
let (mut tx3, _) = create_transaction(reinvite_request).await;
let result3 = module
.on_transaction_begin(
CancellationToken::new(),
&mut tx3,
TransactionCookie::default(),
)
.await
.unwrap();
println!("Re-INVITE result: {:?}", result3);
}