ftth_rsipstack/dialog/
authenticate.rs

1use super::DialogId;
2use crate::transaction::key::{TransactionKey, TransactionRole};
3use crate::transaction::transaction::Transaction;
4use crate::transaction::{make_via_branch, random_text, CNONCE_LEN};
5use crate::Result;
6use rsip::headers::auth::{AuthQop, Qop};
7use rsip::prelude::{HasHeaders, HeadersExt, ToTypedHeader};
8use rsip::services::DigestGenerator;
9use rsip::typed::{Authorization, ProxyAuthorization};
10use rsip::{Header, Param, Response};
11
12/// SIP Authentication Credentials
13///
14/// `Credential` contains the authentication information needed for SIP
15/// digest authentication. This is used when a SIP server challenges
16/// a request with a 401 Unauthorized or 407 Proxy Authentication Required
17/// response.
18///
19/// # Fields
20///
21/// * `username` - The username for authentication
22/// * `password` - The password for authentication
23/// * `realm` - Optional authentication realm (extracted from challenge)
24///
25/// # Examples
26///
27/// ## Basic Usage
28///
29/// ```rust,no_run
30/// # use rsipstack::dialog::authenticate::Credential;
31/// # fn example() -> rsipstack::Result<()> {
32/// let credential = Credential {
33///     username: "alice".to_string(),
34///     password: "secret123".to_string(),
35///     realm: Some("example.com".to_string()),
36/// };
37/// # Ok(())
38/// # }
39/// ```
40///
41/// ## Usage with Registration
42///
43/// ```rust,no_run
44/// # use rsipstack::dialog::authenticate::Credential;
45/// # fn example() -> rsipstack::Result<()> {
46/// let credential = Credential {
47///     username: "alice".to_string(),
48///     password: "secret123".to_string(),
49///     realm: None, // Will be extracted from server challenge
50/// };
51///
52/// // Use credential with registration
53/// // let registration = Registration::new(endpoint.inner.clone(), Some(credential));
54/// # Ok(())
55/// # }
56/// ```
57///
58/// ## Usage with INVITE
59///
60/// ```rust,no_run
61/// # use rsipstack::dialog::authenticate::Credential;
62/// # use rsipstack::dialog::invitation::InviteOption;
63/// # fn example() -> rsipstack::Result<()> {
64/// # let sdp_bytes = vec![];
65/// # let credential = Credential {
66/// #     username: "alice".to_string(),
67/// #     password: "secret123".to_string(),
68/// #     realm: Some("example.com".to_string()),
69/// # };
70/// let invite_option = InviteOption {
71///     caller: rsip::Uri::try_from("sip:alice@example.com")?,
72///     callee: rsip::Uri::try_from("sip:bob@example.com")?,
73///     destination: None,
74///     content_type: Some("application/sdp".to_string()),
75///     offer: Some(sdp_bytes),
76///     contact: rsip::Uri::try_from("sip:alice@192.168.1.100:5060")?,
77///     credential: Some(credential),
78///     headers: None,
79/// };
80/// # Ok(())
81/// # }
82/// ```
83#[derive(Clone)]
84pub struct Credential {
85    pub username: String,
86    pub password: String,
87    pub realm: Option<String>,
88}
89
90/// Handle client-side authentication challenge
91///
92/// This function processes a 401 Unauthorized or 407 Proxy Authentication Required
93/// response and creates a new transaction with proper authentication headers.
94/// It implements SIP digest authentication according to RFC 3261 and RFC 2617.
95///
96/// # Parameters
97///
98/// * `new_seq` - New CSeq number for the authenticated request
99/// * `tx` - Original transaction that received the authentication challenge
100/// * `resp` - Authentication challenge response (401 or 407)
101/// * `cred` - User credentials for authentication
102///
103/// # Returns
104///
105/// * `Ok(Transaction)` - New transaction with authentication headers
106/// * `Err(Error)` - Failed to process authentication challenge
107///
108/// # Examples
109///
110/// ## Automatic Authentication Handling
111///
112/// ```rust,no_run
113/// # use rsipstack::dialog::authenticate::{handle_client_authenticate, Credential};
114/// # use rsipstack::transaction::transaction::Transaction;
115/// # use rsip::Response;
116/// # async fn example() -> rsipstack::Result<()> {
117/// # let new_seq = 1u32;
118/// # let original_tx: Transaction = todo!();
119/// # let auth_challenge_response: Response = todo!();
120/// # let credential = Credential {
121/// #     username: "alice".to_string(),
122/// #     password: "secret123".to_string(),
123/// #     realm: Some("example.com".to_string()),
124/// # };
125/// // This is typically called automatically by dialog methods
126/// let new_tx = handle_client_authenticate(
127///     new_seq,
128///     original_tx,
129///     auth_challenge_response,
130///     &credential
131/// ).await?;
132///
133/// // Send the authenticated request
134/// new_tx.send().await?;
135/// # Ok(())
136/// # }
137/// ```
138///
139/// ## Manual Authentication Flow
140///
141/// ```rust,no_run
142/// # use rsipstack::dialog::authenticate::{handle_client_authenticate, Credential};
143/// # use rsipstack::transaction::transaction::Transaction;
144/// # use rsip::{SipMessage, StatusCode, Response};
145/// # async fn example() -> rsipstack::Result<()> {
146/// # let mut tx: Transaction = todo!();
147/// # let credential = Credential {
148/// #     username: "alice".to_string(),
149/// #     password: "secret123".to_string(),
150/// #     realm: Some("example.com".to_string()),
151/// # };
152/// # let new_seq = 2u32;
153/// // Send initial request
154/// tx.send().await?;
155///
156/// while let Some(message) = tx.receive().await {
157///     match message {
158///         SipMessage::Response(resp) => {
159///             match resp.status_code {
160///                 StatusCode::Unauthorized | StatusCode::ProxyAuthenticationRequired => {
161///                     // Handle authentication challenge
162///                     let auth_tx = handle_client_authenticate(
163///                         new_seq, tx, resp, &credential
164///                     ).await?;
165///
166///                     // Send authenticated request
167///                     auth_tx.send().await?;
168///                     tx = auth_tx;
169///                 },
170///                 StatusCode::OK => {
171///                     println!("Request successful");
172///                     break;
173///                 },
174///                 _ => {
175///                     println!("Request failed: {}", resp.status_code);
176///                     break;
177///                 }
178///             }
179///         },
180///         _ => {}
181///     }
182/// }
183/// # Ok(())
184/// # }
185/// ```
186///
187/// This function handles SIP authentication challenges and creates authenticated requests.
188pub async fn handle_client_authenticate(
189    new_seq: u32,
190    tx: Transaction,
191    resp: Response,
192    cred: &Credential,
193) -> Result<Transaction> {
194    let header = match resp.www_authenticate_header() {
195        Some(h) => Header::WwwAuthenticate(h.clone()),
196        None => {
197            let code = resp.status_code.clone();
198            let proxy_header = rsip::header_opt!(resp.headers().iter(), Header::ProxyAuthenticate);
199            let proxy_header = proxy_header.ok_or(crate::Error::DialogError(
200                "missing proxy/www authenticate".to_string(),
201                DialogId::try_from(&tx.original)?,
202                code,
203            ))?;
204            Header::ProxyAuthenticate(proxy_header.clone())
205        }
206    };
207
208    let mut new_req = tx.original.clone();
209    new_req.cseq_header_mut()?.mut_seq(new_seq)?;
210
211    let challenge = match &header {
212        Header::WwwAuthenticate(h) => h.typed()?,
213        Header::ProxyAuthenticate(h) => h.typed()?.0,
214        _ => unreachable!(),
215    };
216
217    let cnonce = random_text(CNONCE_LEN);
218    let auth_qop = match challenge.qop {
219        Some(Qop::Auth) => Some(AuthQop::Auth { cnonce, nc: 1 }),
220        Some(Qop::AuthInt) => Some(AuthQop::AuthInt { cnonce, nc: 1 }),
221        _ => None,
222    };
223
224    // Use MD5 as default algorithm if none specified (RFC 2617 compatibility)
225    let algorithm = challenge
226        .algorithm
227        .unwrap_or(rsip::headers::auth::Algorithm::Md5);
228
229    let response = DigestGenerator {
230        username: cred.username.as_str(),
231        password: cred.password.as_str(),
232        algorithm,
233        nonce: challenge.nonce.as_str(),
234        method: &tx.original.method,
235        qop: auth_qop.as_ref(),
236        uri: &tx.original.uri,
237        realm: challenge.realm.as_str(),
238    }
239    .compute();
240
241    let auth = Authorization {
242        scheme: challenge.scheme,
243        username: cred.username.clone(),
244        realm: challenge.realm,
245        nonce: challenge.nonce,
246        uri: tx.original.uri.clone(),
247        response,
248        algorithm: Some(algorithm),
249        opaque: challenge.opaque,
250        qop: auth_qop,
251    };
252
253    let via_header = tx.original.via_header()?.clone();
254
255    // update new branch
256    let mut params = via_header.params().clone()?;
257    params.push(make_via_branch());
258    params.push(Param::Other("rport".into(), None));
259    new_req.headers_mut().unique_push(via_header.into());
260
261    new_req.headers_mut().retain(|h| {
262        !matches!(
263            h,
264            Header::ProxyAuthenticate(_)
265                | Header::Authorization(_)
266                | Header::WwwAuthenticate(_)
267                | Header::ProxyAuthorization(_)
268        )
269    });
270
271    match header {
272        Header::WwwAuthenticate(_) => {
273            new_req.headers_mut().unique_push(auth.into());
274        }
275        Header::ProxyAuthenticate(_) => {
276            new_req
277                .headers_mut()
278                .unique_push(ProxyAuthorization(auth).into());
279        }
280        _ => unreachable!(),
281    }
282    let key = TransactionKey::from_request(&new_req, TransactionRole::Client)?;
283    let mut new_tx = Transaction::new_client(
284        key,
285        new_req,
286        tx.endpoint_inner.clone(),
287        tx.connection.clone(),
288    );
289    new_tx.destination = tx.destination.clone();
290    Ok(new_tx)
291}