use crate::dialog::authenticate::{handle_client_authenticate, Credential};
use crate::sip::headers::*;
use crate::sip::prelude::{HeadersExt, ToTypedHeader};
use crate::sip::{Request, Response, StatusCode};
use crate::transaction::{
endpoint::EndpointBuilder,
key::{TransactionKey, TransactionRole},
transaction::Transaction,
};
use crate::transport::TransportLayer;
use tokio_util::sync::CancellationToken;
async fn create_test_endpoint() -> crate::Result<crate::transaction::endpoint::Endpoint> {
let token = CancellationToken::new();
let tl = TransportLayer::new(token.child_token());
let endpoint = EndpointBuilder::new()
.with_user_agent("rsipstack-test")
.with_transport_layer(tl)
.build();
Ok(endpoint)
}
fn create_request_with_branch(branch: &str) -> Request {
Request {
method: crate::sip::Method::Register,
uri: crate::sip::Uri::try_from("sip:example.com:5060").unwrap(),
headers: vec![
Via::new(&format!(
"SIP/2.0/UDP alice.example.com:5060;branch={}",
branch
))
.into(),
CSeq::new("1 REGISTER").into(),
From::new("Alice <sip:alice@example.com>;tag=1928301774").into(),
To::new("Bob <sip:bob@example.com>").into(),
CallId::new("a84b4c76e66710@pc33.atlanta.com").into(),
MaxForwards::new("70").into(),
]
.into(),
version: crate::sip::Version::V2,
body: vec![],
}
}
fn create_401_response() -> Response {
Response {
status_code: StatusCode::Unauthorized,
version: crate::sip::Version::V2,
headers: vec![
Via::new("SIP/2.0/UDP alice.example.com:5060;branch=z9hG4bKnashds").into(),
CSeq::new("1 REGISTER").into(),
From::new("Alice <sip:alice@example.com>;tag=1928301774").into(),
To::new("Bob <sip:bob@example.com>").into(),
CallId::new("a84b4c76e66710@pc33.atlanta.com").into(),
WwwAuthenticate::new(
r#"Digest realm="example.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", algorithm=MD5, qop="auth""#,
)
.into(),
]
.into(),
body: vec![],
}
}
#[tokio::test]
async fn test_authenticate_via_header_branch_update() -> crate::Result<()> {
let endpoint = create_test_endpoint().await?;
let original_branch = "z9hG4bKoriginal123";
let original_req = create_request_with_branch(original_branch);
let original_via = original_req
.via_header()
.expect("Request should have Via header")
.typed()
.expect("Via header should be parseable");
let original_branch_param = original_via
.params
.iter()
.find(|p| matches!(p, crate::sip::Param::Branch(_)))
.expect("Original request should have branch parameter");
let original_branch_value = match original_branch_param {
crate::sip::Param::Branch(b) => b.to_string(),
_ => unreachable!(),
};
assert_eq!(original_branch_value, original_branch);
let key = TransactionKey::from_request(&original_req, TransactionRole::Client)?;
let tx = Transaction::new_client(key, original_req, endpoint.inner.clone(), None);
let resp = create_401_response();
let cred = Credential {
username: "alice".to_string(),
password: "secret123".to_string(),
realm: None,
};
let new_tx = handle_client_authenticate(2, &tx, resp, &cred).await?;
let new_via = new_tx
.original
.via_header()
.expect("New request should have Via header")
.typed()
.expect("Via header should be parseable");
let old_branch_exists = new_via.params.iter().any(
|p| matches!(p, crate::sip::Param::Branch(b) if b.to_string() == original_branch_value),
);
assert!(
!old_branch_exists,
"Old branch parameter should be removed from Via header"
);
let new_branch_param = new_via
.params
.iter()
.find(|p| matches!(p, crate::sip::Param::Branch(_)))
.expect("New request should have a new branch parameter");
let new_branch_value = match new_branch_param {
crate::sip::Param::Branch(b) => b.to_string(),
_ => unreachable!(),
};
assert_ne!(
new_branch_value, original_branch_value,
"New branch should be different from old branch"
);
assert!(
new_branch_value.starts_with("z9hG4bK"),
"New branch should start with z9hG4bK"
);
let has_rport = new_via
.params
.iter()
.any(|p| matches!(p, crate::sip::Param::Rport(_)));
assert!(
has_rport,
"Via header should have rport parameter after authentication"
);
Ok(())
}
#[test]
fn test_extract_digest_uri_raw() {
use crate::dialog::authenticate::extract_digest_uri_raw;
let header = r#"Digest username="111",realm="pbx.e36",nonce="K1KmT96onZZVMvBB",uri="sip:pbx.e36:5061;transport=tls",response="0c9ba3a13fbcc4f342fd7eb9c2be6a83",algorithm=MD5"#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:pbx.e36:5061;transport=tls".to_string()));
let header = r#"Digest username="111",realm="pbx.e36",nonce="abc",uri="sip:pbx.e36:5061;transport=TLS",response="xxx",algorithm=MD5"#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:pbx.e36:5061;transport=TLS".to_string()));
let header = r#"Digest username="111",realm="pbx.e36",nonce="MoLk0nzBonitjdoo",uri="sip:pbx.e36:5060;transport=udp",response="5a832a648a56b95f905b8db1d28d8f5b",algorithm=MD5"#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:pbx.e36:5060;transport=udp".to_string()));
let header = r#"Digest username="alice",realm="example.com",nonce="abc",uri="sip:example.com",response="xxx""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:example.com".to_string()));
let header = r#"Digest username="alice",realm="example.com",nonce="abc",uri="sip:example.com:5060",response="xxx""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:example.com:5060".to_string()));
let header = r#"Digest username="111",realm="pbx.e36",nonce="abc",uri="sip:pbx.e36:5061;transport=Tls",response="xxx",algorithm=MD5"#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:pbx.e36:5061;transport=Tls".to_string()));
let header = r#"Digest username="alice",realm="example.com",nonce="abc",uri="sip:example.com:5060;transport=tcp",response="xxx""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:example.com:5060;transport=tcp".to_string()));
let header = r#"Digest username="alice",realm="example.com",nonce="abc",uri="sip:alice@example.com:5060;transport=tls",response="xxx""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(
uri,
Some("sip:alice@example.com:5060;transport=tls".to_string())
);
let header = r#"Digest username="alice",realm="example.com",nonce="abc",uri="sips:example.com",response="xxx""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sips:example.com".to_string()));
let header = r#"Digest username="alice", realm="example.com", nonce="abc", uri="sip:example.com;transport=ws", response="xxx""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:example.com;transport=ws".to_string()));
let header = r#"Digest username="alice",realm="example.com",nonce="abc",uri="sip:example.com:5060;transport=udp",response="xxx",algorithm=MD5,qop=auth,nc=00000001,cnonce="yz""#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:example.com:5060;transport=udp".to_string()));
let header = r#"Digest username="111",realm="192.168.1.1",nonce="abc",uri="sip:192.168.1.1:5061;transport=tls",response="xxx",algorithm=MD5"#;
let uri = extract_digest_uri_raw(header);
assert_eq!(uri, Some("sip:192.168.1.1:5061;transport=tls".to_string()));
}
#[test]
fn test_compute_digest_case_sensitive_uri() {
use crate::dialog::authenticate::compute_digest;
use crate::sip::headers::auth::Algorithm;
let response_lower = compute_digest(
"111",
"111",
"pbx.e36",
"K1KmT96onZZVMvBB",
&crate::sip::Method::Register,
"sip:pbx.e36:5061;transport=tls",
Algorithm::Md5,
None,
);
let response_upper = compute_digest(
"111",
"111",
"pbx.e36",
"K1KmT96onZZVMvBB",
&crate::sip::Method::Register,
"sip:pbx.e36:5061;transport=TLS",
Algorithm::Md5,
None,
);
assert_ne!(
response_lower, response_upper,
"Digest should differ for different URI case"
);
let response_no_transport = compute_digest(
"111",
"111",
"pbx.e36",
"K1KmT96onZZVMvBB",
&crate::sip::Method::Register,
"sip:pbx.e36:5061",
Algorithm::Md5,
None,
);
assert_ne!(response_no_transport, response_lower);
assert_ne!(response_no_transport, response_upper);
}
fn build_and_verify(
username: &str,
password: &str,
realm: &str,
nonce: &str,
method: &crate::sip::Method,
uri_raw: &str,
algorithm: crate::sip::headers::auth::Algorithm,
) -> (bool, String) {
use crate::dialog::authenticate::{compute_digest, verify_digest};
let response = compute_digest(
username, password, realm, nonce, method, uri_raw, algorithm, None,
);
let auth_header_value = format!(
r#"Digest username="{}",realm="{}",nonce="{}",uri="{}",response="{}",algorithm={}"#,
username, realm, nonce, uri_raw, response, algorithm
);
let auth: crate::sip::typed::Authorization =
crate::sip::typed::Authorization::parse(&auth_header_value).unwrap();
let is_valid = verify_digest(&auth, password, method, &auth_header_value);
(is_valid, auth_header_value)
}
#[test]
fn test_verify_digest_lowercase_tls() {
let (is_valid, _) = build_and_verify(
"111",
"111",
"pbx.e36",
"K1KmT96onZZVMvBB",
&crate::sip::Method::Register,
"sip:pbx.e36:5061;transport=tls",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle lowercase transport=tls"
);
}
#[test]
fn test_verify_digest_uppercase_tls() {
let (is_valid, _) = build_and_verify(
"111",
"111",
"pbx.e36",
"K1KmT96onZZVMvBB",
&crate::sip::Method::Register,
"sip:pbx.e36:5061;transport=TLS",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle uppercase transport=TLS"
);
}
#[test]
fn test_verify_digest_lowercase_udp() {
let (is_valid, _) = build_and_verify(
"111",
"111",
"pbx.e36",
"MoLk0nzBonitjdoo",
&crate::sip::Method::Register,
"sip:pbx.e36:5060;transport=udp",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle lowercase transport=udp"
);
}
#[test]
fn test_verify_digest_uppercase_udp() {
let (is_valid, _) = build_and_verify(
"111",
"111",
"pbx.e36",
"MoLk0nzBonitjdoo",
&crate::sip::Method::Register,
"sip:pbx.e36:5060;transport=UDP",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle uppercase transport=UDP"
);
}
#[test]
fn test_verify_digest_lowercase_tcp() {
let (is_valid, _) = build_and_verify(
"alice",
"secret",
"example.com",
"nonce123",
&crate::sip::Method::Register,
"sip:example.com:5060;transport=tcp",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle lowercase transport=tcp"
);
}
#[test]
fn test_verify_digest_no_transport() {
let (is_valid, _) = build_and_verify(
"alice",
"secret123",
"example.com",
"dcd98b7102dd2f0e8b11d0f600bfb0c093",
&crate::sip::Method::Register,
"sip:example.com",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should work with URI without transport param"
);
}
#[test]
fn test_verify_digest_with_port_no_transport() {
let (is_valid, _) = build_and_verify(
"alice",
"secret123",
"example.com",
"nonce456",
&crate::sip::Method::Register,
"sip:example.com:5060",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should work with URI with port but no transport"
);
}
#[test]
fn test_verify_digest_invite_method() {
let (is_valid, _) = build_and_verify(
"alice",
"secret",
"example.com",
"nonce789",
&crate::sip::Method::Invite,
"sip:bob@example.com:5060;transport=tls",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(is_valid, "verify_digest should work with INVITE method");
}
#[test]
fn test_verify_digest_sips_uri() {
let (is_valid, _) = build_and_verify(
"alice",
"secret",
"example.com",
"nonce_sips",
&crate::sip::Method::Register,
"sips:example.com",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(is_valid, "verify_digest should work with SIPS URI");
}
#[test]
fn test_verify_digest_ip_address_uri() {
let (is_valid, _) = build_and_verify(
"111",
"111",
"192.168.201.31",
"nonce_ip",
&crate::sip::Method::Register,
"sip:192.168.201.31:5061;transport=tls",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(is_valid, "verify_digest should work with IP address URI");
}
#[test]
fn test_verify_digest_wrong_password_fails() {
use crate::dialog::authenticate::{compute_digest, verify_digest};
use crate::sip::headers::auth::Algorithm;
let uri_raw = "sip:pbx.e36:5061;transport=tls";
let response = compute_digest(
"111",
"111", "pbx.e36",
"nonce123",
&crate::sip::Method::Register,
uri_raw,
Algorithm::Md5,
None,
);
let auth_header_value = format!(
r#"Digest username="111",realm="pbx.e36",nonce="nonce123",uri="{}",response="{}",algorithm=MD5"#,
uri_raw, response
);
let auth: crate::sip::typed::Authorization =
crate::sip::typed::Authorization::parse(&auth_header_value).unwrap();
let is_valid = verify_digest(
&auth,
"wrong_password",
&crate::sip::Method::Register,
&auth_header_value,
);
assert!(!is_valid, "verify_digest should fail with wrong password");
}
#[test]
fn test_verify_digest_mixed_case_transport() {
let (is_valid, _) = build_and_verify(
"alice",
"secret",
"example.com",
"nonce_mixed",
&crate::sip::Method::Register,
"sip:example.com:5060;transport=Tcp",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle mixed-case transport=Tcp"
);
}
#[test]
fn test_verify_digest_websocket_transport() {
let (is_valid, _) = build_and_verify(
"webuser",
"webpass",
"ws.example.com",
"nonce_ws",
&crate::sip::Method::Register,
"sip:ws.example.com;transport=ws",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(
is_valid,
"verify_digest should handle WebSocket transport=ws"
);
}
#[test]
fn test_verify_digest_with_user_in_uri() {
let (is_valid, _) = build_and_verify(
"alice",
"secret",
"example.com",
"nonce_user",
&crate::sip::Method::Register,
"sip:alice@example.com:5060;transport=tls",
crate::sip::headers::auth::Algorithm::Md5,
);
assert!(is_valid, "verify_digest should work with user@host URI");
}
#[test]
fn test_verify_digest_rsip_digest_generator_mismatch() {
use crate::dialog::authenticate::{compute_digest, verify_digest};
use crate::sip::headers::auth::Algorithm;
let uri_raw = "sip:pbx.e36:5061;transport=tls";
let response = compute_digest(
"111",
"111",
"pbx.e36",
"K1KmT96onZZVMvBB",
&crate::sip::Method::Register,
uri_raw,
Algorithm::Md5,
None,
);
let auth_header_value = format!(
r#"Digest username="111",realm="pbx.e36",nonce="K1KmT96onZZVMvBB",uri="sip:pbx.e36:5061;transport=tls",response="{}",algorithm=MD5"#,
response
);
let auth: crate::sip::typed::Authorization =
crate::sip::typed::Authorization::parse(&auth_header_value).unwrap();
assert_eq!(
auth.uri.to_string(),
"sip:pbx.e36:5061;transport=TLS",
"rsip normalizes transport to uppercase in parsed URI"
);
let digest_gen =
crate::sip::services::DigestGenerator::from(&auth, "111", &crate::sip::Method::Register);
let rsip_response = digest_gen.compute();
assert_ne!(
rsip_response, response,
"rsip DigestGenerator produces wrong hash due to URI case normalization"
);
let is_valid = verify_digest(
&auth,
"111",
&crate::sip::Method::Register,
&auth_header_value,
);
assert!(
is_valid,
"verify_digest should succeed where rsip DigestGenerator fails"
);
}
#[test]
fn test_verify_digest_with_qop_auth() {
use crate::dialog::authenticate::{compute_digest, verify_digest};
use crate::sip::headers::auth::{Algorithm, AuthQop};
let uri_raw = "sip:pbx.e36:5061;transport=tls";
let qop = AuthQop::Auth {
cnonce: "0a4f113b".to_string(),
nc: 1,
};
let response = compute_digest(
"111",
"111",
"pbx.e36",
"dcd98b7102dd2f0e8b11d0f600bfb0c093",
&crate::sip::Method::Register,
uri_raw,
Algorithm::Md5,
Some(&qop),
);
let auth_header_value = format!(
r#"Digest username="111",realm="pbx.e36",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",uri="{}",response="{}",algorithm=MD5,qop=auth,nc=00000001,cnonce="0a4f113b""#,
uri_raw, response
);
let auth: crate::sip::typed::Authorization =
crate::sip::typed::Authorization::parse(&auth_header_value).unwrap();
let is_valid = verify_digest(
&auth,
"111",
&crate::sip::Method::Register,
&auth_header_value,
);
assert!(
is_valid,
"verify_digest should work with qop=auth and lowercase transport"
);
}
#[test]
fn test_verify_digest_real_world_unify_tls() {
use crate::dialog::authenticate::{compute_digest, verify_digest};
use crate::sip::headers::auth::Algorithm;
let username = "111";
let password = "111";
let realm = "pbx.e36";
let nonce = "K1KmT96onZZVMvBB";
let uri_raw = "sip:pbx.e36:5061;transport=tls";
let response = compute_digest(
username,
password,
realm,
nonce,
&crate::sip::Method::Register,
uri_raw,
Algorithm::Md5,
None,
);
let auth_header_value = format!(
r#"Digest username="{}",realm="{}",nonce="{}",uri="{}",response="{}",algorithm=MD5"#,
username, realm, nonce, uri_raw, response
);
let auth: crate::sip::typed::Authorization =
crate::sip::typed::Authorization::parse(&auth_header_value).unwrap();
let is_valid = verify_digest(
&auth,
password,
&crate::sip::Method::Register,
&auth_header_value,
);
assert!(
is_valid,
"Real-world Unify TLS case should pass verification"
);
}
#[test]
fn test_verify_digest_real_world_unify_udp() {
use crate::dialog::authenticate::{compute_digest, verify_digest};
use crate::sip::headers::auth::Algorithm;
let username = "111";
let password = "111";
let realm = "pbx.e36";
let nonce = "MoLk0nzBonitjdoo";
let uri_raw = "sip:pbx.e36:5060;transport=udp";
let response = compute_digest(
username,
password,
realm,
nonce,
&crate::sip::Method::Register,
uri_raw,
Algorithm::Md5,
None,
);
let auth_header_value = format!(
r#"Digest username="{}",realm="{}",nonce="{}",uri="{}",response="{}",algorithm=MD5"#,
username, realm, nonce, uri_raw, response
);
let auth: crate::sip::typed::Authorization =
crate::sip::typed::Authorization::parse(&auth_header_value).unwrap();
let is_valid = verify_digest(
&auth,
password,
&crate::sip::Method::Register,
&auth_header_value,
);
assert!(
is_valid,
"Real-world Unify UDP case should pass verification"
);
}