Skip to main content

sipx_ua/
reginfo.rs

1//! Bounded RFC 3680 registration-information consumer.
2//!
3//! This is package policy behind [`crate::event_client::PackageConsumer`], not another event
4//! client. The normative merge and refusal rules are in `docs/specs/registration-discovery.md`.
5
6use std::collections::{HashMap, HashSet};
7
8use bytes::Bytes;
9use quick_xml::XmlVersion;
10use quick_xml::events::{BytesStart, Event};
11use quick_xml::name::{Namespace, ResolveResult};
12use quick_xml::reader::NsReader;
13use sipx_sip::Uri;
14
15use crate::event_client::{PackageConsumer, PackageRejection};
16use crate::packages::REGINFO_TYPE;
17
18const NAMESPACE: Namespace<'static> = Namespace(b"urn:ietf:params:xml:ns:reginfo");
19
20/// Where a registration snapshot was learned.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct RegistrarSource {
23    /// Exact resource URI subscribed at the registrar.
24    pub resource: String,
25}
26
27/// One currently active registrar contact.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct RegistrationPeer {
30    /// User-facing name derived from the registration AOR.
31    pub name: String,
32    /// Address of record which owns this contact.
33    pub aor: String,
34    /// Exact active SIP or SIPS contact URI.
35    pub uri: String,
36    /// Stable registration key from the document.
37    pub registration_id: String,
38    /// Stable contact key from the document.
39    pub contact_id: String,
40    /// Registrar resource which supplied this fact.
41    pub source: RegistrarSource,
42}
43
44/// Complete current state after one full or partial document.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct RegistrationSnapshot {
47    /// Last applied document version.
48    pub version: u32,
49    /// Active contacts, sorted by AOR, URI and document keys.
50    pub peers: Vec<RegistrationPeer>,
51}
52
53/// Stateful, bounded `reg` package consumer.
54#[derive(Debug)]
55pub struct RegistrationConsumer {
56    source: RegistrarSource,
57    contact_limit: usize,
58    version: Option<u32>,
59    peers: HashMap<(String, String), RegistrationPeer>,
60    accepts: Vec<String>,
61}
62
63impl RegistrationConsumer {
64    /// Construct one registrar view. A zero contact limit is rejected.
65    pub fn new(
66        resource: impl Into<String>,
67        contact_limit: usize,
68    ) -> Result<Self, PackageRejection> {
69        let resource = resource.into();
70        if contact_limit == 0 || !is_sip_uri(&resource) {
71            return Err(PackageRejection::malformed());
72        }
73        Ok(Self {
74            source: RegistrarSource { resource },
75            contact_limit,
76            version: None,
77            peers: HashMap::new(),
78            accepts: vec![REGINFO_TYPE.to_owned()],
79        })
80    }
81
82    fn apply(&mut self, document: Document) -> Result<RegistrationSnapshot, PackageRejection> {
83        match (self.version, document.kind) {
84            (None, DocumentKind::Full) if document.version == 0 => {}
85            (Some(previous), DocumentKind::Partial)
86                if previous.checked_add(1) == Some(document.version) => {}
87            (Some(previous), DocumentKind::Full) if document.version > previous => {}
88            _ => return Err(PackageRejection::malformed()),
89        }
90
91        let mut next = if document.kind == DocumentKind::Full {
92            HashMap::new()
93        } else {
94            self.peers.clone()
95        };
96        for registration in document.registrations {
97            if matches!(
98                registration.state,
99                RegistrationState::Init | RegistrationState::Terminated
100            ) {
101                next.retain(|(registration_id, _), _| registration_id != &registration.id);
102                continue;
103            }
104            for contact in registration.contacts {
105                let key = (registration.id.clone(), contact.id.clone());
106                if next.keys().any(|(registration_id, contact_id)| {
107                    contact_id == &contact.id && registration_id != &registration.id
108                }) {
109                    return Err(PackageRejection::malformed());
110                }
111                if contact.active()? {
112                    let uri = contact.uri.ok_or_else(PackageRejection::malformed)?;
113                    if !is_sip_uri(&uri) {
114                        return Err(PackageRejection::malformed());
115                    }
116                    next.insert(
117                        key,
118                        RegistrationPeer {
119                            name: peer_name(&registration.aor),
120                            aor: registration.aor.clone(),
121                            uri,
122                            registration_id: registration.id.clone(),
123                            contact_id: contact.id,
124                            source: self.source.clone(),
125                        },
126                    );
127                    if next.len() > self.contact_limit {
128                        return Err(PackageRejection { status: 413 });
129                    }
130                } else {
131                    next.remove(&key);
132                }
133            }
134        }
135
136        let mut peers: Vec<_> = next.values().cloned().collect();
137        peers.sort_by(|left, right| {
138            (
139                &left.aor,
140                &left.uri,
141                &left.registration_id,
142                &left.contact_id,
143            )
144                .cmp(&(
145                    &right.aor,
146                    &right.uri,
147                    &right.registration_id,
148                    &right.contact_id,
149                ))
150        });
151        self.version = Some(document.version);
152        self.peers = next;
153        Ok(RegistrationSnapshot {
154            version: document.version,
155            peers,
156        })
157    }
158}
159
160impl PackageConsumer for RegistrationConsumer {
161    type Value = RegistrationSnapshot;
162
163    fn event(&self) -> &'static str {
164        "reg"
165    }
166
167    fn accept(&self) -> &[String] {
168        &self.accepts
169    }
170
171    fn neutral(&mut self) -> Option<Self::Value> {
172        None
173    }
174
175    fn empty_terminal_is_valid(&self) -> bool {
176        true
177    }
178
179    fn consume(
180        &mut self,
181        content_type: Option<&[u8]>,
182        body: &[u8],
183    ) -> Result<Self::Value, PackageRejection> {
184        if !content_type.is_some_and(reginfo_content_type) {
185            return Err(PackageRejection::unsupported_media());
186        }
187        let text = std::str::from_utf8(body).map_err(|_| PackageRejection::malformed())?;
188        self.apply(parse(text)?)
189    }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193enum DocumentKind {
194    Full,
195    Partial,
196}
197
198#[derive(Debug)]
199struct Document {
200    version: u32,
201    kind: DocumentKind,
202    registrations: Vec<Registration>,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum RegistrationState {
207    Init,
208    Active,
209    Terminated,
210}
211
212#[derive(Debug)]
213struct Registration {
214    aor: String,
215    id: String,
216    state: RegistrationState,
217    contacts: Vec<ContactChange>,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221enum ContactState {
222    Active,
223    Terminated,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227enum ContactEvent {
228    Registered,
229    Created,
230    Refreshed,
231    Shortened,
232    Expired,
233    Deactivated,
234    Probation,
235    Unregistered,
236    Rejected,
237}
238
239#[derive(Debug)]
240struct ContactChange {
241    id: String,
242    state: ContactState,
243    event: ContactEvent,
244    uri: Option<String>,
245}
246
247impl ContactChange {
248    fn active(&self) -> Result<bool, PackageRejection> {
249        match (self.state, self.event) {
250            (
251                ContactState::Active,
252                ContactEvent::Registered
253                | ContactEvent::Created
254                | ContactEvent::Refreshed
255                | ContactEvent::Shortened,
256            ) => Ok(true),
257            (
258                ContactState::Terminated,
259                ContactEvent::Expired
260                | ContactEvent::Deactivated
261                | ContactEvent::Probation
262                | ContactEvent::Unregistered
263                | ContactEvent::Rejected,
264            ) => Ok(false),
265            _ => Err(PackageRejection::malformed()),
266        }
267    }
268}
269
270#[allow(
271    clippy::too_many_lines,
272    reason = "the XML state stack stays in one fail-closed event table so nesting order is auditable"
273)]
274fn parse(input: &str) -> Result<Document, PackageRejection> {
275    let mut reader = NsReader::from_str(input);
276    reader.config_mut().trim_text(true);
277    let mut document: Option<Document> = None;
278    let mut registration: Option<Registration> = None;
279    let mut contact: Option<ContactChange> = None;
280    let mut in_uri = false;
281    let mut foreign_depth = 0_usize;
282    let mut seen_registrations = HashSet::new();
283    let mut seen_contacts = HashSet::new();
284
285    loop {
286        let (namespace, event) = reader
287            .read_resolved_event()
288            .map_err(|_| PackageRejection::malformed())?;
289        match event {
290            Event::Start(_) if foreign_depth > 0 => {
291                foreign_depth = foreign_depth.saturating_add(1);
292            }
293            Event::End(_) if foreign_depth > 0 => foreign_depth = foreign_depth.saturating_sub(1),
294            _ if foreign_depth > 0 => {}
295            Event::Start(_) if !native(&namespace) => foreign_depth = 1,
296            Event::Start(element) => match element.local_name().as_ref() {
297                b"reginfo" if document.is_none() => {
298                    document = Some(document_start(&reader, &element)?);
299                }
300                b"registration" if document.is_some() && registration.is_none() => {
301                    let parsed = registration_start(&reader, &element)?;
302                    if !seen_registrations.insert(parsed.id.clone()) {
303                        return Err(PackageRejection::malformed());
304                    }
305                    registration = Some(parsed);
306                }
307                b"contact" if registration.is_some() && contact.is_none() => {
308                    let parsed = contact_start(&reader, &element)?;
309                    if !seen_contacts.insert(parsed.id.clone()) {
310                        return Err(PackageRejection::malformed());
311                    }
312                    contact = Some(parsed);
313                }
314                b"uri" if contact.is_some() && !in_uri => in_uri = true,
315                _ => return Err(PackageRejection::malformed()),
316            },
317            Event::Empty(element) if native(&namespace) => match element.local_name().as_ref() {
318                b"reginfo" if document.is_none() => {
319                    document = Some(document_start(&reader, &element)?);
320                }
321                b"registration" if document.is_some() && registration.is_none() => {
322                    let parsed = registration_start(&reader, &element)?;
323                    if !seen_registrations.insert(parsed.id.clone()) {
324                        return Err(PackageRejection::malformed());
325                    }
326                    document
327                        .as_mut()
328                        .ok_or_else(PackageRejection::malformed)?
329                        .registrations
330                        .push(parsed);
331                }
332                b"contact" if registration.is_some() && contact.is_none() => {
333                    let parsed = contact_start(&reader, &element)?;
334                    if !seen_contacts.insert(parsed.id.clone()) {
335                        return Err(PackageRejection::malformed());
336                    }
337                    registration
338                        .as_mut()
339                        .ok_or_else(PackageRejection::malformed)?
340                        .contacts
341                        .push(parsed);
342                }
343                _ => return Err(PackageRejection::malformed()),
344            },
345            Event::End(element) if native(&namespace) => match element.local_name().as_ref() {
346                b"uri" if in_uri => in_uri = false,
347                b"contact" if contact.is_some() && !in_uri => {
348                    registration
349                        .as_mut()
350                        .ok_or_else(PackageRejection::malformed)?
351                        .contacts
352                        .push(contact.take().ok_or_else(PackageRejection::malformed)?);
353                }
354                b"registration" if registration.is_some() && contact.is_none() => {
355                    document
356                        .as_mut()
357                        .ok_or_else(PackageRejection::malformed)?
358                        .registrations
359                        .push(
360                            registration
361                                .take()
362                                .ok_or_else(PackageRejection::malformed)?,
363                        );
364                }
365                b"reginfo" if registration.is_none() && contact.is_none() => {}
366                _ => return Err(PackageRejection::malformed()),
367            },
368            Event::Text(text) if in_uri => append_uri(
369                contact.as_mut().ok_or_else(PackageRejection::malformed)?,
370                &text.decode().map_err(|_| PackageRejection::malformed())?,
371            ),
372            Event::CData(text) if in_uri => append_uri(
373                contact.as_mut().ok_or_else(PackageRejection::malformed)?,
374                &text.decode().map_err(|_| PackageRejection::malformed())?,
375            ),
376            Event::GeneralRef(reference) if in_uri => {
377                let decoded = reference
378                    .decode()
379                    .map_err(|_| PackageRejection::malformed())?;
380                let value = match decoded.as_ref() {
381                    "amp" => "&",
382                    "lt" => "<",
383                    "gt" => ">",
384                    "apos" => "'",
385                    "quot" => "\"",
386                    _ => return Err(PackageRejection::malformed()),
387                };
388                append_uri(
389                    contact.as_mut().ok_or_else(PackageRejection::malformed)?,
390                    value,
391                );
392            }
393            Event::Text(_) | Event::Comment(_) | Event::Decl(_) | Event::Empty(_) => {}
394            Event::DocType(_) | Event::GeneralRef(_) | Event::PI(_) | Event::CData(_) => {
395                return Err(PackageRejection::malformed());
396            }
397            Event::Eof => break,
398            Event::End(_) => return Err(PackageRejection::malformed()),
399        }
400    }
401
402    if registration.is_some() || contact.is_some() || in_uri || foreign_depth != 0 {
403        return Err(PackageRejection::malformed());
404    }
405    document.ok_or_else(PackageRejection::malformed)
406}
407
408fn document_start(
409    reader: &NsReader<&[u8]>,
410    element: &BytesStart<'_>,
411) -> Result<Document, PackageRejection> {
412    let version = required_attribute(reader, element, b"version")?
413        .parse::<u32>()
414        .map_err(|_| PackageRejection::malformed())?;
415    let kind = match required_attribute(reader, element, b"state")?.as_str() {
416        "full" => DocumentKind::Full,
417        "partial" => DocumentKind::Partial,
418        _ => return Err(PackageRejection::malformed()),
419    };
420    Ok(Document {
421        version,
422        kind,
423        registrations: Vec::new(),
424    })
425}
426
427fn registration_start(
428    reader: &NsReader<&[u8]>,
429    element: &BytesStart<'_>,
430) -> Result<Registration, PackageRejection> {
431    let aor = required_attribute(reader, element, b"aor")?;
432    if !is_sip_uri(&aor) {
433        return Err(PackageRejection::malformed());
434    }
435    let id = nonempty(required_attribute(reader, element, b"id")?)?;
436    let state = match required_attribute(reader, element, b"state")?.as_str() {
437        "init" => RegistrationState::Init,
438        "active" => RegistrationState::Active,
439        "terminated" => RegistrationState::Terminated,
440        _ => return Err(PackageRejection::malformed()),
441    };
442    Ok(Registration {
443        aor,
444        id,
445        state,
446        contacts: Vec::new(),
447    })
448}
449
450fn contact_start(
451    reader: &NsReader<&[u8]>,
452    element: &BytesStart<'_>,
453) -> Result<ContactChange, PackageRejection> {
454    let id = nonempty(required_attribute(reader, element, b"id")?)?;
455    let state = match required_attribute(reader, element, b"state")?.as_str() {
456        "active" => ContactState::Active,
457        "terminated" => ContactState::Terminated,
458        _ => return Err(PackageRejection::malformed()),
459    };
460    let event = match required_attribute(reader, element, b"event")?.as_str() {
461        "registered" => ContactEvent::Registered,
462        "created" => ContactEvent::Created,
463        "refreshed" => ContactEvent::Refreshed,
464        "shortened" => ContactEvent::Shortened,
465        "expired" => ContactEvent::Expired,
466        "deactivated" => ContactEvent::Deactivated,
467        "probation" => ContactEvent::Probation,
468        "unregistered" => ContactEvent::Unregistered,
469        "rejected" => ContactEvent::Rejected,
470        _ => return Err(PackageRejection::malformed()),
471    };
472    if event == ContactEvent::Shortened {
473        required_attribute(reader, element, b"expires")?
474            .parse::<u32>()
475            .map_err(|_| PackageRejection::malformed())?;
476    }
477    if event == ContactEvent::Probation {
478        required_attribute(reader, element, b"retry-after")?
479            .parse::<u32>()
480            .map_err(|_| PackageRejection::malformed())?;
481    }
482    Ok(ContactChange {
483        id,
484        state,
485        event,
486        uri: None,
487    })
488}
489
490fn required_attribute(
491    reader: &NsReader<&[u8]>,
492    element: &BytesStart<'_>,
493    name: &[u8],
494) -> Result<String, PackageRejection> {
495    let mut found = None;
496    for attribute in element.attributes() {
497        let attribute = attribute.map_err(|_| PackageRejection::malformed())?;
498        if attribute.key.as_ref() == name {
499            if found.is_some() {
500                return Err(PackageRejection::malformed());
501            }
502            found = Some(
503                attribute
504                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
505                    .map_err(|_| PackageRejection::malformed())?
506                    .into_owned(),
507            );
508        }
509    }
510    found.ok_or_else(PackageRejection::malformed)
511}
512
513fn append_uri(contact: &mut ContactChange, value: &str) {
514    contact.uri.get_or_insert_with(String::new).push_str(value);
515}
516
517fn nonempty(value: String) -> Result<String, PackageRejection> {
518    (!value.is_empty())
519        .then_some(value)
520        .ok_or_else(PackageRejection::malformed)
521}
522
523fn native(namespace: &ResolveResult<'_>) -> bool {
524    matches!(namespace, ResolveResult::Bound(value) if *value == NAMESPACE)
525}
526
527fn reginfo_content_type(value: &[u8]) -> bool {
528    value
529        .split(|byte| *byte == b';')
530        .next()
531        .is_some_and(|media| {
532            media
533                .trim_ascii()
534                .eq_ignore_ascii_case(REGINFO_TYPE.as_bytes())
535        })
536}
537
538fn is_sip_uri(value: &str) -> bool {
539    Uri::parse(Bytes::copy_from_slice(value.as_bytes())).is_ok_and(|uri| uri.scheme().is_sip())
540}
541
542fn peer_name(aor: &str) -> String {
543    Uri::parse(Bytes::copy_from_slice(aor.as_bytes()))
544        .ok()
545        .and_then(|uri| uri.decoded_user())
546        .filter(|user| !user.is_empty())
547        .map_or_else(
548            || aor.to_owned(),
549            |user| String::from_utf8_lossy(&user).into_owned(),
550        )
551}