ftth_rsipstack/dialog/
authenticate.rs

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