1use 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#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct RegistrarSource {
23 pub resource: String,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct RegistrationPeer {
30 pub name: String,
32 pub aor: String,
34 pub uri: String,
36 pub registration_id: String,
38 pub contact_id: String,
40 pub source: RegistrarSource,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct RegistrationSnapshot {
47 pub version: u32,
49 pub peers: Vec<RegistrationPeer>,
51}
52
53#[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 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 != ®istration.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 != ®istration.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(®istration.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}