1use std::fmt;
9
10use percent_encoding::percent_decode_str;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use super::xml::{self as phone_xml, PhoneXmlError};
15use crate::types::{ApplicationId, CallReference, LineInstance, TransactionId};
16
17pub const MAX_PHONE_SERVICE_DATA_BYTES: usize = 2_000;
19pub const MAX_PHONE_SERVICE_RESPONSE_ITEMS: usize = 3;
21pub const MAX_PHONE_SERVICE_ROUTE_SEGMENTS: usize = 32;
23pub const MAX_PHONE_SERVICE_ROUTE_COMPONENT_BYTES: usize = 1_024;
25pub const MAX_PHONE_SERVICE_SUBMITTED_VALUES: usize = 32;
27pub const MAX_PHONE_SERVICE_PARAMETER_NAME_BYTES: usize = 128;
29pub const MAX_PHONE_SERVICE_PARAMETER_VALUE_BYTES: usize = 1_024;
31pub const MAX_PHONE_SERVICE_RESPONSE_DATA_CHARS: usize = 256;
33pub const MAX_PHONE_SERVICE_RESPONSE_URL_CHARS: usize = 256;
35pub const MAX_PHONE_SERVICE_ERROR_MESSAGE_CHARS: usize = 256;
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum PhoneServiceMessageKind {
41 Data,
43 Response,
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct PhoneServiceRouting {
51 pub application_id: ApplicationId,
52 pub line_instance: LineInstance,
53 pub call_reference: CallReference,
54 pub transaction_id: TransactionId,
55}
56
57#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub struct PhoneServiceExtendedRouting {
60 pub sequence_flag: u32,
62 pub display_priority: u32,
64 pub conference_id: u32,
66 pub application_instance_id: u32,
68 pub routing: u32,
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct PhoneServiceEvent {
75 pub kind: PhoneServiceMessageKind,
77 pub routing: PhoneServiceRouting,
78 pub extended: Option<PhoneServiceExtendedRouting>,
80 pub payload: PhoneServicePayload,
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
86pub enum PhoneServicePayload {
87 ExecuteResponse(CiscoIpPhoneResponse),
88 Error(CiscoIpPhoneError),
89 Submission(PhoneServiceSubmission),
90 Opaque(Vec<u8>),
93}
94
95#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
97#[serde(rename = "CiscoIPPhoneResponse")]
98pub struct CiscoIpPhoneResponse {
99 #[serde(rename = "ResponseItem", default)]
100 pub items: Vec<CiscoIpPhoneResponseItem>,
101}
102
103impl CiscoIpPhoneResponse {
104 pub fn validate(&self) -> Result<(), PhoneXmlError> {
106 if self.items.len() > MAX_PHONE_SERVICE_RESPONSE_ITEMS {
107 return Err(PhoneXmlError::LimitExceeded {
108 kind: "phone-service response items",
109 actual: self.items.len(),
110 maximum: MAX_PHONE_SERVICE_RESPONSE_ITEMS,
111 });
112 }
113 for item in &self.items {
114 validate_response_text(
115 "phone-service response data",
116 &item.data,
117 MAX_PHONE_SERVICE_RESPONSE_DATA_CHARS,
118 )?;
119 validate_response_text(
120 "phone-service response URL",
121 &item.url,
122 MAX_PHONE_SERVICE_RESPONSE_URL_CHARS,
123 )?;
124 }
125 Ok(())
126 }
127}
128
129#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
131#[serde(transparent)]
132pub struct PhoneExecuteStatus(pub u32);
133
134impl PhoneExecuteStatus {
135 pub const OK: Self = Self(0);
136 pub const ERROR: Self = Self(1);
137 pub const URI_NOT_FOUND: Self = Self(4);
138 pub const NO_ACTIVE_CALL: Self = Self(6);
139
140 pub const fn get(self) -> u32 {
141 self.0
142 }
143}
144
145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
147pub struct CiscoIpPhoneResponseItem {
148 #[serde(rename = "@Status", alias = "Status")]
149 pub status: PhoneExecuteStatus,
150 #[serde(rename = "@Data", alias = "Data")]
151 pub data: String,
153 #[serde(rename = "@URL", alias = "URL")]
154 pub url: String,
156}
157
158#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
160#[serde(transparent)]
161pub struct PhoneServiceErrorCode(pub u32);
162
163impl PhoneServiceErrorCode {
164 pub const PARSING: Self = Self(1);
165 pub const FRAMING: Self = Self(2);
166 pub const INTERNAL_FILE: Self = Self(3);
167 pub const AUTHENTICATION: Self = Self(4);
168
169 pub const fn get(self) -> u32 {
170 self.0
171 }
172}
173
174#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
176#[serde(rename = "CiscoIPPhoneError")]
177pub struct CiscoIpPhoneError {
178 #[serde(rename = "@Number", alias = "Number")]
179 pub number: PhoneServiceErrorCode,
180 #[serde(rename = "$text", default, skip_serializing_if = "String::is_empty")]
181 pub message: String,
182}
183
184impl CiscoIpPhoneError {
185 pub fn validate(&self) -> Result<(), PhoneXmlError> {
187 validate_response_text(
188 "phone-service error message",
189 &self.message,
190 MAX_PHONE_SERVICE_ERROR_MESSAGE_CHARS,
191 )
192 }
193}
194
195#[derive(Clone, Debug, Eq, PartialEq)]
197pub struct PhoneServiceSubmission {
198 pub route: Vec<String>,
200 pub values: Vec<PhoneServiceSubmittedValue>,
202}
203
204impl PhoneServiceSubmission {
205 pub fn values_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
207 self.values
208 .iter()
209 .filter(move |value| value.name == name)
210 .map(|value| value.value.as_str())
211 }
212}
213
214#[derive(Clone, Eq, PartialEq)]
216pub struct PhoneServiceSubmittedValue {
217 pub name: String,
218 pub value: String,
220}
221
222impl fmt::Debug for PhoneServiceSubmittedValue {
223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224 formatter
225 .debug_struct("PhoneServiceSubmittedValue")
226 .field("name", &self.name)
227 .field("value", &"<redacted>")
228 .finish()
229 }
230}
231
232#[derive(Debug, Error)]
234pub enum PhoneServiceError {
235 #[error(transparent)]
237 Xml(#[from] PhoneXmlError),
238 #[error("phone-service submission is not valid UTF-8")]
239 InvalidUtf8,
240 #[error("phone-service submission contains invalid percent encoding")]
241 InvalidPercentEncoding,
242 #[error("phone-service submission contains a forbidden control character")]
243 ControlCharacter,
244 #[error("phone-service submission route has {actual} components; maximum is {maximum}")]
246 TooManyRouteSegments { actual: usize, maximum: usize },
247 #[error("phone-service submission has {actual} values; maximum is {maximum}")]
249 TooManyValues { actual: usize, maximum: usize },
250 #[error("phone-service submission has an empty parameter name")]
251 EmptyParameterName,
252 #[error("phone-service submission has an empty route component")]
253 EmptyRouteComponent,
254 #[error("phone-service submission {kind} has {actual} bytes; maximum is {maximum}")]
256 ComponentTooLong {
257 kind: &'static str,
258 actual: usize,
259 maximum: usize,
260 },
261}
262
263#[derive(Deserialize)]
264enum PhoneServiceXmlSchema {
265 #[serde(rename = "CiscoIPPhoneResponse")]
266 Response {
267 #[serde(rename = "ResponseItem", default)]
268 items: Vec<CiscoIpPhoneResponseItem>,
269 },
270 #[serde(rename = "CiscoIPPhoneError")]
271 Error {
272 #[serde(rename = "@Number", alias = "Number")]
273 number: PhoneServiceErrorCode,
274 #[serde(rename = "$text", default)]
275 message: String,
276 },
277 #[serde(other)]
278 Unknown,
279}
280
281pub fn parse_phone_service_payload(
285 data: &[u8],
286 kind: PhoneServiceMessageKind,
287) -> Result<PhoneServicePayload, PhoneServiceError> {
288 if data.len() > MAX_PHONE_SERVICE_DATA_BYTES {
289 return Err(PhoneXmlError::LimitExceeded {
290 kind: "phone-service data",
291 actual: data.len(),
292 maximum: MAX_PHONE_SERVICE_DATA_BYTES,
293 }
294 .into());
295 }
296 let trimmed = trim_protocol_padding(data);
297 if trimmed.is_empty() {
298 return Ok(PhoneServicePayload::Opaque(data.to_vec()));
299 }
300
301 if trimmed.first() == Some(&b'<') {
302 return match phone_xml::from_bytes::<PhoneServiceXmlSchema>(
303 trimmed,
304 MAX_PHONE_SERVICE_DATA_BYTES,
305 )? {
306 PhoneServiceXmlSchema::Response { items } => {
307 let response = CiscoIpPhoneResponse { items };
308 response.validate()?;
309 Ok(PhoneServicePayload::ExecuteResponse(response))
310 }
311 PhoneServiceXmlSchema::Error { number, message } => {
312 let error = CiscoIpPhoneError { number, message };
313 error.validate()?;
314 Ok(PhoneServicePayload::Error(error))
315 }
316 PhoneServiceXmlSchema::Unknown => Ok(PhoneServicePayload::Opaque(data.to_vec())),
317 };
318 }
319
320 if kind == PhoneServiceMessageKind::Response {
321 return Ok(PhoneServicePayload::Opaque(data.to_vec()));
322 }
323 let Ok(text) = std::str::from_utf8(trimmed) else {
324 return Ok(PhoneServicePayload::Opaque(data.to_vec()));
325 };
326 parse_submission(text).map(PhoneServicePayload::Submission)
327}
328
329fn validate_response_text(
330 field: &'static str,
331 value: &str,
332 maximum: usize,
333) -> Result<(), PhoneXmlError> {
334 if value.chars().count() > maximum {
335 Err(PhoneXmlError::InvalidField {
336 field,
337 expected: "within the documented phone-service text bound",
338 })
339 } else {
340 Ok(())
341 }
342}
343
344fn parse_submission(text: &str) -> Result<PhoneServiceSubmission, PhoneServiceError> {
345 if text.chars().any(char::is_control) || text.contains('#') {
346 return Err(PhoneServiceError::ControlCharacter);
347 }
348 let (route, query) = text
349 .split_once('?')
350 .map_or((text, None), |(route, query)| (route, Some(query)));
351 let route = if route.is_empty() {
352 Vec::new()
353 } else {
354 route
355 .split('/')
356 .map(|component| {
357 decode_component(
358 component,
359 "route component",
360 MAX_PHONE_SERVICE_ROUTE_COMPONENT_BYTES,
361 )
362 })
363 .collect::<Result<Vec<_>, _>>()?
364 };
365 if route.len() > MAX_PHONE_SERVICE_ROUTE_SEGMENTS {
366 return Err(PhoneServiceError::TooManyRouteSegments {
367 actual: route.len(),
368 maximum: MAX_PHONE_SERVICE_ROUTE_SEGMENTS,
369 });
370 }
371 let values = query.map_or_else(|| Ok(Vec::new()), parse_form_values)?;
372 Ok(PhoneServiceSubmission { route, values })
373}
374
375fn parse_form_values(query: &str) -> Result<Vec<PhoneServiceSubmittedValue>, PhoneServiceError> {
376 if query.is_empty() {
377 return Ok(Vec::new());
378 }
379 for field in query.split('&') {
380 if field.is_empty() {
381 return Err(PhoneServiceError::EmptyParameterName);
382 }
383 let (name, value) = field.split_once('=').unwrap_or((field, ""));
384 validate_encoded_component(name)?;
385 validate_encoded_component(value)?;
386 percent_decode_str(name)
387 .decode_utf8()
388 .map_err(|_| PhoneServiceError::InvalidUtf8)?;
389 percent_decode_str(value)
390 .decode_utf8()
391 .map_err(|_| PhoneServiceError::InvalidUtf8)?;
392 }
393
394 let values = form_urlencoded::parse(query.as_bytes())
395 .map(|(name, value)| {
396 if name.is_empty() {
397 return Err(PhoneServiceError::EmptyParameterName);
398 }
399 validate_component_length(
400 "parameter name",
401 &name,
402 MAX_PHONE_SERVICE_PARAMETER_NAME_BYTES,
403 )?;
404 validate_component_length(
405 "parameter value",
406 &value,
407 MAX_PHONE_SERVICE_PARAMETER_VALUE_BYTES,
408 )?;
409 reject_decoded_controls(&name)?;
410 reject_decoded_controls(&value)?;
411 Ok(PhoneServiceSubmittedValue {
412 name: name.into_owned(),
413 value: value.into_owned(),
414 })
415 })
416 .collect::<Result<Vec<_>, _>>()?;
417 if values.len() > MAX_PHONE_SERVICE_SUBMITTED_VALUES {
418 return Err(PhoneServiceError::TooManyValues {
419 actual: values.len(),
420 maximum: MAX_PHONE_SERVICE_SUBMITTED_VALUES,
421 });
422 }
423 Ok(values)
424}
425
426fn decode_component(
427 encoded: &str,
428 kind: &'static str,
429 maximum: usize,
430) -> Result<String, PhoneServiceError> {
431 if encoded.is_empty() {
432 return Err(PhoneServiceError::EmptyRouteComponent);
433 }
434 validate_encoded_component(encoded)?;
435 let decoded = percent_decode_str(encoded)
436 .decode_utf8()
437 .map_err(|_| PhoneServiceError::InvalidUtf8)?;
438 validate_component_length(kind, &decoded, maximum)?;
439 reject_decoded_controls(&decoded)?;
440 Ok(decoded.into_owned())
441}
442
443fn validate_encoded_component(encoded: &str) -> Result<(), PhoneServiceError> {
444 let bytes = encoded.as_bytes();
445 let mut index = 0;
446 while index < bytes.len() {
447 if bytes[index] == b'%' {
448 let Some(pair) = bytes.get(index + 1..index + 3) else {
449 return Err(PhoneServiceError::InvalidPercentEncoding);
450 };
451 if !pair.iter().all(u8::is_ascii_hexdigit) {
452 return Err(PhoneServiceError::InvalidPercentEncoding);
453 }
454 index += 3;
455 } else {
456 index += 1;
457 }
458 }
459 Ok(())
460}
461
462fn validate_component_length(
463 kind: &'static str,
464 component: &str,
465 maximum: usize,
466) -> Result<(), PhoneServiceError> {
467 if component.len() > maximum {
468 return Err(PhoneServiceError::ComponentTooLong {
469 kind,
470 actual: component.len(),
471 maximum,
472 });
473 }
474 Ok(())
475}
476
477fn reject_decoded_controls(component: &str) -> Result<(), PhoneServiceError> {
478 if component.chars().any(char::is_control) {
479 Err(PhoneServiceError::ControlCharacter)
480 } else {
481 Ok(())
482 }
483}
484
485fn trim_protocol_padding(mut data: &[u8]) -> &[u8] {
486 while data.first().is_some_and(|byte| byte.is_ascii_whitespace()) {
487 data = &data[1..];
488 }
489 while data
490 .last()
491 .is_some_and(|byte| *byte == 0 || byte.is_ascii_whitespace())
492 {
493 data = &data[..data.len() - 1];
494 }
495 data
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn typed_execute_responses_and_errors_use_the_xml_boundary() {
504 let payload = parse_phone_service_payload(
505 br#"<CiscoIPPhoneResponse><ResponseItem Status="0" Data="Taylor & Co" URL="Play:chime.raw"/><ResponseItem Status="6" Data="No Active Call" URL="SendDigits:12"/></CiscoIPPhoneResponse>"#,
506 PhoneServiceMessageKind::Response,
507 )
508 .unwrap();
509 let PhoneServicePayload::ExecuteResponse(response) = payload else {
510 panic!("expected typed execute response");
511 };
512 assert_eq!(response.items.len(), 2);
513 assert_eq!(response.items[0].status, PhoneExecuteStatus::OK);
514 assert_eq!(response.items[0].data, "Taylor & Co");
515 assert_eq!(response.items[1].status, PhoneExecuteStatus::NO_ACTIVE_CALL);
516
517 assert_eq!(
518 parse_phone_service_payload(
519 br#"<CiscoIPPhoneError Number="4">Authentication failed</CiscoIPPhoneError>"#,
520 PhoneServiceMessageKind::Response,
521 )
522 .unwrap(),
523 PhoneServicePayload::Error(CiscoIpPhoneError {
524 number: PhoneServiceErrorCode::AUTHENTICATION,
525 message: "Authentication failed".into(),
526 })
527 );
528 }
529
530 #[test]
531 fn submitted_route_and_form_values_are_decoded_in_order() {
532 let payload = parse_phone_service_payload(
533 b"invite/desk%20one?NUMBER=555%2A12&NAME=Fran%C3%A7ois&NOTE=a+b&NOTE=second",
534 PhoneServiceMessageKind::Data,
535 )
536 .unwrap();
537 let PhoneServicePayload::Submission(submission) = payload else {
538 panic!("expected typed submission");
539 };
540 assert_eq!(submission.route, ["invite", "desk one"]);
541 assert_eq!(
542 submission
543 .values
544 .iter()
545 .map(|value| (value.name.as_str(), value.value.as_str()))
546 .collect::<Vec<_>>(),
547 [
548 ("NUMBER", "555*12"),
549 ("NAME", "François"),
550 ("NOTE", "a b"),
551 ("NOTE", "second"),
552 ]
553 );
554 assert_eq!(
555 submission.values_named("NOTE").collect::<Vec<_>>(),
556 ["a b", "second"]
557 );
558 let debug = format!("{:?}", submission.values[0]);
559 assert!(debug.contains("<redacted>"));
560 assert!(!debug.contains("555"));
561 }
562
563 #[test]
564 fn opaque_payloads_are_reserved_for_unknown_schemas() {
565 let unknown = b"<VendorPhoneResult><Value>one</Value></VendorPhoneResult>";
566 assert_eq!(
567 parse_phone_service_payload(unknown, PhoneServiceMessageKind::Response).unwrap(),
568 PhoneServicePayload::Opaque(unknown.to_vec())
569 );
570 assert_eq!(
571 parse_phone_service_payload(b"Success", PhoneServiceMessageKind::Response).unwrap(),
572 PhoneServicePayload::Opaque(b"Success".to_vec())
573 );
574 assert_eq!(
575 parse_phone_service_payload(&[0xff, 0x00], PhoneServiceMessageKind::Data).unwrap(),
576 PhoneServicePayload::Opaque(vec![0xff, 0x00])
577 );
578 }
579
580 #[test]
581 fn malformed_xml_encoding_and_bounds_fail_closed_without_values_in_errors() {
582 for malformed in [
583 b"<CiscoIPPhoneResponse>".as_slice(),
584 b"<!DOCTYPE x><CiscoIPPhoneResponse/>".as_slice(),
585 b"<VendorPhoneResult>".as_slice(),
586 ] {
587 assert!(
588 parse_phone_service_payload(malformed, PhoneServiceMessageKind::Response).is_err()
589 );
590 }
591
592 for malformed in [
593 "invite?PIN=secret%",
594 "invite?PIN=secret%GG",
595 "invite?PIN=%FF",
596 "invite?PIN=secret%0Avalue",
597 "invite?=secret",
598 "invite//desk?PIN=secret",
599 "invite?PIN=secret#fragment",
600 ] {
601 let error =
602 parse_phone_service_payload(malformed.as_bytes(), PhoneServiceMessageKind::Data)
603 .unwrap_err()
604 .to_string();
605 assert!(!error.contains("secret"), "{error}");
606 }
607
608 assert!(
609 parse_phone_service_payload(
610 &vec![b'x'; MAX_PHONE_SERVICE_DATA_BYTES + 1],
611 PhoneServiceMessageKind::Data,
612 )
613 .is_err()
614 );
615 let too_many = format!(
616 "route?{}",
617 (0..=MAX_PHONE_SERVICE_SUBMITTED_VALUES)
618 .map(|index| format!("p{index}=x"))
619 .collect::<Vec<_>>()
620 .join("&")
621 );
622 assert!(
623 parse_phone_service_payload(too_many.as_bytes(), PhoneServiceMessageKind::Data,)
624 .is_err()
625 );
626 let too_many_items = format!(
627 "<CiscoIPPhoneResponse>{}</CiscoIPPhoneResponse>",
628 r#"<ResponseItem Status="0" Data="ok" URL="Init:Services"/>"#
629 .repeat(MAX_PHONE_SERVICE_RESPONSE_ITEMS + 1)
630 );
631 assert!(
632 parse_phone_service_payload(
633 too_many_items.as_bytes(),
634 PhoneServiceMessageKind::Response,
635 )
636 .is_err()
637 );
638 }
639
640 #[test]
641 fn every_submission_collection_and_component_bound_is_enforced() {
642 let too_many_route_segments =
643 std::iter::repeat_n("x", MAX_PHONE_SERVICE_ROUTE_SEGMENTS + 1)
644 .collect::<Vec<_>>()
645 .join("/");
646 assert!(matches!(
647 parse_phone_service_payload(
648 too_many_route_segments.as_bytes(),
649 PhoneServiceMessageKind::Data,
650 ),
651 Err(PhoneServiceError::TooManyRouteSegments { .. })
652 ));
653
654 let long_route = "x".repeat(MAX_PHONE_SERVICE_ROUTE_COMPONENT_BYTES + 1);
655 assert!(matches!(
656 parse_phone_service_payload(long_route.as_bytes(), PhoneServiceMessageKind::Data),
657 Err(PhoneServiceError::ComponentTooLong {
658 kind: "route component",
659 ..
660 })
661 ));
662
663 let long_name = format!(
664 "route?{}=value",
665 "n".repeat(MAX_PHONE_SERVICE_PARAMETER_NAME_BYTES + 1)
666 );
667 assert!(matches!(
668 parse_phone_service_payload(long_name.as_bytes(), PhoneServiceMessageKind::Data),
669 Err(PhoneServiceError::ComponentTooLong {
670 kind: "parameter name",
671 ..
672 })
673 ));
674
675 let long_value = format!(
676 "route?name={}",
677 "v".repeat(MAX_PHONE_SERVICE_PARAMETER_VALUE_BYTES + 1)
678 );
679 assert!(matches!(
680 parse_phone_service_payload(long_value.as_bytes(), PhoneServiceMessageKind::Data),
681 Err(PhoneServiceError::ComponentTooLong {
682 kind: "parameter value",
683 ..
684 })
685 ));
686 }
687}