1use std::fmt;
9use std::net::IpAddr;
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13
14use super::validation::{text_length_is_within, validate_count};
15use super::xml::{PhoneXmlError, from_bytes, to_string, to_writer as write_document};
16
17pub const PROVISIONING_MAX_BYTES: usize = 128 * 1_024;
19pub const PROVISIONING_MAX_CALL_MANAGERS: usize = 5;
21pub const PROVISIONING_MAX_NTP_SERVERS: usize = 5;
23pub const PROVISIONING_MAX_FIRMWARE_LOADS: usize = 8;
25const PROVISIONING_MAX_TEXT_CHARS: usize = 256;
26
27#[derive(Clone, Eq, PartialEq, Deserialize, Serialize)]
29#[serde(transparent)]
30pub struct ProvisioningSecret(String);
31
32impl ProvisioningSecret {
33 pub fn new(value: impl Into<String>) -> Result<Self, PhoneXmlError> {
35 let value = value.into();
36 validate_text(
37 "provisioning secret",
38 &value,
39 1,
40 PROVISIONING_MAX_TEXT_CHARS,
41 )?;
42 Ok(Self(value))
43 }
44
45 pub fn expose(&self) -> &str {
47 &self.0
48 }
49}
50
51impl fmt::Debug for ProvisioningSecret {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str("ProvisioningSecret(<redacted>)")
54 }
55}
56
57impl TryFrom<String> for ProvisioningSecret {
58 type Error = PhoneXmlError;
59
60 fn try_from(value: String) -> Result<Self, Self::Error> {
61 Self::new(value)
62 }
63}
64
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
67pub enum ProvisioningProtocol {
68 #[default]
69 #[serde(rename = "SCCP")]
70 Sccp,
71}
72
73#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
75pub enum ProvisioningTransport {
76 #[default]
77 #[serde(rename = "TCP")]
78 Clear,
79 #[serde(rename = "TLS")]
80 Tls,
81}
82
83#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
85pub enum ProvisioningBoolean {
86 #[default]
88 #[serde(rename = "0")]
89 Disabled,
90 #[serde(rename = "1")]
92 Enabled,
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct ProvisioningHost(String);
98
99impl ProvisioningHost {
100 pub fn new(value: impl Into<String>) -> Result<Self, PhoneXmlError> {
102 let value = value.into();
103 validate_host(&value)?;
104 Ok(Self(value))
105 }
106
107 pub fn as_str(&self) -> &str {
108 &self.0
109 }
110}
111
112impl AsRef<str> for ProvisioningHost {
113 fn as_ref(&self) -> &str {
114 self.as_str()
115 }
116}
117
118impl TryFrom<String> for ProvisioningHost {
119 type Error = PhoneXmlError;
120
121 fn try_from(value: String) -> Result<Self, Self::Error> {
122 Self::new(value)
123 }
124}
125
126impl FromStr for ProvisioningHost {
127 type Err = PhoneXmlError;
128
129 fn from_str(value: &str) -> Result<Self, Self::Err> {
130 Self::new(value)
131 }
132}
133
134impl Serialize for ProvisioningHost {
135 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136 where
137 S: serde::Serializer,
138 {
139 serializer.serialize_str(&self.0)
140 }
141}
142
143impl<'de> Deserialize<'de> for ProvisioningHost {
144 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145 where
146 D: serde::Deserializer<'de>,
147 {
148 let value = String::deserialize(deserializer)?;
149 Self::new(value).map_err(serde::de::Error::custom)
150 }
151}
152
153#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
155#[serde(deny_unknown_fields)]
156pub struct ProvisioningPorts {
157 #[serde(rename = "ethernetPhonePort")]
158 pub signaling: u16,
160 #[serde(
161 rename = "securedEthernetPhonePort",
162 default,
163 skip_serializing_if = "Option::is_none"
164 )]
165 pub secure_signaling: Option<u16>,
167}
168
169#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
171#[serde(deny_unknown_fields)]
172pub struct ProvisioningCallManager {
173 #[serde(rename = "processNodeName")]
174 pub host: ProvisioningHost,
175 pub ports: ProvisioningPorts,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
180#[serde(deny_unknown_fields)]
181pub struct ProvisioningCallManagerMember {
182 #[serde(rename = "@priority")]
183 pub priority: u8,
185 #[serde(rename = "callManager")]
186 pub call_manager: ProvisioningCallManager,
187}
188
189#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
191#[serde(deny_unknown_fields)]
192pub struct ProvisioningCallManagerMembers {
193 #[serde(rename = "member", default)]
194 pub entries: Vec<ProvisioningCallManagerMember>,
195}
196
197#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
199#[serde(deny_unknown_fields)]
200pub struct ProvisioningCallManagerGroup {
201 pub members: ProvisioningCallManagerMembers,
202}
203
204#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
206#[serde(deny_unknown_fields)]
207pub struct ProvisioningNtpServer {
208 pub name: ProvisioningHost,
209 #[serde(rename = "ntpMode", default)]
210 pub mode: ProvisioningNtpMode,
211}
212
213#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
215pub enum ProvisioningNtpMode {
216 #[default]
217 Unicast,
218 DirectedBroadcast,
219}
220
221#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
223#[serde(deny_unknown_fields)]
224pub struct ProvisioningNtpServers {
225 #[serde(rename = "ntp", default)]
226 pub entries: Vec<ProvisioningNtpServer>,
227}
228
229#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
231#[serde(deny_unknown_fields)]
232pub struct ProvisioningDateTime {
233 #[serde(rename = "dateTemplate")]
234 pub date_template: String,
236 #[serde(rename = "timeZone")]
237 pub time_zone: String,
239 pub ntps: ProvisioningNtpServers,
240}
241
242#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
244#[serde(deny_unknown_fields)]
245pub struct ProvisioningDevicePool {
246 #[serde(rename = "dateTimeSetting")]
247 pub date_time: ProvisioningDateTime,
248 #[serde(rename = "callManagerGroup")]
249 pub call_managers: ProvisioningCallManagerGroup,
250}
251
252#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
254#[serde(deny_unknown_fields)]
255pub struct ProvisioningLocales {
256 #[serde(
257 rename = "userLocale",
258 default,
259 skip_serializing_if = "Option::is_none"
260 )]
261 pub user: Option<String>,
262 #[serde(
263 rename = "networkLocale",
264 default,
265 skip_serializing_if = "Option::is_none"
266 )]
267 pub network: Option<String>,
268 #[serde(
269 rename = "userLocaleVersion",
270 default,
271 skip_serializing_if = "Option::is_none"
272 )]
273 pub user_version: Option<String>,
274 #[serde(
275 rename = "networkLocaleVersion",
276 default,
277 skip_serializing_if = "Option::is_none"
278 )]
279 pub network_version: Option<String>,
280}
281
282#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
284#[serde(deny_unknown_fields)]
285pub struct ProvisioningServiceUrls {
286 #[serde(
287 rename = "authenticationURL",
288 default,
289 skip_serializing_if = "Option::is_none"
290 )]
291 pub authentication: Option<String>,
292 #[serde(
293 rename = "directoryURL",
294 default,
295 skip_serializing_if = "Option::is_none"
296 )]
297 pub directory: Option<String>,
298 #[serde(rename = "idleURL", default, skip_serializing_if = "Option::is_none")]
299 pub idle: Option<String>,
300 #[serde(
301 rename = "informationURL",
302 default,
303 skip_serializing_if = "Option::is_none"
304 )]
305 pub information: Option<String>,
306 #[serde(
307 rename = "messagesURL",
308 default,
309 skip_serializing_if = "Option::is_none"
310 )]
311 pub messages: Option<String>,
312 #[serde(
313 rename = "proxyServerURL",
314 default,
315 skip_serializing_if = "Option::is_none"
316 )]
317 pub proxy: Option<String>,
318 #[serde(
319 rename = "servicesURL",
320 default,
321 skip_serializing_if = "Option::is_none"
322 )]
323 pub services: Option<String>,
324}
325
326#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
328#[serde(deny_unknown_fields)]
329pub struct ProvisioningDscp {
330 #[serde(
331 rename = "dscpForCallControl",
332 default,
333 skip_serializing_if = "Option::is_none"
334 )]
335 pub signaling: Option<u8>,
337 #[serde(
338 rename = "dscpForAudio",
339 default,
340 skip_serializing_if = "Option::is_none"
341 )]
342 pub audio: Option<u8>,
344 #[serde(
345 rename = "dscpForVideo",
346 default,
347 skip_serializing_if = "Option::is_none"
348 )]
349 pub video: Option<u8>,
351}
352
353#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
355#[serde(deny_unknown_fields)]
356pub struct ProvisioningFeatures {
357 #[serde(
358 rename = "enblocDialing",
359 default,
360 skip_serializing_if = "Option::is_none"
361 )]
362 pub enbloc_dialing: Option<ProvisioningBoolean>,
363 #[serde(
364 rename = "dndControl",
365 default,
366 skip_serializing_if = "Option::is_none"
367 )]
368 pub do_not_disturb: Option<ProvisioningBoolean>,
369 #[serde(
370 rename = "joinAcrossLines",
371 default,
372 skip_serializing_if = "Option::is_none"
373 )]
374 pub join_across_lines: Option<ProvisioningBoolean>,
375 #[serde(
376 rename = "callPickup",
377 default,
378 skip_serializing_if = "Option::is_none"
379 )]
380 pub call_pickup: Option<ProvisioningBoolean>,
381 #[serde(rename = "barge", default, skip_serializing_if = "Option::is_none")]
382 pub barge: Option<ProvisioningBoolean>,
383 #[serde(rename = "blf", default, skip_serializing_if = "Option::is_none")]
384 pub blf: Option<ProvisioningBoolean>,
385 #[serde(rename = "mwi", default, skip_serializing_if = "Option::is_none")]
386 pub mwi: Option<ProvisioningBoolean>,
387 #[serde(rename = "mobility", default, skip_serializing_if = "Option::is_none")]
388 pub mobility: Option<ProvisioningBoolean>,
389}
390
391#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
393#[serde(deny_unknown_fields)]
394pub struct ProvisioningCodecPolicy {
395 #[serde(
396 rename = "preferredCodec",
397 default,
398 skip_serializing_if = "Option::is_none"
399 )]
400 pub preferred: Option<String>,
401 #[serde(
402 rename = "g722CodecSupport",
403 default,
404 skip_serializing_if = "Option::is_none"
405 )]
406 pub g722: Option<ProvisioningBoolean>,
407 #[serde(
408 rename = "g729CodecSupport",
409 default,
410 skip_serializing_if = "Option::is_none"
411 )]
412 pub g729: Option<ProvisioningBoolean>,
413}
414
415#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
417#[serde(deny_unknown_fields)]
418pub struct ProvisioningCapf {
419 #[serde(rename = "phonePort")]
420 pub phone_port: u16,
422 #[serde(rename = "processNodeName")]
423 pub host: ProvisioningHost,
424 #[serde(
425 rename = "authenticationMode",
426 default,
427 skip_serializing_if = "Option::is_none"
428 )]
429 pub authentication_mode: Option<String>,
430 #[serde(
431 rename = "authenticationToken",
432 default,
433 skip_serializing_if = "Option::is_none"
434 )]
435 pub authentication_token: Option<ProvisioningSecret>,
437 #[serde(
438 rename = "certificateOperation",
439 default,
440 skip_serializing_if = "Option::is_none"
441 )]
442 pub certificate_operation: Option<String>,
443}
444
445#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
447#[serde(deny_unknown_fields)]
448pub struct ProvisioningSecurity {
449 #[serde(rename = "transportLayerProtocol", default)]
450 pub transport: ProvisioningTransport,
452 #[serde(
453 rename = "deviceSecurityMode",
454 default,
455 skip_serializing_if = "Option::is_none"
456 )]
457 pub device_security_mode: Option<u8>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub capf: Option<ProvisioningCapf>,
461 #[serde(
462 rename = "encryptedConfig",
463 default,
464 skip_serializing_if = "Option::is_none"
465 )]
466 pub encrypted_config: Option<ProvisioningSecret>,
468 #[serde(rename = "trustList", default, skip_serializing_if = "Option::is_none")]
469 pub trust_list: Option<ProvisioningSecret>,
471}
472
473#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
475#[serde(deny_unknown_fields)]
476pub struct ProvisioningFirmwareLoad {
477 #[serde(rename = "@model", default, skip_serializing_if = "Option::is_none")]
478 pub model: Option<String>,
480 #[serde(rename = "$text")]
481 pub file: String,
482}
483
484#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
486#[serde(deny_unknown_fields)]
487pub struct ProvisioningVendorConfig {
488 #[serde(default)]
489 pub dscp: ProvisioningDscp,
490 #[serde(default)]
491 pub codecs: ProvisioningCodecPolicy,
492 #[serde(default)]
493 pub features: ProvisioningFeatures,
494}
495
496#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
498#[serde(rename = "device", deny_unknown_fields)]
499pub struct DeviceProvisioning {
500 #[serde(rename = "deviceProtocol", default)]
501 pub protocol: ProvisioningProtocol,
502 #[serde(rename = "devicePool")]
503 pub device_pool: ProvisioningDevicePool,
504 #[serde(default)]
505 pub locales: ProvisioningLocales,
506 #[serde(rename = "phoneServices", default)]
507 pub service_urls: ProvisioningServiceUrls,
508 #[serde(rename = "securityProfile", default)]
509 pub security: ProvisioningSecurity,
510 #[serde(rename = "vendorConfig", default)]
511 pub vendor: ProvisioningVendorConfig,
512 #[serde(rename = "loadInformation", default)]
513 pub firmware_loads: Vec<ProvisioningFirmwareLoad>,
515 #[serde(rename = "sshUserId", default, skip_serializing_if = "Option::is_none")]
516 pub ssh_user_id: Option<String>,
518 #[serde(
519 rename = "sshPassword",
520 default,
521 skip_serializing_if = "Option::is_none"
522 )]
523 pub ssh_password: Option<ProvisioningSecret>,
525}
526
527impl DeviceProvisioning {
528 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
530 let value: Self = from_bytes(document, PROVISIONING_MAX_BYTES)?;
531 value.validate()?;
532 Ok(value)
533 }
534
535 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
537 self.validate()?;
538 to_string(self, PROVISIONING_MAX_BYTES)
539 }
540
541 pub fn write_xml(&self, writer: impl fmt::Write) -> Result<(), PhoneXmlError> {
543 self.validate()?;
544 write_document(writer, self, PROVISIONING_MAX_BYTES)
545 }
546
547 pub fn validate(&self) -> Result<(), PhoneXmlError> {
549 validate_device_pool(&self.device_pool)?;
550 validate_transport_endpoints(&self.device_pool, self.security.transport)?;
551 validate_locales(&self.locales)?;
552 validate_service_urls(&self.service_urls)?;
553 validate_dscp(&self.vendor.dscp)?;
554 validate_codec_policy(&self.vendor.codecs)?;
555 validate_security(&self.security)?;
556 validate_count(
557 "firmware load entries",
558 self.firmware_loads.len(),
559 PROVISIONING_MAX_FIRMWARE_LOADS,
560 )?;
561 validate_firmware_loads(&self.firmware_loads)?;
562 validate_optional_text("SSH user ID", self.ssh_user_id.as_deref(), 1, 64)
563 }
564}
565
566fn validate_transport_endpoints(
567 pool: &ProvisioningDevicePool,
568 transport: ProvisioningTransport,
569) -> Result<(), PhoneXmlError> {
570 if transport == ProvisioningTransport::Tls
571 && !pool
572 .call_managers
573 .members
574 .entries
575 .iter()
576 .all(|member| member.call_manager.ports.secure_signaling.is_some())
577 {
578 return Err(PhoneXmlError::InvalidField {
579 field: "secure signaling endpoint",
580 expected: "a secure signaling port for every configured call manager",
581 });
582 }
583 Ok(())
584}
585
586#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
588#[serde(rename = "Default", deny_unknown_fields)]
589pub struct DefaultProvisioning {
590 #[serde(rename = "devicePool")]
591 pub device_pool: ProvisioningDevicePool,
592 #[serde(rename = "loadInformation", default)]
593 pub firmware_loads: Vec<ProvisioningFirmwareLoad>,
594}
595
596impl DefaultProvisioning {
597 pub fn from_xml(document: &[u8]) -> Result<Self, PhoneXmlError> {
599 let value: Self = from_bytes(document, PROVISIONING_MAX_BYTES)?;
600 value.validate()?;
601 Ok(value)
602 }
603
604 pub fn to_xml(&self) -> Result<String, PhoneXmlError> {
606 self.validate()?;
607 to_string(self, PROVISIONING_MAX_BYTES)
608 }
609
610 pub fn write_xml(&self, writer: impl fmt::Write) -> Result<(), PhoneXmlError> {
612 self.validate()?;
613 write_document(writer, self, PROVISIONING_MAX_BYTES)
614 }
615
616 pub fn validate(&self) -> Result<(), PhoneXmlError> {
618 validate_device_pool(&self.device_pool)?;
619 validate_firmware_loads(&self.firmware_loads)
620 }
621}
622
623fn validate_device_pool(pool: &ProvisioningDevicePool) -> Result<(), PhoneXmlError> {
624 validate_text("date template", &pool.date_time.date_template, 1, 16)?;
625 validate_text("time zone", &pool.date_time.time_zone, 1, 64)?;
626 validate_count(
627 "NTP servers",
628 pool.date_time.ntps.entries.len(),
629 PROVISIONING_MAX_NTP_SERVERS,
630 )?;
631 let members = &pool.call_managers.members.entries;
632 if members.is_empty() {
633 return Err(PhoneXmlError::InvalidField {
634 field: "call-manager group",
635 expected: "at least one server",
636 });
637 }
638 validate_count(
639 "call-manager servers",
640 members.len(),
641 PROVISIONING_MAX_CALL_MANAGERS,
642 )?;
643 let mut priorities = members
644 .iter()
645 .map(|member| member.priority)
646 .collect::<Vec<_>>();
647 priorities.sort_unstable();
648 if priorities.windows(2).any(|pair| pair[0] == pair[1]) {
649 return Err(PhoneXmlError::InvalidField {
650 field: "call-manager priority",
651 expected: "unique priorities",
652 });
653 }
654 if members.iter().any(|member| {
655 member.call_manager.ports.signaling == 0
656 || member.call_manager.ports.secure_signaling == Some(0)
657 }) {
658 return Err(PhoneXmlError::InvalidField {
659 field: "call-manager port",
660 expected: "between 1 and 65535",
661 });
662 }
663 Ok(())
664}
665
666fn validate_firmware_loads(loads: &[ProvisioningFirmwareLoad]) -> Result<(), PhoneXmlError> {
667 validate_count(
668 "firmware load entries",
669 loads.len(),
670 PROVISIONING_MAX_FIRMWARE_LOADS,
671 )?;
672 for load in loads {
673 validate_optional_text("firmware model", load.model.as_deref(), 1, 64)?;
674 validate_text("firmware load", &load.file, 1, 128)?;
675 }
676 Ok(())
677}
678
679fn validate_locales(locales: &ProvisioningLocales) -> Result<(), PhoneXmlError> {
680 for value in [
681 locales.user.as_deref(),
682 locales.network.as_deref(),
683 locales.user_version.as_deref(),
684 locales.network_version.as_deref(),
685 ] {
686 validate_optional_text("locale", value, 1, 64)?;
687 }
688 Ok(())
689}
690
691fn validate_service_urls(urls: &ProvisioningServiceUrls) -> Result<(), PhoneXmlError> {
692 for value in [
693 urls.authentication.as_deref(),
694 urls.directory.as_deref(),
695 urls.idle.as_deref(),
696 urls.information.as_deref(),
697 urls.messages.as_deref(),
698 urls.proxy.as_deref(),
699 urls.services.as_deref(),
700 ] {
701 validate_optional_text("provisioning service URL", value, 1, 256)?;
702 }
703 Ok(())
704}
705
706fn validate_dscp(dscp: &ProvisioningDscp) -> Result<(), PhoneXmlError> {
707 if [dscp.signaling, dscp.audio, dscp.video]
708 .into_iter()
709 .flatten()
710 .any(|value| value > 63)
711 {
712 return Err(PhoneXmlError::InvalidField {
713 field: "provisioning DSCP",
714 expected: "between 0 and 63",
715 });
716 }
717 Ok(())
718}
719
720fn validate_codec_policy(policy: &ProvisioningCodecPolicy) -> Result<(), PhoneXmlError> {
721 validate_optional_text("preferred codec", policy.preferred.as_deref(), 1, 32)
722}
723
724fn validate_security(security: &ProvisioningSecurity) -> Result<(), PhoneXmlError> {
725 if let Some(capf) = &security.capf {
726 if capf.phone_port == 0 {
727 return Err(PhoneXmlError::InvalidField {
728 field: "CAPF port",
729 expected: "between 1 and 65535",
730 });
731 }
732 validate_optional_text(
733 "CAPF authentication mode",
734 capf.authentication_mode.as_deref(),
735 1,
736 64,
737 )?;
738 validate_optional_text(
739 "CAPF certificate operation",
740 capf.certificate_operation.as_deref(),
741 1,
742 64,
743 )?;
744 }
745 Ok(())
746}
747
748fn validate_host(value: &str) -> Result<(), PhoneXmlError> {
749 if value.parse::<IpAddr>().is_ok() {
750 return Ok(());
751 }
752 validate_text("provisioning host", value, 1, 253)?;
753 if value.split('.').any(|label| {
754 label.is_empty()
755 || label.len() > 63
756 || label.starts_with('-')
757 || label.ends_with('-')
758 || !label
759 .bytes()
760 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
761 }) {
762 return Err(PhoneXmlError::InvalidField {
763 field: "provisioning host",
764 expected: "an IPv4, IPv6, or DNS name",
765 });
766 }
767 Ok(())
768}
769
770fn validate_optional_text(
771 field: &'static str,
772 value: Option<&str>,
773 minimum: usize,
774 maximum: usize,
775) -> Result<(), PhoneXmlError> {
776 match value {
777 Some(value) => validate_text(field, value, minimum, maximum),
778 None => Ok(()),
779 }
780}
781
782fn validate_text(
783 field: &'static str,
784 value: &str,
785 minimum: usize,
786 maximum: usize,
787) -> Result<(), PhoneXmlError> {
788 if !text_length_is_within(value, minimum, maximum)
789 || value.chars().any(|character| character.is_control())
790 {
791 Err(PhoneXmlError::InvalidField {
792 field,
793 expected: "within the provisioning text bounds",
794 })
795 } else {
796 Ok(())
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 fn device() -> DeviceProvisioning {
805 DeviceProvisioning {
806 protocol: ProvisioningProtocol::Sccp,
807 device_pool: ProvisioningDevicePool {
808 date_time: ProvisioningDateTime {
809 date_template: "D/M/Ya".into(),
810 time_zone: "Pacific Standard/Daylight Time".into(),
811 ntps: ProvisioningNtpServers {
812 entries: vec![ProvisioningNtpServer {
813 name: ProvisioningHost::new("192.0.2.10").unwrap(),
814 mode: ProvisioningNtpMode::Unicast,
815 }],
816 },
817 },
818 call_managers: ProvisioningCallManagerGroup {
819 members: ProvisioningCallManagerMembers {
820 entries: vec![ProvisioningCallManagerMember {
821 priority: 0,
822 call_manager: ProvisioningCallManager {
823 host: ProvisioningHost::new("pbx.example.test").unwrap(),
824 ports: ProvisioningPorts {
825 signaling: 2000,
826 secure_signaling: Some(2443),
827 },
828 },
829 }],
830 },
831 },
832 },
833 locales: ProvisioningLocales {
834 user: Some("English_United_States".into()),
835 network: Some("United_States".into()),
836 ..Default::default()
837 },
838 service_urls: ProvisioningServiceUrls {
839 directory: Some("https://pbx.example.test/sccp/directory".into()),
840 services: Some("https://pbx.example.test/sccp/services".into()),
841 ..Default::default()
842 },
843 security: ProvisioningSecurity {
844 transport: ProvisioningTransport::Tls,
845 capf: Some(ProvisioningCapf {
846 phone_port: 3804,
847 host: ProvisioningHost::new("198.51.100.8").unwrap(),
848 authentication_mode: Some("ByAuthenticationString".into()),
849 authentication_token: Some(ProvisioningSecret::new("secret-token").unwrap()),
850 certificate_operation: Some("InstallUpgrade".into()),
851 }),
852 encrypted_config: Some(ProvisioningSecret::new("encrypted-config-token").unwrap()),
853 ..Default::default()
854 },
855 vendor: ProvisioningVendorConfig {
856 dscp: ProvisioningDscp {
857 signaling: Some(24),
858 audio: Some(46),
859 video: Some(34),
860 },
861 codecs: ProvisioningCodecPolicy {
862 preferred: Some("g711alaw".into()),
863 g722: Some(ProvisioningBoolean::Enabled),
864 g729: Some(ProvisioningBoolean::Disabled),
865 },
866 features: ProvisioningFeatures {
867 enbloc_dialing: Some(ProvisioningBoolean::Enabled),
868 do_not_disturb: Some(ProvisioningBoolean::Enabled),
869 call_pickup: Some(ProvisioningBoolean::Enabled),
870 barge: Some(ProvisioningBoolean::Enabled),
871 blf: Some(ProvisioningBoolean::Enabled),
872 mwi: Some(ProvisioningBoolean::Enabled),
873 mobility: Some(ProvisioningBoolean::Enabled),
874 ..Default::default()
875 },
876 },
877 firmware_loads: vec![ProvisioningFirmwareLoad {
878 model: Some("Cisco 7961".into()),
879 file: "term61.default.loads".into(),
880 }],
881 ssh_user_id: Some("phone-admin".into()),
882 ssh_password: Some(ProvisioningSecret::new("ssh-password").unwrap()),
883 }
884 }
885
886 #[test]
887 fn device_and_default_roots_round_trip_bootable_subset() {
888 let mut expected = device();
889 expected
890 .device_pool
891 .call_managers
892 .members
893 .entries
894 .push(ProvisioningCallManagerMember {
895 priority: 1,
896 call_manager: ProvisioningCallManager {
897 host: ProvisioningHost::new("2001:db8::20").unwrap(),
898 ports: ProvisioningPorts {
899 signaling: 2000,
900 secure_signaling: Some(2443),
901 },
902 },
903 });
904 expected.service_urls.information =
905 Some("https://pbx.example.test/info?name=A&B=<all>".into());
906 let xml = expected.to_xml().unwrap();
907 assert!(xml.starts_with("<device>"));
908 assert!(xml.contains("A&B=<all>"));
909 assert!(
910 xml.find("pbx.example.test").unwrap() < xml.find("2001:db8::20").unwrap(),
911 "call-manager order must remain deterministic"
912 );
913 assert_eq!(
914 DeviceProvisioning::from_xml(xml.as_bytes()).unwrap(),
915 expected
916 );
917
918 let default = DefaultProvisioning {
919 device_pool: expected.device_pool.clone(),
920 firmware_loads: expected.firmware_loads.clone(),
921 };
922 let xml = default.to_xml().unwrap();
923 assert_eq!(
924 DefaultProvisioning::from_xml(xml.as_bytes()).unwrap(),
925 default
926 );
927 }
928
929 #[test]
930 fn provisioning_rejects_ambiguous_endpoints_priorities_and_dscp() {
931 assert!(ProvisioningHost::new("bad host").is_err());
932 let mut invalid = device();
933 invalid
934 .device_pool
935 .call_managers
936 .members
937 .entries
938 .push(invalid.device_pool.call_managers.members.entries[0].clone());
939 assert!(invalid.validate().is_err());
940
941 let mut invalid = device();
942 let mut clear_only = invalid.device_pool.call_managers.members.entries[0].clone();
943 clear_only.priority = 1;
944 clear_only.call_manager.ports.secure_signaling = None;
945 invalid
946 .device_pool
947 .call_managers
948 .members
949 .entries
950 .push(clear_only);
951 assert!(invalid.validate().is_err());
952
953 let mut invalid = device();
954 invalid
955 .device_pool
956 .call_managers
957 .members
958 .entries
959 .iter_mut()
960 .for_each(|member| member.call_manager.ports.secure_signaling = None);
961 assert!(invalid.validate().is_err());
962
963 let mut invalid = device();
964 invalid.vendor.dscp.audio = Some(64);
965 assert!(invalid.validate().is_err());
966
967 let mut invalid = device();
968 invalid.device_pool.call_managers.members.entries[0]
969 .call_manager
970 .ports
971 .secure_signaling = Some(0);
972 assert!(invalid.validate().is_err());
973
974 let mut invalid = DefaultProvisioning {
975 device_pool: device().device_pool,
976 firmware_loads: vec![ProvisioningFirmwareLoad {
977 model: None,
978 file: String::new(),
979 }],
980 };
981 assert!(invalid.validate().is_err());
982 invalid.firmware_loads[0].file = "term.default.loads".into();
983 assert!(invalid.validate().is_ok());
984 }
985
986 #[test]
987 fn provisioning_debug_redacts_every_secret() {
988 let debug = format!("{:?}", device());
989 assert!(!debug.contains("secret-token"));
990 assert!(!debug.contains("encrypted-config-token"));
991 assert!(!debug.contains("ssh-password"));
992 assert!(debug.contains("<redacted>"));
993 }
994
995 #[test]
996 fn provisioning_rejects_wrong_unsafe_unknown_malformed_and_oversized_xml() {
997 assert!(DeviceProvisioning::from_xml(b"<Default/>").is_err());
998 assert!(DeviceProvisioning::from_xml(b"<device><unknown/></device>").is_err());
999 assert!(DeviceProvisioning::from_xml(b"<device>").is_err());
1000 assert!(matches!(
1001 DeviceProvisioning::from_xml(
1002 br#"<!DOCTYPE device [<!ENTITY host "pbx.example.test">]><device/>"#,
1003 ),
1004 Err(PhoneXmlError::DocumentTypeForbidden)
1005 ));
1006 assert!(matches!(
1007 DeviceProvisioning::from_xml(&vec![b'x'; PROVISIONING_MAX_BYTES + 1]),
1008 Err(PhoneXmlError::LimitExceeded { .. })
1009 ));
1010
1011 #[derive(Debug)]
1012 struct FailingWriter;
1013 impl fmt::Write for FailingWriter {
1014 fn write_str(&mut self, _value: &str) -> fmt::Result {
1015 Err(fmt::Error)
1016 }
1017 }
1018 assert!(matches!(
1019 device().write_xml(FailingWriter),
1020 Err(PhoneXmlError::Write(_))
1021 ));
1022 }
1023}