use rsip::headers::auth::{Algorithm, AuthQop, Scheme};
use rsip::headers::typed::{Authorization, WwwAuthenticate};
use rsip::headers::ToTypedHeader;
use rsip::message::HeadersExt;
use rsip::services::DigestGenerator;
use rsip::{Header, Method, Request, Response, Uri};
use std::sync::atomic::{AtomicU64, Ordering};
use super::transaction::gen_branch;
pub(crate) struct Credentials<'a> {
pub username: &'a str,
pub password: &'a str,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ChallengeKind {
Www,
Proxy,
}
pub(crate) fn build_retry(
original: &Request,
response: &Response,
creds: Credentials,
) -> Option<Request> {
let (kind, challenge) = extract_challenge(response)?;
let authorization = build_authorization(&challenge, &creds, original.method(), &original.uri);
let mut retry = original.clone();
bump_via_branch(&mut retry)?;
bump_cseq(&mut retry)?;
let header = match kind {
ChallengeKind::Www => Header::Authorization(authorization.into()),
ChallengeKind::Proxy => {
let proxy = rsip::headers::typed::ProxyAuthorization(authorization);
Header::ProxyAuthorization(proxy.into())
}
};
retry.headers.unique_push(header);
Some(retry)
}
fn extract_challenge(response: &Response) -> Option<(ChallengeKind, WwwAuthenticate)> {
match response.status_code().code() {
401 => {
let www = response.www_authenticate_header()?.typed().ok()?;
Some((ChallengeKind::Www, www))
}
407 => {
let proxy = response.headers.iter().find_map(|h| match h {
Header::ProxyAuthenticate(p) => Some(p.clone()),
_ => None,
})?;
let typed = proxy.typed().ok()?;
Some((ChallengeKind::Proxy, typed.0))
}
_ => None,
}
}
fn build_authorization(
challenge: &WwwAuthenticate,
creds: &Credentials,
method: &Method,
uri: &Uri,
) -> Authorization {
let algorithm = challenge.algorithm.unwrap_or(Algorithm::Md5);
let qop = challenge.qop.as_ref().map(|_| AuthQop::Auth {
cnonce: gen_cnonce(),
nc: 1,
});
let response = DigestGenerator {
username: creds.username,
password: creds.password,
nonce: &challenge.nonce,
uri,
realm: &challenge.realm,
method,
qop: qop.as_ref(),
algorithm,
}
.compute();
Authorization {
scheme: Scheme::Digest,
username: creds.username.to_string(),
realm: challenge.realm.clone(),
nonce: challenge.nonce.clone(),
uri: uri.clone(),
response,
algorithm: Some(algorithm),
opaque: challenge.opaque.clone(),
qop,
}
}
fn bump_via_branch(request: &mut Request) -> Option<()> {
use rsip::common::uri::param::{Branch, Param};
let mut via = request.via_header().ok()?.typed().ok()?;
via.params.retain(|p| !matches!(p, Param::Branch(_)));
via.params.push(Param::Branch(Branch::new(gen_branch())));
request.headers.unique_push(Header::Via(via.into()));
Some(())
}
fn bump_cseq(request: &mut Request) -> Option<()> {
let cseq = request.cseq_header().ok()?.typed().ok()?;
let next = rsip::typed::CSeq {
seq: cseq.seq + 1,
method: cseq.method,
};
request.headers.unique_push(Header::CSeq(next.into()));
Some(())
}
static CNONCE_COUNTER: AtomicU64 = AtomicU64::new(0);
fn gen_cnonce() -> String {
use std::hash::BuildHasher;
let n = CNONCE_COUNTER.fetch_add(1, Ordering::Relaxed);
let seed = std::collections::hash_map::RandomState::new().hash_one(n);
format!("{n:x}{seed:016x}")
}
#[cfg(test)]
mod tests {
use super::*;
fn register() -> Request {
let raw = "REGISTER sip:example.com SIP/2.0\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-orig\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:alice@example.com>\r\n\
Call-ID: call-reg\r\n\
CSeq: 1 REGISTER\r\n\
Content-Length: 0\r\n\r\n";
Request::try_from(raw.as_bytes()).unwrap()
}
fn challenge_401(qop: &str, algorithm: &str) -> Response {
let qop_param = if qop.is_empty() {
String::new()
} else {
format!(", qop=\"{qop}\"")
};
let raw = format!(
"SIP/2.0 401 Unauthorized\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-orig\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:alice@example.com>;tag=srv\r\n\
Call-ID: call-reg\r\n\
CSeq: 1 REGISTER\r\n\
WWW-Authenticate: Digest realm=\"example.com\", \
nonce=\"abc123\", algorithm={algorithm}{qop_param}\r\n\
Content-Length: 0\r\n\r\n"
);
Response::try_from(raw.as_bytes()).unwrap()
}
fn creds() -> Credentials<'static> {
Credentials {
username: "alice",
password: "secret",
}
}
#[test]
fn retry_adds_authorization_and_freshens_transaction() {
let retry = build_retry(®ister(), &challenge_401("auth", "MD5"), creds()).unwrap();
let cseq = retry.cseq_header().unwrap().typed().unwrap();
assert_eq!(cseq.seq, 2);
assert_eq!(cseq.method, Method::Register);
let branch = retry
.via_header()
.unwrap()
.typed()
.unwrap()
.branch()
.unwrap()
.to_string();
assert!(branch.starts_with("z9hG4bK"));
assert_ne!(branch, "z9hG4bK-orig");
let auth = retry.authorization_header().unwrap().typed().unwrap();
assert_eq!(auth.realm, "example.com");
assert_eq!(auth.nonce, "abc123");
assert!(!auth.response.is_empty());
}
#[test]
fn computed_response_verifies_against_rsip() {
let (_, challenge) = extract_challenge(&challenge_401("auth", "SHA256")).unwrap();
let uri = register().uri;
let auth = build_authorization(&challenge, &creds(), &Method::Register, &uri);
assert_eq!(auth.algorithm, Some(Algorithm::Sha256));
let generator = DigestGenerator::from(&auth, "secret", &Method::Register);
assert!(generator.verify(&auth.response));
}
#[test]
fn no_qop_still_authorizes() {
let (_, challenge) = extract_challenge(&challenge_401("", "MD5")).unwrap();
let uri = register().uri;
let auth = build_authorization(&challenge, &creds(), &Method::Register, &uri);
assert!(auth.qop.is_none());
let generator = DigestGenerator::from(&auth, "secret", &Method::Register);
assert!(generator.verify(&auth.response));
}
#[test]
fn authorization_header_serializes_with_expected_params() {
let (_, challenge) = extract_challenge(&challenge_401("auth", "SHA256")).unwrap();
let uri = register().uri;
let auth = build_authorization(&challenge, &creds(), &Method::Register, &uri);
let wire = auth.to_string();
assert!(wire.contains("Digest username=\"alice\""));
assert!(wire.contains("realm=\"example.com\""));
assert!(wire.contains("algorithm=SHA256"));
assert!(wire.contains("qop=\"auth\""));
assert!(wire.contains("cnonce="));
}
#[test]
fn proxy_challenge_uses_proxy_authorization() {
let raw = "SIP/2.0 407 Proxy Authentication Required\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-orig\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:alice@example.com>;tag=srv\r\n\
Call-ID: call-reg\r\n\
CSeq: 1 REGISTER\r\n\
Proxy-Authenticate: Digest realm=\"proxy\", nonce=\"xyz\", qop=\"auth\"\r\n\
Content-Length: 0\r\n\r\n";
let response = Response::try_from(raw.as_bytes()).unwrap();
let retry = build_retry(®ister(), &response, creds()).unwrap();
let has_proxy_auth = retry
.headers
.iter()
.any(|h| matches!(h, Header::ProxyAuthorization(_)));
assert!(has_proxy_auth);
assert!(!retry
.headers
.iter()
.any(|h| matches!(h, Header::Authorization(_))));
}
#[test]
fn non_challenge_response_yields_no_retry() {
let raw = "SIP/2.0 200 OK\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-orig\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:alice@example.com>;tag=srv\r\n\
Call-ID: call-reg\r\n\
CSeq: 1 REGISTER\r\n\
Content-Length: 0\r\n\r\n";
let response = Response::try_from(raw.as_bytes()).unwrap();
assert!(build_retry(®ister(), &response, creds()).is_none());
}
#[test]
fn cnonce_is_unique_per_call() {
assert_ne!(gen_cnonce(), gen_cnonce());
}
}