use std::sync::Arc;
use rvoip_sip_core::types::headers::TypedHeader;
use rvoip_sip_core::types::Method;
use crate::api::headers::{take_staged, BuilderHeaderState, SipRequestOptions};
use crate::api::incoming::ExactResponseObligation;
use crate::api::respond::AuthScheme;
use crate::api::unified::UnifiedCoordinator;
use crate::errors::{Result, SessionError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegisterResponseKind {
Accept,
Challenge(AuthScheme, bool ),
Reject(u16),
}
pub struct RegisterResponseBuilder {
transaction_id: String,
coordinator: Option<Arc<UnifiedCoordinator>>,
response_obligation: Option<Arc<ExactResponseObligation>>,
kind: RegisterResponseKind,
expires: u32,
min_expires: Option<u32>,
service_route: Vec<String>,
path_echo: bool,
associated_uri: Vec<String>,
contact: Option<String>,
www_authenticate_raw: Option<String>,
challenge_realm: Option<String>,
challenge_nonce: Option<String>,
challenge_algorithm: Option<String>,
challenge_opaque: Option<String>,
challenge_qop: Option<String>,
challenge_stale: bool,
state: BuilderHeaderState,
}
impl RegisterResponseBuilder {
pub(crate) fn new(
transaction_id: impl Into<String>,
coordinator: Option<Arc<UnifiedCoordinator>>,
) -> Self {
Self {
transaction_id: transaction_id.into(),
coordinator,
response_obligation: None,
kind: RegisterResponseKind::Accept,
expires: 3600,
min_expires: None,
service_route: Vec::new(),
path_echo: false,
associated_uri: Vec::new(),
contact: None,
www_authenticate_raw: None,
challenge_realm: None,
challenge_nonce: None,
challenge_algorithm: None,
challenge_opaque: None,
challenge_qop: None,
challenge_stale: false,
state: BuilderHeaderState::default(),
}
}
pub(crate) fn new_challenge(
transaction_id: impl Into<String>,
coordinator: Option<Arc<UnifiedCoordinator>>,
scheme: AuthScheme,
) -> Self {
let mut s = Self::new(transaction_id, coordinator);
s.kind = RegisterResponseKind::Challenge(scheme, false);
s
}
pub(crate) fn new_reject(
transaction_id: impl Into<String>,
coordinator: Option<Arc<UnifiedCoordinator>>,
status: u16,
) -> Self {
let mut s = Self::new(transaction_id, coordinator);
s.kind = RegisterResponseKind::Reject(status);
s
}
pub(crate) fn with_response_obligation(
mut self,
response_obligation: Option<Arc<ExactResponseObligation>>,
) -> Self {
self.response_obligation = response_obligation;
self
}
pub fn with_expires(mut self, secs: u32) -> Self {
self.expires = secs;
self
}
pub fn with_min_expires(mut self, secs: u32) -> Self {
self.min_expires = Some(secs);
self
}
pub fn with_service_route(mut self, routes: Vec<String>) -> Self {
self.service_route = routes;
self
}
pub fn with_path_echo(mut self) -> Self {
self.path_echo = true;
self
}
pub fn with_associated_uri(mut self, uris: Vec<String>) -> Self {
self.associated_uri = uris;
self
}
pub fn with_contact_from_binding(mut self, contact_uri: impl Into<String>) -> Self {
self.contact = Some(contact_uri.into());
self
}
pub fn with_status(mut self, code: u16) -> Self {
self.kind = RegisterResponseKind::Reject(code);
self
}
pub fn with_realm(mut self, s: impl Into<String>) -> Self {
self.challenge_realm = Some(s.into());
self
}
pub fn with_nonce(mut self, s: impl Into<String>) -> Self {
self.challenge_nonce = Some(s.into());
self
}
pub fn with_algorithm(mut self, s: impl Into<String>) -> Self {
self.challenge_algorithm = Some(s.into());
self
}
pub fn with_opaque(mut self, s: impl Into<String>) -> Self {
self.challenge_opaque = Some(s.into());
self
}
pub fn with_qop(mut self, s: impl Into<String>) -> Self {
self.challenge_qop = Some(s.into());
self
}
pub fn with_stale(mut self, stale: bool) -> Self {
self.challenge_stale = stale;
self
}
pub fn with_digest_challenge(mut self, challenge: &crate::auth::DigestChallenge) -> Self {
self.challenge_realm = Some(challenge.realm.clone());
self.challenge_nonce = Some(challenge.nonce.clone());
self.challenge_algorithm = Some(challenge.algorithm.as_str().to_string());
self.challenge_opaque = challenge.opaque.clone();
self.challenge_qop = challenge.qop.as_ref().map(|qop| qop.join(","));
self
}
pub fn as_proxy_challenge(mut self, proxy: bool) -> Self {
if let RegisterResponseKind::Challenge(scheme, _) = self.kind {
self.kind = RegisterResponseKind::Challenge(scheme, proxy);
}
self
}
pub fn with_raw_www_authenticate(mut self, body: impl Into<String>) -> Self {
self.www_authenticate_raw = Some(body.into());
self
}
fn render_digest_challenge(&self) -> Result<String> {
let realm = self.challenge_realm.clone().ok_or_else(|| {
SessionError::InvalidInput(
"RegisterResponseBuilder challenge requires with_realm(..)".to_string(),
)
})?;
let nonce = self.challenge_nonce.clone().ok_or_else(|| {
SessionError::InvalidInput(
"RegisterResponseBuilder challenge requires with_nonce(..)".to_string(),
)
})?;
let mut out = format!("Digest realm=\"{}\", nonce=\"{}\"", realm, nonce);
if let Some(alg) = self.challenge_algorithm.as_ref() {
out.push_str(&format!(", algorithm={}", alg));
}
if let Some(opaque) = self.challenge_opaque.as_ref() {
out.push_str(&format!(", opaque=\"{}\"", opaque));
}
if let Some(qop) = self.challenge_qop.as_ref() {
out.push_str(&format!(", qop=\"{}\"", qop));
}
if self.challenge_stale {
out.push_str(", stale=true");
}
Ok(out)
}
pub fn build_event_fields(mut self) -> Result<RegisterResponseEventFields> {
let extras = take_staged(&mut self.state);
let extra_headers_wire = extras
.into_iter()
.map(|h| {
let name = h.name().canonical_wire_name().as_str().to_string();
let value = render_typed_header_value(&h);
(name, value)
})
.collect::<Vec<_>>();
let (status_code, reason, www_authenticate) = match self.kind {
RegisterResponseKind::Accept => (200u16, "OK".to_string(), None),
RegisterResponseKind::Reject(s) => (s, default_reason_for(s).to_string(), None),
RegisterResponseKind::Challenge(AuthScheme::Digest, proxy) => {
let rendered = self
.www_authenticate_raw
.clone()
.map(Ok)
.unwrap_or_else(|| self.render_digest_challenge())?;
if proxy {
(
407u16,
"Proxy Authentication Required".to_string(),
Some(rendered),
)
} else {
(401u16, "Unauthorized".to_string(), Some(rendered))
}
}
RegisterResponseKind::Challenge(AuthScheme::Bearer, proxy) => {
let realm = self.challenge_realm.clone().ok_or_else(|| {
SessionError::InvalidInput(
"Bearer challenge requires with_realm(..)".to_string(),
)
})?;
let rendered = format!("Bearer realm=\"{}\"", realm);
if proxy {
(
407u16,
"Proxy Authentication Required".to_string(),
Some(rendered),
)
} else {
(401u16, "Unauthorized".to_string(), Some(rendered))
}
}
RegisterResponseKind::Challenge(AuthScheme::Basic, proxy) => {
let realm = self.challenge_realm.clone().ok_or_else(|| {
SessionError::InvalidInput(
"Basic challenge requires with_realm(..)".to_string(),
)
})?;
let rendered = format!("Basic realm=\"{}\"", realm);
if proxy {
(
407u16,
"Proxy Authentication Required".to_string(),
Some(rendered),
)
} else {
(401u16, "Unauthorized".to_string(), Some(rendered))
}
}
RegisterResponseKind::Challenge(AuthScheme::Aka, proxy) => {
let realm = self.challenge_realm.clone().ok_or_else(|| {
SessionError::InvalidInput("AKA challenge requires with_realm(..)".to_string())
})?;
let nonce = self.challenge_nonce.clone().ok_or_else(|| {
SessionError::InvalidInput("AKA challenge requires with_nonce(..)".to_string())
})?;
let algorithm = self
.challenge_algorithm
.clone()
.unwrap_or_else(|| "AKAv1-MD5".to_string());
let mut rendered = format!(
"Digest realm=\"{}\", nonce=\"{}\", algorithm={}",
realm, nonce, algorithm
);
if let Some(qop) = self.challenge_qop.as_ref() {
rendered.push_str(&format!(", qop=\"{}\"", qop));
}
if self.challenge_stale {
rendered.push_str(", stale=true");
}
if proxy {
(
407u16,
"Proxy Authentication Required".to_string(),
Some(rendered),
)
} else {
(401u16, "Unauthorized".to_string(), Some(rendered))
}
}
};
Ok(RegisterResponseEventFields {
transaction_id: self.transaction_id,
status_code,
reason,
www_authenticate,
contact: self.contact,
expires: if matches!(self.kind, RegisterResponseKind::Accept) {
Some(self.expires)
} else {
None
},
min_expires: self.min_expires,
service_route: self.service_route,
path_echo: self.path_echo,
associated_uri: self.associated_uri,
extra_headers: extra_headers_wire,
})
}
pub async fn send(self) -> Result<()> {
let coordinator = self.coordinator.clone().ok_or_else(|| {
SessionError::InvalidInput(
"RegisterResponseBuilder.send() requires an IncomingRegister with a \
coordinator hook; synthesized wrappers (tests, legacy registrar) cannot \
dispatch via this builder"
.to_string(),
)
})?;
let response_obligation = self.response_obligation.clone();
let fields = self.build_event_fields()?;
let Some(obligation) = response_obligation else {
return coordinator.send_register_response_fields(&fields).await;
};
let claim = obligation.claim()?;
let result = coordinator
.dialog_adapter()
.send_register_response_fields_classified(&fields)
.await;
match result {
Ok(rvoip_sip_dialog::FinalResponseCompletionDisposition::WrittenSuccessTerminal)
| Ok(rvoip_sip_dialog::FinalResponseCompletionDisposition::WireUnknownErrorTerminal) => {
claim.complete();
Ok(())
}
Ok(rvoip_sip_dialog::FinalResponseCompletionDisposition::ZeroWireRetryable) => {
claim.release_after_failure();
Err(SessionError::DialogError(
"failed to send exact REGISTER response before transport write".to_string(),
))
}
Err(error)
if error.disposition
== rvoip_sip_dialog::FinalResponseCompletionDisposition::ZeroWireRetryable =>
{
claim.release_after_failure();
Err(SessionError::DialogError(
"failed to send exact REGISTER response before transport write".to_string(),
))
}
Err(error) => {
claim.complete();
tracing::warn!(
status_code = fields.status_code,
disposition = ?error.disposition,
"REGISTER response became wire-unknown; retiring its exact response obligation"
);
Ok(())
}
}
}
}
#[derive(Clone)]
pub struct RegisterResponseEventFields {
pub transaction_id: String,
pub status_code: u16,
pub reason: String,
pub www_authenticate: Option<String>,
pub contact: Option<String>,
pub expires: Option<u32>,
pub min_expires: Option<u32>,
pub service_route: Vec<String>,
pub path_echo: bool,
pub associated_uri: Vec<String>,
pub extra_headers: Vec<(String, String)>,
}
impl std::fmt::Debug for RegisterResponseEventFields {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RegisterResponseEventFields")
.field("transaction_bytes", &self.transaction_id.len())
.field("status_code", &self.status_code)
.field("reason_bytes", &self.reason.len())
.field("challenge_present", &self.www_authenticate.is_some())
.field("contact_present", &self.contact.is_some())
.field("expires", &self.expires)
.field("min_expires", &self.min_expires)
.field("service_route_count", &self.service_route.len())
.field("path_echo", &self.path_echo)
.field("associated_uri_count", &self.associated_uri.len())
.field("extra_header_count", &self.extra_headers.len())
.finish()
}
}
impl SipRequestOptions for RegisterResponseBuilder {
fn method(&self) -> Method {
Method::Register
}
fn header_state_mut(&mut self) -> &mut BuilderHeaderState {
&mut self.state
}
fn header_state(&self) -> &BuilderHeaderState {
&self.state
}
}
fn render_typed_header_value(h: &TypedHeader) -> String {
if let TypedHeader::Other(_, hv) = h {
if let Some(s) = hv.as_text() {
return s.to_string();
}
}
let rendered = h.to_string();
if let Some(idx) = rendered.find(':') {
rendered[idx + 1..].trim().to_string()
} else {
rendered
}
}
fn default_reason_for(status: u16) -> &'static str {
match status {
200 => "OK",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
407 => "Proxy Authentication Required",
423 => "Interval Too Brief",
503 => "Service Unavailable",
_ => "Rejected",
}
}
#[cfg(test)]
mod tests {
use super::*;
use rvoip_sip_core::types::headers::HeaderName;
#[test]
fn staged_register_response_headers_keep_wire_names() {
let fields = RegisterResponseBuilder::new("register-transaction", None)
.with_raw_header(HeaderName::Other("sUpPoRtEd".into()), "outbound")
.expect("Supported is application-controlled on REGISTER responses")
.with_raw_header(HeaderName::Other("x-VENDOR-trace".into()), "trace-value")
.expect("extension header is application-controlled")
.build_event_fields()
.expect("event fields");
assert_eq!(
fields.extra_headers,
vec![
("Supported".to_string(), "outbound".to_string()),
("X-Vendor-Trace".to_string(), "trace-value".to_string()),
]
);
assert!(fields
.extra_headers
.iter()
.all(|(name, _)| !name.contains("HeaderName")));
}
#[test]
fn application_register_response_completes_single_writer_obligation() {
let source = include_str!("register_response.rs");
let send = source
.split("pub async fn send(self) -> Result<()> {")
.nth(1)
.and_then(|tail| tail.split("/// Wire-format snapshot").next())
.expect("RegisterResponseBuilder::send source");
assert!(send.contains("obligation.claim()?"));
assert!(send.contains("send_register_response_fields_classified"));
assert!(send.contains("claim.complete()"));
assert!(send.contains("claim.release_after_failure()"));
assert!(send.contains("ZeroWireRetryable"));
assert!(send.contains("WireUnknownErrorTerminal"));
}
}