1use std::{collections::BTreeMap, fmt, str::FromStr};
2
3use serde::de::DeserializeOwned;
4use serde_json::{Map, Value};
5use thiserror::Error;
6use url::{Host, Url};
7
8const V6MIG_SPEC: &str = "v6mig-1";
9const MAX_TTL_SECS: u64 = 604_800;
10
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum BootstrapError {
15 #[error("url: {0}, err: {1}")]
17 InvalidUrl(String, String),
18 #[error("extracting tls policy : {0}")]
20 InvalidTlsPolicy(String),
21 #[error("parsing field, expected: '{0}', got: '{1}'")]
23 MalformedField(String, String),
24 #[error("missing field: {0}")]
26 MissingField(&'static str),
27 #[error("unsupported spec version: {0}")]
29 UnsupportedVersion(String),
30 #[error("tls policy set to validate for http scheme")]
32 InvalidTlsForHttp,
33 #[error("provisioning URL must contain a host")]
35 MissingUrlHost,
36 #[error("record contain data beyond spec fields, record: {0}")]
38 InvalidRecord(String),
39 #[error("provisioning URL cannot use an IPv4 address")]
41 Ipv4EndpointNotAllowed,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
45pub enum Capability {
47 Xlat464,
49 DsLite,
51 IpIp,
53 Lw4o6,
55 MapE,
57 MapT,
59}
60
61impl Capability {
62 const ALL: [Self; 6] = [
63 Self::Xlat464,
64 Self::DsLite,
65 Self::IpIp,
66 Self::Lw4o6,
67 Self::MapE,
68 Self::MapT,
69 ];
70
71 pub fn as_str(self) -> &'static str {
73 match self {
74 Self::Xlat464 => "464xlat",
75 Self::DsLite => "dslite",
76 Self::IpIp => "ipip",
77 Self::Lw4o6 => "lw4o6",
78 Self::MapE => "map_e",
79 Self::MapT => "map_t",
80 }
81 }
82}
83
84#[derive(Debug, Error, PartialEq, Eq)]
85pub enum CapabilityError {
87 #[error("unsupported HB46PP capability: {0}")]
89 UnsupportedName(String),
90}
91
92impl FromStr for Capability {
93 type Err = CapabilityError;
94
95 fn from_str(value: &str) -> Result<Self, Self::Err> {
96 Self::ALL
97 .into_iter()
98 .find(|capability| capability.as_str() == value)
99 .ok_or_else(|| CapabilityError::UnsupportedName(value.to_string()))
100 }
101}
102
103#[derive(Debug, Error, PartialEq, Eq)]
104pub enum TtlError {
106 #[error("TTL must be at most {MAX_TTL_SECS} seconds")]
108 TooLarge,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct Ttl(u32);
116
117impl Ttl {
118 pub fn as_secs(self) -> u32 {
120 self.0
121 }
122}
123
124impl TryFrom<u64> for Ttl {
125 type Error = TtlError;
126
127 fn try_from(value: u64) -> Result<Self, Self::Error> {
128 if value > MAX_TTL_SECS {
129 return Err(TtlError::TooLarge);
130 }
131
132 Ok(Self(value as u32))
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum AuthStatus {
139 Required,
141 Rejected,
143 Accepted,
145}
146
147impl AuthStatus {
148 pub fn as_str(self) -> &'static str {
150 match self {
151 Self::Required => "req",
152 Self::Rejected => "bad",
153 Self::Accepted => "ok",
154 }
155 }
156}
157
158#[derive(Debug, Error, PartialEq, Eq)]
159pub enum AuthStatusError {
161 #[error("unsupported HB46PP auth status: {0}")]
163 UnsupportedStatus(String),
164}
165
166impl FromStr for AuthStatus {
167 type Err = AuthStatusError;
168
169 fn from_str(value: &str) -> Result<Self, Self::Err> {
170 match value {
171 "req" => Ok(Self::Required),
172 "bad" => Ok(Self::Rejected),
173 "ok" => Ok(Self::Accepted),
174 _ => Err(AuthStatusError::UnsupportedStatus(value.to_string())),
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ProviderInfo {
186 enabler_name: String,
187 service_name: Option<String>,
188 isp_name: Option<String>,
189}
190
191impl ProviderInfo {
192 pub fn enabler_name(&self) -> &str {
194 &self.enabler_name
195 }
196
197 pub fn service_name(&self) -> Option<&str> {
199 self.service_name.as_deref()
200 }
201
202 pub fn isp_name(&self) -> Option<&str> {
204 self.isp_name.as_deref()
205 }
206}
207
208pub struct SelectedOffer<'a> {
210 capability: Capability,
211 parameters: &'a serde_json::Value,
212}
213
214impl SelectedOffer<'_> {
215 pub fn capability(&self) -> Capability {
217 self.capability
218 }
219
220 pub fn parameters(&self) -> &serde_json::Value {
225 self.parameters
226 }
227}
228
229#[derive(Debug, Error)]
230#[non_exhaustive]
231pub enum ProvisioningDataError {
233 #[error("response is not a JSON object")]
235 NotObject,
236 #[error("missing required response field: {0}")]
238 MissingField(&'static str),
239 #[error("response field must not be null: {0}")]
241 NullField(&'static str),
242 #[error("invalid response field {field}: {source}")]
244 InvalidField {
245 field: &'static str,
247 #[source]
249 source: serde_json::Error,
250 },
251 #[error("response field exceeds 256 bytes including quotes: {0}")]
253 InformationalNameTooLong(&'static str),
254 #[error(transparent)]
256 Ttl(#[from] TtlError),
257 #[error(transparent)]
259 Token(#[from] TokenError),
260 #[error(transparent)]
262 AuthStatus(#[from] AuthStatusError),
263 #[error(transparent)]
265 Capability(#[from] CapabilityError),
266 #[error("duplicate capability in response order: {0:?}")]
268 DuplicateOrder(Capability),
269 #[error("response order lists a method without its provisioning payload: {0:?}")]
271 MissingOffer(Capability),
272 #[error("invalid provisioning payload shape for capability: {0:?}")]
274 InvalidOfferShape(Capability),
275}
276
277#[derive(Debug, Clone)]
278pub struct ProvisioningData {
284 provider_info: ProviderInfo,
285 ttl: Option<Ttl>,
286 token: Option<Token>,
287 auth: Option<AuthStatus>,
288 order: Vec<Capability>,
289 ipv6_mostly: Option<bool>,
290 offers: BTreeMap<Capability, Value>,
291}
292
293impl ProvisioningData {
294 pub fn parse(input: &str) -> Result<Self, ProvisioningDataError> {
296 let value =
297 serde_json::from_str(input).map_err(|source| ProvisioningDataError::InvalidField {
298 field: "response",
299 source,
300 })?;
301 let mut fields = match value {
302 Value::Object(fields) => fields,
303 _ => return Err(ProvisioningDataError::NotObject),
304 };
305
306 let enabler_name = take_required::<String>(&mut fields, "enabler_name")?;
307 validate_informational_name("enabler_name", &enabler_name)?;
308 let service_name = take_optional::<String>(&mut fields, "service_name")?;
309 if let Some(service_name) = &service_name {
310 validate_informational_name("service_name", service_name)?;
311 }
312 let isp_name = take_optional::<String>(&mut fields, "isp_name")?;
313 if let Some(isp_name) = &isp_name {
314 validate_informational_name("isp_name", isp_name)?;
315 }
316
317 let ttl = take_optional::<u64>(&mut fields, "ttl")
318 .map(|ttl| ttl.map(Ttl::try_from).transpose())??;
319 let token = take_optional::<String>(&mut fields, "token")
320 .map(|token| token.map(|token| token.parse()).transpose())??;
321 let auth = take_optional::<String>(&mut fields, "auth")
322 .map(|auth| auth.map(|auth| auth.parse()).transpose())??;
323 let order_names = take_required::<Vec<String>>(&mut fields, "order")?;
324 let ipv6_mostly = take_optional::<bool>(&mut fields, "ipv6_mostly")?;
325
326 let mut order = Vec::with_capacity(order_names.len());
327 for name in order_names {
328 let capability = name.parse()?;
329 if order.contains(&capability) {
330 return Err(ProvisioningDataError::DuplicateOrder(capability));
331 }
332 order.push(capability);
333 }
334
335 let mut offers = BTreeMap::new();
336 for capability in Capability::ALL {
337 let Some(parameters) = take_optional::<Value>(&mut fields, capability.as_str())? else {
338 continue;
339 };
340
341 let has_valid_shape = match capability {
342 Capability::IpIp => parameters
343 .as_array()
344 .is_some_and(|tunnels| tunnels.iter().all(Value::is_object)),
345 _ => parameters.is_object(),
346 };
347
348 if !has_valid_shape {
349 return Err(ProvisioningDataError::InvalidOfferShape(capability));
350 }
351
352 offers.insert(capability, parameters);
353 }
354 for capability in &order {
355 if !offers.contains_key(capability) {
356 return Err(ProvisioningDataError::MissingOffer(*capability));
357 }
358 }
359
360 Ok(Self {
361 provider_info: ProviderInfo {
362 enabler_name,
363 service_name,
364 isp_name,
365 },
366 ttl,
367 token,
368 auth,
369 order,
370 ipv6_mostly,
371 offers,
372 })
373 }
374
375 pub fn select(&self, supported: &[Capability]) -> Option<SelectedOffer<'_>> {
380 for &capability in self.order() {
381 if !supported.contains(&capability) {
382 continue;
383 }
384
385 let Some(parameters) = self.offer(capability) else {
386 continue;
387 };
388
389 return Some(SelectedOffer {
390 capability,
391 parameters,
392 });
393 }
394
395 None
396 }
397
398 pub fn provider_info(&self) -> &ProviderInfo {
400 &self.provider_info
401 }
402
403 pub fn ttl(&self) -> Option<Ttl> {
405 self.ttl
406 }
407
408 pub fn token(&self) -> Option<&Token> {
413 self.token.as_ref()
414 }
415
416 pub fn auth(&self) -> Option<AuthStatus> {
418 self.auth
419 }
420
421 pub fn order(&self) -> &[Capability] {
423 &self.order
424 }
425
426 pub fn ipv6_mostly(&self) -> Option<bool> {
433 self.ipv6_mostly
434 }
435
436 pub fn offer(&self, capability: Capability) -> Option<&Value> {
441 self.offers.get(&capability)
442 }
443}
444
445fn take_required<T>(
446 fields: &mut Map<String, Value>,
447 field: &'static str,
448) -> Result<T, ProvisioningDataError>
449where
450 T: DeserializeOwned,
451{
452 let value = fields
453 .remove(field)
454 .ok_or(ProvisioningDataError::MissingField(field))?;
455 if value.is_null() {
456 return Err(ProvisioningDataError::NullField(field));
457 }
458
459 serde_json::from_value(value)
460 .map_err(|source| ProvisioningDataError::InvalidField { field, source })
461}
462
463fn take_optional<T>(
464 fields: &mut Map<String, Value>,
465 field: &'static str,
466) -> Result<Option<T>, ProvisioningDataError>
467where
468 T: DeserializeOwned,
469{
470 let Some(value) = fields.remove(field) else {
471 return Ok(None);
472 };
473 if value.is_null() {
474 return Err(ProvisioningDataError::NullField(field));
475 }
476
477 serde_json::from_value(value)
478 .map(Some)
479 .map_err(|source| ProvisioningDataError::InvalidField { field, source })
480}
481
482fn validate_informational_name(
483 field: &'static str,
484 value: &str,
485) -> Result<(), ProvisioningDataError> {
486 if value.len() + 2 > 256 {
487 return Err(ProvisioningDataError::InformationalNameTooLong(field));
488 }
489
490 Ok(())
491}
492
493#[derive(Debug, Error, PartialEq, Eq)]
494pub enum ProvisioningRequestError {
496 #[error("at least one capability is required")]
498 EmptyCapabilities,
499 #[error("capabilities must not contain duplicates")]
501 DuplicateCapability,
502}
503
504#[derive(Debug, Error, PartialEq, Eq)]
505pub enum VendorIdError {
507 #[error("vendor ID must be 6 ASCII hex digits with an optional 1..24 character suffix")]
509 InvalidFormat,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq)]
513pub struct VendorId(String);
519
520impl VendorId {
521 pub fn as_str(&self) -> &str {
523 &self.0
524 }
525}
526
527impl FromStr for VendorId {
528 type Err = VendorIdError;
529
530 fn from_str(value: &str) -> Result<Self, Self::Err> {
531 let (oui, suffix) = match value.split_once('-') {
532 Some((oui, suffix)) => (oui, Some(suffix)),
533 None => (value, None),
534 };
535
536 if oui.len() != 6
537 || !oui.chars().all(|c| c.is_ascii_hexdigit())
538 || suffix.is_some_and(|suffix| {
539 suffix.is_empty()
540 || suffix.len() > 24
541 || !suffix
542 .chars()
543 .all(|c| c.is_ascii_alphanumeric() || c == '_')
544 })
545 {
546 return Err(VendorIdError::InvalidFormat);
547 }
548
549 Ok(Self(value.to_string()))
550 }
551}
552
553#[derive(Debug, Error, PartialEq, Eq)]
554pub enum ProductError {
556 #[error("product must be 1..32 ASCII letters, digits, '_' or '-'")]
558 InvalidFormat,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct Product(String);
564
565impl Product {
566 pub fn as_str(&self) -> &str {
568 &self.0
569 }
570}
571
572impl FromStr for Product {
573 type Err = ProductError;
574
575 fn from_str(value: &str) -> Result<Self, Self::Err> {
576 if value.is_empty()
577 || value.len() > 32
578 || !value
579 .chars()
580 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
581 {
582 return Err(ProductError::InvalidFormat);
583 }
584
585 Ok(Self(value.to_string()))
586 }
587}
588
589#[derive(Debug, Error, PartialEq, Eq)]
590pub enum FirmwareVersionError {
592 #[error("firmware version must be 1..32 ASCII digits or '_'")]
594 InvalidFormat,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub struct FirmwareVersion(String);
603
604impl FirmwareVersion {
605 pub fn as_str(&self) -> &str {
607 &self.0
608 }
609}
610
611impl FromStr for FirmwareVersion {
612 type Err = FirmwareVersionError;
613
614 fn from_str(value: &str) -> Result<Self, Self::Err> {
615 if value.is_empty()
616 || value.len() > 32
617 || !value.chars().all(|c| c.is_ascii_digit() || c == '_')
618 {
619 return Err(FirmwareVersionError::InvalidFormat);
620 }
621
622 Ok(Self(value.to_string()))
623 }
624}
625
626#[derive(Debug, Error, PartialEq, Eq)]
627pub enum CredentialsError {
629 #[error("user must be at most 32 ASCII letters, digits, '_' or '-'")]
631 InvalidUser,
632 #[error("password must be at most 32 ASCII letters, digits, '_' or '-'")]
634 InvalidPassword,
635 #[error("expected server name is not a valid URL host")]
637 InvalidExpectedServerName,
638}
639
640#[derive(Clone)]
641pub struct Credentials {
645 user: String,
646 password: String,
647 expected_server_name: Option<Host<String>>,
648}
649
650impl Credentials {
651 pub fn for_server(
657 user: String,
658 password: String,
659 expected_server_name: String,
660 ) -> Result<Self, CredentialsError> {
661 validate_credentials(&user, &password)?;
662
663 let expected_server_name = Host::parse(&expected_server_name)
664 .map_err(|_| CredentialsError::InvalidExpectedServerName)?;
665
666 Ok(Self {
667 user,
668 password,
669 expected_server_name: Some(expected_server_name),
670 })
671 }
672
673 pub fn unrestricted(user: String, password: String) -> Result<Self, CredentialsError> {
679 validate_credentials(&user, &password)?;
680
681 Ok(Self {
682 user,
683 password,
684 expected_server_name: None,
685 })
686 }
687
688 pub fn user(&self) -> &str {
690 &self.user
691 }
692
693 pub fn password(&self) -> &str {
698 &self.password
699 }
700}
701
702impl fmt::Debug for Credentials {
703 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
704 f.debug_struct("Credentials")
705 .field("user", &self.user)
706 .field("password", &"[redacted]")
707 .field("expected_server_name", &self.expected_server_name)
708 .finish()
709 }
710}
711
712fn validate_credentials(user: &str, password: &str) -> Result<(), CredentialsError> {
713 if !valid_credential_component(user) {
714 return Err(CredentialsError::InvalidUser);
715 }
716 if !valid_credential_component(password) {
717 return Err(CredentialsError::InvalidPassword);
718 }
719
720 Ok(())
721}
722
723fn valid_credential_component(value: &str) -> bool {
724 value.len() <= 32
725 && value
726 .chars()
727 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
728}
729
730#[derive(Debug, Error, PartialEq, Eq)]
731pub enum TokenError {
733 #[error("token must be lowercase ASCII hexadecimal only, 64 characters long")]
735 InvalidFormat,
736}
737
738#[derive(Clone, PartialEq, Eq)]
739pub struct Token(String);
743
744impl Token {
745 pub fn as_str(&self) -> &str {
750 &self.0
751 }
752}
753
754impl fmt::Debug for Token {
755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756 f.write_str("Token([redacted])")
757 }
758}
759
760impl FromStr for Token {
761 type Err = TokenError;
762
763 fn from_str(value: &str) -> Result<Self, Self::Err> {
764 if value.len() != 64 || !value.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')) {
765 return Err(TokenError::InvalidFormat);
766 }
767
768 Ok(Self(value.to_string()))
769 }
770}
771
772#[derive(Debug, Error, PartialEq, Eq)]
773pub enum ProvisioningUrlError {
775 #[error("credentials with an expected server name require HTTPS")]
777 CredentialsRequireHttps,
778 #[error("credentials with an expected server name require certificate validation")]
780 CredentialsRequireCertificateValidation,
781 #[error("provisioning URL host does not match the expected server name")]
783 UnexpectedProvisioningHost,
784}
785
786#[derive(Clone)]
787pub struct ProvisioningRequest {
793 vendor_id: VendorId,
794 product: Product,
795 version: FirmwareVersion,
796 capabilities: Vec<Capability>,
797 token: Option<Token>,
798 credentials: Option<Credentials>,
799}
800
801impl ProvisioningRequest {
802 pub fn new(
808 vendor_id: VendorId,
809 product: Product,
810 version: FirmwareVersion,
811 capabilities: Vec<Capability>,
812 token: Option<Token>,
813 credentials: Option<Credentials>,
814 ) -> Result<Self, ProvisioningRequestError> {
815 if capabilities.is_empty() {
816 return Err(ProvisioningRequestError::EmptyCapabilities);
817 }
818 if capabilities
819 .iter()
820 .enumerate()
821 .any(|(index, capability)| capabilities[..index].contains(capability))
822 {
823 return Err(ProvisioningRequestError::DuplicateCapability);
824 }
825
826 Ok(Self {
827 vendor_id,
828 product,
829 version,
830 capabilities,
831 token,
832 credentials,
833 })
834 }
835
836 pub fn vendor_id(&self) -> &VendorId {
838 &self.vendor_id
839 }
840
841 pub fn product(&self) -> &Product {
843 &self.product
844 }
845
846 pub fn version(&self) -> &FirmwareVersion {
848 &self.version
849 }
850
851 pub fn capabilities(&self) -> &[Capability] {
853 &self.capabilities
854 }
855
856 pub fn token(&self) -> Option<&str> {
860 self.token.as_ref().map(Token::as_str)
861 }
862
863 pub fn set_token(&mut self, token: Option<Token>) {
867 self.token = token;
868 }
869
870 pub fn credentials(&self) -> Option<&Credentials> {
872 self.credentials.as_ref()
873 }
874}
875
876impl fmt::Debug for ProvisioningRequest {
877 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878 f.debug_struct("ProvisioningRequest")
879 .field("vendor_id", &self.vendor_id)
880 .field("product", &self.product)
881 .field("version", &self.version)
882 .field("capabilities", &self.capabilities)
883 .field("token", &self.token)
884 .field("credentials", &self.credentials)
885 .finish()
886 }
887}
888
889#[derive(Debug, Clone, Copy, PartialEq, Eq)]
890pub enum TlsPolicy {
892 NoCertificateValidation, ValidateCertificate, }
897
898#[derive(Debug)]
899pub struct Bootstrap {
904 url: Url,
905 tls_policy: TlsPolicy,
906}
907
908impl Bootstrap {
909 pub fn parse(txt: &str) -> Result<Self, BootstrapError> {
911 let mut iter = txt.split(' ');
912
913 let version_field = iter.next().ok_or(BootstrapError::MissingField("v"))?;
914 let version_value = parse_field(version_field, "v")?;
915 if version_value != V6MIG_SPEC {
916 return Err(BootstrapError::UnsupportedVersion(
917 version_value.to_string(),
918 ));
919 }
920
921 let url_field = iter.next().ok_or(BootstrapError::MissingField("url"))?;
922 let url_value = parse_field(url_field, "url")?;
923
924 let tls_field = iter.next().ok_or(BootstrapError::MissingField("t"))?;
925 let tls_value = parse_field(tls_field, "t")?;
926
927 if iter.next().is_some() {
928 return Err(BootstrapError::InvalidRecord(txt.to_string()));
929 };
930
931 let tls_policy = match tls_value {
932 "a" => TlsPolicy::NoCertificateValidation,
933 "b" => TlsPolicy::ValidateCertificate,
934 _ => {
935 return Err(BootstrapError::InvalidTlsPolicy(format!(
936 "invalid tls policy value: {tls_value}, expected '<a|b>'"
937 )));
938 }
939 };
940
941 let url = Url::parse(url_value)
942 .map_err(|e| BootstrapError::InvalidUrl(url_value.to_string(), e.to_string()))?;
943
944 if url.scheme() != "http" && url.scheme() != "https" {
945 return Err(BootstrapError::InvalidUrl(
946 url_value.to_string(),
947 format!(
948 "unsuported url scheme: {}, supported: <http|https>",
949 url.scheme(),
950 ),
951 ));
952 };
953
954 if url.scheme() == "http" && tls_policy == TlsPolicy::ValidateCertificate {
955 return Err(BootstrapError::InvalidTlsForHttp);
956 }
957
958 match url.host() {
959 Some(Host::Ipv4(_)) => return Err(BootstrapError::Ipv4EndpointNotAllowed),
960 Some(_) => {}
961 None => return Err(BootstrapError::MissingUrlHost),
962 }
963
964 Ok(Bootstrap { url, tls_policy })
965 }
966
967 pub fn provisioning_url(
972 &self,
973 request: &ProvisioningRequest,
974 ) -> Result<Url, ProvisioningUrlError> {
975 self.provisioning_url_for(self.url.clone(), request)
976 }
977
978 pub(crate) fn provisioning_url_for(
979 &self,
980 endpoint: Url,
981 request: &ProvisioningRequest,
982 ) -> Result<Url, ProvisioningUrlError> {
983 if let Some(credentials) = request.credentials() {
984 self.validate_credentials(&endpoint, credentials)?;
985 }
986 let mut request_url = endpoint;
987 let capabilities = request
988 .capabilities()
989 .iter()
990 .map(|c| c.as_str())
991 .collect::<Vec<_>>()
992 .join(",");
993 {
994 let mut query = request_url.query_pairs_mut();
995 query.append_pair("vendorid", request.vendor_id().as_str());
996 query.append_pair("product", request.product().as_str());
997 query.append_pair("version", request.version().as_str());
998 query.append_pair("capability", &capabilities);
999 if let Some(token) = request.token() {
1000 query.append_pair("token", token);
1001 }
1002 if let Some(credentials) = request.credentials() {
1003 query.append_pair("user", credentials.user());
1004 query.append_pair("pass", credentials.password());
1005 }
1006 }
1007 Ok(request_url)
1008 }
1009
1010 fn validate_credentials(
1011 &self,
1012 endpoint: &Url,
1013 credentials: &Credentials,
1014 ) -> Result<(), ProvisioningUrlError> {
1015 let Some(expected_server_name) = &credentials.expected_server_name else {
1016 return Ok(());
1017 };
1018
1019 if endpoint.scheme() != "https" {
1020 return Err(ProvisioningUrlError::CredentialsRequireHttps);
1021 }
1022 if self.tls_policy != TlsPolicy::ValidateCertificate {
1023 return Err(ProvisioningUrlError::CredentialsRequireCertificateValidation);
1024 }
1025
1026 let endpoint_host = endpoint.host().map(|host| host.to_owned());
1027 if endpoint_host.as_ref() != Some(expected_server_name) {
1028 return Err(ProvisioningUrlError::UnexpectedProvisioningHost);
1029 }
1030
1031 Ok(())
1032 }
1033
1034 pub fn tls_policy(&self) -> TlsPolicy {
1036 self.tls_policy
1037 }
1038
1039 pub fn endpoint(&self) -> &Url {
1041 &self.url
1042 }
1043}
1044
1045fn parse_field<'a>(field: &'a str, expected_key: &'static str) -> Result<&'a str, BootstrapError> {
1046 let (key, value) = field.split_once('=').ok_or(BootstrapError::MalformedField(
1047 format!("{expected_key}=<value>"),
1048 field.to_string(),
1049 ))?;
1050
1051 if key != expected_key {
1052 return Err(BootstrapError::MalformedField(
1053 expected_key.to_string(),
1054 key.to_string(),
1055 ));
1056 };
1057 Ok(value)
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 const V6CONNECT_BOOTSTRAP: &str =
1065 "v=v6mig-1 url=https://prod.v6mig.v6connect.net/cpe/v1/config t=b";
1066 const TOKEN: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
1067
1068 fn vendor_id() -> VendorId {
1069 "000000".parse().unwrap()
1070 }
1071
1072 fn product() -> Product {
1073 "dslite-b4".parse().unwrap()
1074 }
1075
1076 fn version() -> FirmwareVersion {
1077 "0_1_0".parse().unwrap()
1078 }
1079
1080 fn credentials_for_server(expected_server_name: &str) -> Credentials {
1081 Credentials::for_server(
1082 "user".to_string(),
1083 "pass".to_string(),
1084 expected_server_name.to_string(),
1085 )
1086 .unwrap()
1087 }
1088
1089 fn valid_request() -> ProvisioningRequest {
1090 ProvisioningRequest::new(
1091 vendor_id(),
1092 product(),
1093 version(),
1094 vec![Capability::DsLite],
1095 None,
1096 None,
1097 )
1098 .unwrap()
1099 }
1100
1101 #[test]
1102 fn serializes_capabilities_to_hb46pp_wire_names() {
1103 let names: Vec<_> = Capability::ALL
1104 .into_iter()
1105 .map(Capability::as_str)
1106 .collect();
1107
1108 assert_eq!(
1109 names,
1110 ["464xlat", "dslite", "ipip", "lw4o6", "map_e", "map_t"]
1111 );
1112 }
1113
1114 #[test]
1115 fn parses_hb46pp_capability_wire_names() {
1116 for capability in Capability::ALL {
1117 assert_eq!(capability.as_str().parse(), Ok(capability));
1118 }
1119 }
1120
1121 #[test]
1122 fn rejects_unknown_capability_wire_names() {
1123 for name in ["DS-Lite", "wireguard"] {
1124 let error = name.parse::<Capability>().unwrap_err();
1125
1126 assert_eq!(error, CapabilityError::UnsupportedName(name.to_string()));
1127 }
1128 }
1129
1130 #[test]
1131 fn accepts_ttl_at_the_specification_limit() {
1132 let ttl = Ttl::try_from(604_800).unwrap();
1133
1134 assert_eq!(ttl.as_secs(), 604_800);
1135 }
1136
1137 #[test]
1138 fn rejects_ttl_above_the_specification_limit() {
1139 let error = Ttl::try_from(604_801).unwrap_err();
1140
1141 assert_eq!(error, TtlError::TooLarge);
1142 }
1143
1144 #[test]
1145 fn parses_hb46pp_auth_statuses() {
1146 for (wire_name, status) in [
1147 ("req", AuthStatus::Required),
1148 ("bad", AuthStatus::Rejected),
1149 ("ok", AuthStatus::Accepted),
1150 ] {
1151 assert_eq!(wire_name.parse(), Ok(status));
1152 assert_eq!(status.as_str(), wire_name);
1153 }
1154 }
1155
1156 #[test]
1157 fn rejects_unknown_hb46pp_auth_status() {
1158 let error = "required".parse::<AuthStatus>().unwrap_err();
1159
1160 assert_eq!(
1161 error,
1162 AuthStatusError::UnsupportedStatus("required".to_string())
1163 );
1164 }
1165
1166 #[test]
1167 fn parses_v6connect_response_shape() {
1168 let response = ProvisioningData::parse(&format!(
1169 r#"{{
1170 "ttl": 86400,
1171 "token": "{TOKEN}",
1172 "service_name": "v6 コネクト",
1173 "enabler_name": "v6 コネクト",
1174 "dslite": {{"aftr": "dslite.v6connect.net"}},
1175 "order": ["dslite"],
1176 "future_extension": {{"ignored": true}}
1177 }}"#
1178 ))
1179 .unwrap();
1180
1181 assert_eq!(response.provider_info().enabler_name(), "v6 コネクト");
1182 assert_eq!(response.provider_info().service_name(), Some("v6 コネクト"));
1183 assert_eq!(response.provider_info().isp_name(), None);
1184 assert_eq!(response.ttl().unwrap().as_secs(), 86_400);
1185 assert_eq!(response.token().unwrap().as_str(), TOKEN);
1186 assert_eq!(response.auth(), None);
1187 assert_eq!(response.order(), [Capability::DsLite]);
1188 assert_eq!(
1189 response.offer(Capability::DsLite),
1190 Some(&serde_json::json!({"aftr": "dslite.v6connect.net"}))
1191 );
1192 }
1193
1194 #[test]
1195 fn retains_ipv6_mostly_xlat_offer_outside_activation_order() {
1196 let response = ProvisioningData::parse(
1197 r#"{
1198 "enabler_name": "example",
1199 "order": ["dslite"],
1200 "ipv6_mostly": true,
1201 "dslite": {"aftr": "dslite.example"},
1202 "464xlat": {"nat64prefix": "64:ff9b::/96"}
1203 }"#,
1204 )
1205 .unwrap();
1206
1207 assert_eq!(response.order(), [Capability::DsLite]);
1208 assert_eq!(response.ipv6_mostly(), Some(true));
1209 assert_eq!(
1210 response.offer(Capability::Xlat464),
1211 Some(&serde_json::json!({"nat64prefix": "64:ff9b::/96"}))
1212 );
1213 }
1214
1215 #[test]
1216 fn selects_the_first_server_ordered_supported_offer() {
1217 let response = ProvisioningData::parse(
1218 r#"{
1219 "enabler_name": "example",
1220 "order": ["map_e", "dslite"],
1221 "map_e": {"br": "2001:db8::1", "rules": []},
1222 "dslite": {"aftr": "dslite.example"}
1223 }"#,
1224 )
1225 .unwrap();
1226
1227 let selected = response
1228 .select(&[Capability::DsLite, Capability::MapE])
1229 .unwrap();
1230
1231 assert_eq!(selected.capability(), Capability::MapE);
1232 assert_eq!(
1233 selected.parameters(),
1234 &serde_json::json!({"br": "2001:db8::1", "rules": []})
1235 );
1236 }
1237
1238 #[test]
1239 fn selects_a_later_offer_when_higher_priority_offers_are_unsupported() {
1240 let response = ProvisioningData::parse(
1241 r#"{
1242 "enabler_name": "example",
1243 "order": ["map_e", "dslite"],
1244 "map_e": {"br": "2001:db8::1", "rules": []},
1245 "dslite": {"aftr": "dslite.example"}
1246 }"#,
1247 )
1248 .unwrap();
1249
1250 let selected = response.select(&[Capability::DsLite]).unwrap();
1251
1252 assert_eq!(selected.capability(), Capability::DsLite);
1253 assert_eq!(
1254 selected.parameters(),
1255 &serde_json::json!({"aftr": "dslite.example"})
1256 );
1257 }
1258
1259 #[test]
1260 fn selects_nothing_when_no_ordered_offer_is_supported() {
1261 let response = ProvisioningData::parse(
1262 r#"{
1263 "enabler_name": "example",
1264 "order": ["map_e"],
1265 "map_e": {"br": "2001:db8::1", "rules": []}
1266 }"#,
1267 )
1268 .unwrap();
1269
1270 assert!(response.select(&[Capability::DsLite]).is_none());
1271 }
1272
1273 #[test]
1274 fn rejects_null_for_an_optional_response_field() {
1275 let error = ProvisioningData::parse(
1276 r#"{
1277 "enabler_name": "example",
1278 "token": null,
1279 "order": []
1280 }"#,
1281 )
1282 .unwrap_err();
1283
1284 assert!(matches!(error, ProvisioningDataError::NullField("token")));
1285 }
1286
1287 #[test]
1288 fn rejects_non_object_method_payload() {
1289 let error = ProvisioningData::parse(
1290 r#"{
1291 "enabler_name": "example",
1292 "order": ["dslite"],
1293 "dslite": "invalid"
1294 }"#,
1295 )
1296 .unwrap_err();
1297
1298 assert!(matches!(
1299 error,
1300 ProvisioningDataError::InvalidOfferShape(Capability::DsLite)
1301 ));
1302 }
1303
1304 #[test]
1305 fn accepts_ipip_array_payload() {
1306 let response = ProvisioningData::parse(
1307 r#"{
1308 "enabler_name": "example",
1309 "order": ["ipip"],
1310 "ipip": [{
1311 "ipv6_local": "2001:db8:1::1",
1312 "ipv6_remote": "2001:db8:2::1",
1313 "ipv4": "192.0.2.0/29"
1314 }]
1315 }"#,
1316 )
1317 .unwrap();
1318
1319 assert_eq!(
1320 response.offer(Capability::IpIp),
1321 Some(&serde_json::json!([{
1322 "ipv6_local": "2001:db8:1::1",
1323 "ipv6_remote": "2001:db8:2::1",
1324 "ipv4": "192.0.2.0/29"
1325 }]))
1326 );
1327 }
1328
1329 #[test]
1330 fn rejects_ipip_object_payload() {
1331 let error = ProvisioningData::parse(
1332 r#"{
1333 "enabler_name": "example",
1334 "order": ["ipip"],
1335 "ipip": {"ipv6_remote": "2001:db8:2::1"}
1336 }"#,
1337 )
1338 .unwrap_err();
1339
1340 assert!(matches!(
1341 error,
1342 ProvisioningDataError::InvalidOfferShape(Capability::IpIp)
1343 ));
1344 }
1345
1346 #[test]
1347 fn rejects_non_object_entry_in_ipip_array() {
1348 let error = ProvisioningData::parse(
1349 r#"{
1350 "enabler_name": "example",
1351 "order": ["ipip"],
1352 "ipip": ["invalid"]
1353 }"#,
1354 )
1355 .unwrap_err();
1356
1357 assert!(matches!(
1358 error,
1359 ProvisioningDataError::InvalidOfferShape(Capability::IpIp)
1360 ));
1361 }
1362
1363 #[test]
1364 fn rejects_an_ordered_capability_without_a_payload() {
1365 let error = ProvisioningData::parse(
1366 r#"{
1367 "enabler_name": "example",
1368 "order": ["dslite"]
1369 }"#,
1370 )
1371 .unwrap_err();
1372
1373 assert!(matches!(
1374 error,
1375 ProvisioningDataError::MissingOffer(Capability::DsLite)
1376 ));
1377 }
1378
1379 #[test]
1380 fn validates_ttl_and_token_in_a_response() {
1381 let ttl_error = ProvisioningData::parse(
1382 r#"{
1383 "enabler_name": "example",
1384 "ttl": 604801,
1385 "order": []
1386 }"#,
1387 )
1388 .unwrap_err();
1389 let token_error = ProvisioningData::parse(
1390 r#"{
1391 "enabler_name": "example",
1392 "token": "not-a-token",
1393 "order": []
1394 }"#,
1395 )
1396 .unwrap_err();
1397
1398 assert!(matches!(ttl_error, ProvisioningDataError::Ttl(_)));
1399 assert!(matches!(token_error, ProvisioningDataError::Token(_)));
1400 }
1401
1402 #[test]
1403 fn builds_valid_provisioning_request() {
1404 let request = valid_request();
1405
1406 assert_eq!(request.vendor_id().as_str(), "000000");
1407 assert_eq!(request.product().as_str(), "dslite-b4");
1408 assert_eq!(request.version().as_str(), "0_1_0");
1409 assert_eq!(request.capabilities(), [Capability::DsLite]);
1410 assert_eq!(request.token(), None);
1411 }
1412
1413 #[test]
1414 fn accepts_multiple_capabilities_and_a_token() {
1415 let request = ProvisioningRequest::new(
1416 "acde48-v6pc_swg_hgw".parse().unwrap(),
1417 "V6MIG-ROUTER".parse().unwrap(),
1418 "1_32".parse().unwrap(),
1419 vec![Capability::MapE, Capability::DsLite, Capability::Lw4o6],
1420 Some(TOKEN.parse().unwrap()),
1421 None,
1422 )
1423 .unwrap();
1424
1425 assert_eq!(
1426 request.capabilities(),
1427 [Capability::MapE, Capability::DsLite, Capability::Lw4o6]
1428 );
1429 assert_eq!(request.token(), Some(TOKEN));
1430 }
1431
1432 #[test]
1433 fn parses_valid_token() {
1434 let token: Token = TOKEN.parse().unwrap();
1435
1436 assert_eq!(token.as_str(), TOKEN);
1437 }
1438
1439 #[test]
1440 fn rejects_invalid_token_formats() {
1441 let invalid_tokens = [
1442 "0".repeat(63),
1443 "0".repeat(65),
1444 format!("A{}", "0".repeat(63)),
1445 format!("g{}", "0".repeat(63)),
1446 ];
1447
1448 for token in invalid_tokens {
1449 let error = token.parse::<Token>().unwrap_err();
1450
1451 assert_eq!(error, TokenError::InvalidFormat);
1452 }
1453 }
1454
1455 #[test]
1456 fn redacts_tokens_in_debug_output() {
1457 let token: Token = TOKEN.parse().unwrap();
1458
1459 let debug = format!("{token:?}");
1460
1461 assert_eq!(debug, "Token([redacted])");
1462 }
1463
1464 #[test]
1465 fn rejects_invalid_credentials() {
1466 let invalid_user =
1467 Credentials::unrestricted("user!".to_string(), "pass".to_string()).unwrap_err();
1468 let invalid_password =
1469 Credentials::unrestricted("user".to_string(), "pass!".to_string()).unwrap_err();
1470 let invalid_server_name = Credentials::for_server(
1471 "user".to_string(),
1472 "pass".to_string(),
1473 "[2001:db8::1".to_string(),
1474 )
1475 .unwrap_err();
1476
1477 assert_eq!(invalid_user, CredentialsError::InvalidUser);
1478 assert_eq!(invalid_password, CredentialsError::InvalidPassword);
1479 assert_eq!(
1480 invalid_server_name,
1481 CredentialsError::InvalidExpectedServerName
1482 );
1483 }
1484
1485 #[test]
1486 fn rejects_invalid_vendor_id() {
1487 let error = "not-an-oui".parse::<VendorId>().unwrap_err();
1488
1489 assert_eq!(error, VendorIdError::InvalidFormat);
1490 }
1491
1492 #[test]
1493 fn rejects_invalid_product() {
1494 let error = "dslite b4".parse::<Product>().unwrap_err();
1495
1496 assert_eq!(error, ProductError::InvalidFormat);
1497 }
1498
1499 #[test]
1500 fn rejects_invalid_version() {
1501 let error = "0.1.0".parse::<FirmwareVersion>().unwrap_err();
1502
1503 assert_eq!(error, FirmwareVersionError::InvalidFormat);
1504 }
1505
1506 #[test]
1507 fn rejects_empty_capabilities() {
1508 let error =
1509 ProvisioningRequest::new(vendor_id(), product(), version(), Vec::new(), None, None)
1510 .unwrap_err();
1511
1512 assert_eq!(error, ProvisioningRequestError::EmptyCapabilities);
1513 }
1514
1515 #[test]
1516 fn rejects_duplicate_capabilities() {
1517 let error = ProvisioningRequest::new(
1518 vendor_id(),
1519 product(),
1520 version(),
1521 vec![Capability::DsLite, Capability::DsLite],
1522 None,
1523 None,
1524 )
1525 .unwrap_err();
1526
1527 assert_eq!(error, ProvisioningRequestError::DuplicateCapability);
1528 }
1529
1530 #[test]
1531 fn parses_v6connect_bootstrap_record() {
1532 let bootstrap = Bootstrap::parse(V6CONNECT_BOOTSTRAP).unwrap();
1533
1534 assert_eq!(
1535 bootstrap.endpoint().as_str(),
1536 "https://prod.v6mig.v6connect.net/cpe/v1/config"
1537 );
1538 assert_eq!(bootstrap.tls_policy(), TlsPolicy::ValidateCertificate);
1539 }
1540
1541 #[test]
1542 fn accepts_http_without_tls_validation() {
1543 let bootstrap = Bootstrap::parse("v=v6mig-1 url=http://vne.example/rule.cgi t=a").unwrap();
1544
1545 assert_eq!(bootstrap.endpoint().scheme(), "http");
1546 assert_eq!(bootstrap.tls_policy(), TlsPolicy::NoCertificateValidation);
1547 }
1548
1549 #[test]
1550 fn builds_provisioning_url() {
1551 let bootstrap = Bootstrap::parse(V6CONNECT_BOOTSTRAP).unwrap();
1552 let request = valid_request();
1553
1554 let pairs: Vec<_> = bootstrap
1555 .provisioning_url(&request)
1556 .unwrap()
1557 .query_pairs()
1558 .into_owned()
1559 .collect();
1560
1561 assert_eq!(
1562 pairs,
1563 [
1564 ("vendorid".to_string(), "000000".to_string()),
1565 ("product".to_string(), "dslite-b4".to_string()),
1566 ("version".to_string(), "0_1_0".to_string()),
1567 ("capability".to_string(), "dslite".to_string()),
1568 ]
1569 );
1570 }
1571
1572 #[test]
1573 fn preserves_existing_query_pairs_and_appends_token() {
1574 let bootstrap =
1575 Bootstrap::parse("v=v6mig-1 url=https://vne.example/rule.cgi?provider=example t=b")
1576 .unwrap();
1577 let request = ProvisioningRequest::new(
1578 vendor_id(),
1579 product(),
1580 version(),
1581 vec![Capability::MapE, Capability::DsLite],
1582 Some(TOKEN.parse().unwrap()),
1583 None,
1584 )
1585 .unwrap();
1586
1587 let pairs: Vec<_> = bootstrap
1588 .provisioning_url(&request)
1589 .unwrap()
1590 .query_pairs()
1591 .into_owned()
1592 .collect();
1593
1594 assert_eq!(
1595 pairs,
1596 [
1597 ("provider".to_string(), "example".to_string()),
1598 ("vendorid".to_string(), "000000".to_string()),
1599 ("product".to_string(), "dslite-b4".to_string()),
1600 ("version".to_string(), "0_1_0".to_string()),
1601 ("capability".to_string(), "map_e,dslite".to_string()),
1602 ("token".to_string(), TOKEN.to_string()),
1603 ]
1604 );
1605 }
1606
1607 #[test]
1608 fn sends_credentials_without_expected_server_name() {
1609 let bootstrap = Bootstrap::parse(V6CONNECT_BOOTSTRAP).unwrap();
1610 let request = ProvisioningRequest::new(
1611 vendor_id(),
1612 product(),
1613 version(),
1614 vec![Capability::DsLite],
1615 None,
1616 Some(Credentials::unrestricted("user".to_string(), "pass".to_string()).unwrap()),
1617 )
1618 .unwrap();
1619
1620 let pairs: Vec<_> = bootstrap
1621 .provisioning_url(&request)
1622 .unwrap()
1623 .query_pairs()
1624 .into_owned()
1625 .collect();
1626
1627 assert!(pairs.contains(&("user".to_string(), "user".to_string())));
1628 assert!(pairs.contains(&("pass".to_string(), "pass".to_string())));
1629 }
1630
1631 #[test]
1632 fn sends_credentials_when_expected_server_name_matches_validated_https() {
1633 let bootstrap = Bootstrap::parse(V6CONNECT_BOOTSTRAP).unwrap();
1634 let request = ProvisioningRequest::new(
1635 vendor_id(),
1636 product(),
1637 version(),
1638 vec![Capability::DsLite],
1639 None,
1640 Some(credentials_for_server("prod.v6mig.v6connect.net")),
1641 )
1642 .unwrap();
1643
1644 assert!(bootstrap.provisioning_url(&request).is_ok());
1645 }
1646
1647 #[test]
1648 fn rejects_credentials_for_unvalidated_or_unexpected_bootstrap() {
1649 let request_with_expected_server = ProvisioningRequest::new(
1650 vendor_id(),
1651 product(),
1652 version(),
1653 vec![Capability::DsLite],
1654 None,
1655 Some(credentials_for_server("provision.example")),
1656 )
1657 .unwrap();
1658 let http = Bootstrap::parse("v=v6mig-1 url=http://provision.example/rule.cgi t=a").unwrap();
1659 let unvalidated_https =
1660 Bootstrap::parse("v=v6mig-1 url=https://provision.example/rule.cgi t=a").unwrap();
1661 let unexpected_host =
1662 Bootstrap::parse("v=v6mig-1 url=https://other.example/rule.cgi t=b").unwrap();
1663
1664 assert_eq!(
1665 http.provisioning_url(&request_with_expected_server),
1666 Err(ProvisioningUrlError::CredentialsRequireHttps)
1667 );
1668 assert_eq!(
1669 unvalidated_https.provisioning_url(&request_with_expected_server),
1670 Err(ProvisioningUrlError::CredentialsRequireCertificateValidation)
1671 );
1672 assert_eq!(
1673 unexpected_host.provisioning_url(&request_with_expected_server),
1674 Err(ProvisioningUrlError::UnexpectedProvisioningHost)
1675 );
1676 }
1677
1678 #[test]
1679 fn rejects_missing_url_field() {
1680 let error = Bootstrap::parse("v=v6mig-1").unwrap_err();
1681
1682 assert!(matches!(error, BootstrapError::MissingField(_)));
1683 }
1684
1685 #[test]
1686 fn rejects_fields_out_of_order() {
1687 let error = Bootstrap::parse("url=https://vne.example/rule.cgi v=v6mig-1 t=b").unwrap_err();
1688
1689 assert!(matches!(error, BootstrapError::MalformedField(_, _)));
1690 }
1691
1692 #[test]
1693 fn rejects_unsupported_version() {
1694 let error = Bootstrap::parse("v=v6mig-2 url=https://vne.example/rule.cgi t=b").unwrap_err();
1695
1696 assert!(matches!(error, BootstrapError::UnsupportedVersion(_)));
1697 }
1698
1699 #[test]
1700 fn rejects_non_http_url_scheme() {
1701 let error = Bootstrap::parse("v=v6mig-1 url=ftp://vne.example/rule.cgi t=a").unwrap_err();
1702
1703 assert!(matches!(error, BootstrapError::InvalidUrl(_, _)));
1704 }
1705
1706 #[test]
1707 fn rejects_http_with_tls_validation() {
1708 let error = Bootstrap::parse("v=v6mig-1 url=http://vne.example/rule.cgi t=b").unwrap_err();
1709
1710 assert!(matches!(error, BootstrapError::InvalidTlsForHttp));
1711 }
1712
1713 #[test]
1714 fn rejects_extra_fields() {
1715 let error = Bootstrap::parse("v=v6mig-1 url=https://vne.example/rule.cgi t=b extra=value")
1716 .unwrap_err();
1717
1718 assert!(matches!(error, BootstrapError::InvalidRecord(_)));
1719 }
1720
1721 #[test]
1722 fn rejects_ipv4_literal_provisioning_url() {
1723 let error = Bootstrap::parse("v=v6mig-1 url=https://192.0.2.1/provision t=b").unwrap_err();
1724
1725 assert!(matches!(error, BootstrapError::Ipv4EndpointNotAllowed));
1726 }
1727
1728 #[test]
1729 fn sets_and_clears_provisioning_request_token() {
1730 let mut request = valid_request();
1731 assert_eq!(request.token(), None);
1732
1733 request.set_token(Some(TOKEN.parse().unwrap()));
1734 assert_eq!(request.token(), Some(TOKEN));
1735
1736 request.set_token(None);
1737 assert_eq!(request.token(), None);
1738 }
1739}