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