1use std::fmt::{self, Display};
4use std::iter;
5use std::net::SocketAddr;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use itertools::{chain, Itertools};
10use serde::{Deserialize, Serialize};
11
12use tor_basic_utils::derive_serde_raw;
13use tor_config::define_list_builder_accessors;
14use tor_config::{impl_standard_builder, ConfigBuildError};
15use tor_linkspec::RelayId;
16use tor_linkspec::TransportId;
17use tor_linkspec::{ChanTarget, ChannelMethod, HasChanMethod};
18use tor_linkspec::{HasAddrs, HasRelayIds, RelayIdRef, RelayIdType};
19use tor_llcrypto::pk::{ed25519::Ed25519Identity, rsa::RsaIdentity};
20
21use tor_linkspec::BridgeAddr;
22
23#[cfg(feature = "pt-client")]
24use tor_linkspec::{PtTarget, PtTargetAddr};
25
26mod err;
27pub use err::BridgeParseError;
28
29#[derive(Debug, Clone, Eq, PartialEq, Hash)]
78pub struct BridgeConfig(Arc<Inner>);
79
80#[derive(Debug, Clone, Eq, PartialEq, Hash)]
82struct Inner {
83 addrs: ChannelMethod,
90
91 rsa_id: RsaIdentity,
93
94 ed_id: Option<Ed25519Identity>,
96}
97
98impl HasRelayIds for BridgeConfig {
99 fn identity(&self, key_type: RelayIdType) -> Option<RelayIdRef<'_>> {
100 match key_type {
101 RelayIdType::Ed25519 => self.0.ed_id.as_ref().map(RelayIdRef::Ed25519),
102 RelayIdType::Rsa => Some(RelayIdRef::Rsa(&self.0.rsa_id)),
103 _ => None,
104 }
105 }
106}
107
108impl HasChanMethod for BridgeConfig {
109 fn chan_method(&self) -> ChannelMethod {
110 self.0.addrs.clone()
111 }
112}
113
114impl HasAddrs for BridgeConfig {
115 fn addrs(&self) -> &[SocketAddr] {
116 self.0.addrs.addrs()
117 }
118}
119
120impl ChanTarget for BridgeConfig {}
121
122derive_serde_raw! {
123#[derive(Deserialize, Serialize, Default, Clone, Debug)]
131#[serde(try_from="BridgeConfigBuilderSerde", into="BridgeConfigBuilderSerde")]
132#[cfg_attr(test, derive(Eq, PartialEq))]
133pub struct BridgeConfigBuilder = "BridgeConfigBuilder" {
134 transport: Option<String>,
138
139 addrs: Option<Vec<BridgeAddr>>,
143
144 ids: Option<Vec<RelayId>>,
148
149 settings: Option<Vec<(String, String)>>,
151}
152}
153impl_standard_builder! { BridgeConfig: !Default }
154
155#[derive(Serialize, Deserialize)]
157#[serde(untagged)]
158enum BridgeConfigBuilderSerde {
159 BridgeLine(String),
161 Dict(#[serde(with = "BridgeConfigBuilder_Raw")] BridgeConfigBuilder),
163}
164
165impl TryFrom<BridgeConfigBuilderSerde> for BridgeConfigBuilder {
166 type Error = BridgeParseError;
167 fn try_from(input: BridgeConfigBuilderSerde) -> Result<Self, Self::Error> {
168 use BridgeConfigBuilderSerde::*;
169 match input {
170 BridgeLine(s) => s.parse(),
171 Dict(d) => Ok(d),
172 }
173 }
174}
175
176impl From<BridgeConfigBuilder> for BridgeConfigBuilderSerde {
177 fn from(input: BridgeConfigBuilder) -> BridgeConfigBuilderSerde {
178 use BridgeConfigBuilderSerde::*;
179 match input.build() {
181 Ok(bridge) => BridgeLine(bridge.to_string()),
182 Err(_) => Dict(input),
183 }
184 }
185}
186
187impl BridgeConfigBuilder {
188 pub fn transport(&mut self, transport: impl Into<String>) -> &mut Self {
197 self.transport = Some(transport.into());
198 self
199 }
200
201 pub fn direct(&mut self) -> &mut Self {
203 self.transport("")
204 }
205
206 pub fn push_setting(&mut self, k: impl Into<String>, v: impl Into<String>) -> &mut Self {
208 self.settings().push((k.into(), v.into()));
209 self
210 }
211
212 pub fn get_transport(&self) -> Option<&str> {
217 self.transport.as_deref()
218 }
219}
220
221impl BridgeConfigBuilder {
222 pub fn build(&self) -> Result<BridgeConfig, ConfigBuildError> {
224 let transport = self.transport.as_deref().unwrap_or_default();
225 let addrs = self.addrs.as_deref().unwrap_or_default();
226 let settings = self.settings.as_deref().unwrap_or_default();
227
228 let inconsist_transp = |field: &str, problem: &str| ConfigBuildError::Inconsistent {
230 fields: vec![field.into(), "transport".into()],
231 problem: problem.into(),
232 };
233 let unsupported =
234 |field: String, problem: &dyn Display| ConfigBuildError::NoCompileTimeSupport {
235 field,
236 problem: problem.to_string(),
237 };
238 #[cfg_attr(not(feature = "pt-client"), allow(unused_variables))]
239 let invalid = |field: String, problem: &dyn Display| ConfigBuildError::Invalid {
240 field,
241 problem: problem.to_string(),
242 };
243
244 let transp: TransportId = transport
245 .parse()
246 .map_err(|e| invalid("transport".into(), &e))?;
247
248 let addrs = match () {
251 () if transp.is_builtin() => {
252 if !settings.is_empty() {
253 return Err(inconsist_transp(
254 "settings",
255 "Specified `settings` for a direct bridge connection",
256 ));
257 }
258 #[allow(clippy::unnecessary_filter_map)] let addrs = addrs.iter().filter_map(|ba| {
260 #[allow(clippy::redundant_pattern_matching)] if let Some(sa) = ba.as_socketaddr() {
262 Some(Ok(*sa))
263 } else if let Some(_) = ba.as_host_port() {
264 Some(Err(
265 "`addrs` contains hostname and port, but only numeric addresses are supported for a direct bridge connection",
266 ))
267 } else {
268 unreachable!("BridgeAddr is neither addr nor named")
269 }
270 }).collect::<Result<Vec<SocketAddr>,&str>>().map_err(|problem| inconsist_transp(
271 "addrs",
272 problem,
273 ))?;
274 if addrs.is_empty() {
275 return Err(inconsist_transp(
276 "addrs",
277 "Missing `addrs` for a direct bridge connection",
278 ));
279 }
280 ChannelMethod::Direct(addrs)
281 }
282
283 #[cfg(feature = "pt-client")]
284 () if transp.as_pluggable().is_some() => {
285 let transport = transp.into_pluggable().expect("became not pluggable!");
286 let addr =
287 match addrs {
288 [] => PtTargetAddr::None,
289 [addr] => Some(addr.clone()).into(),
290 [_, _, ..] => return Err(inconsist_transp(
291 "addrs",
292 "Transport (non-direct bridge) only supports a single nominal address",
293 )),
294 };
295 let mut target = PtTarget::new(transport, addr);
296 for (i, (k, v)) in settings.iter().enumerate() {
297 target
299 .push_setting(k, v)
300 .map_err(|e| invalid(format!("settings.{}", i), &e))?;
301 }
302 ChannelMethod::Pluggable(target)
303 }
304
305 () => {
306 return Err(unsupported(
310 "transport".into(),
311 &format_args!("support for selected transport '{}' disabled in tor-guardmgr cargo features",
312 transp),
313 ));
314 }
315 };
316
317 let mut rsa_id = None;
318 let mut ed_id = None;
319
320 fn store_id<T: Clone>(
322 u: &mut Option<T>,
323 desc: &str,
324 v: &T,
325 ) -> Result<(), ConfigBuildError> {
326 if u.is_some() {
327 Err(ConfigBuildError::Invalid {
328 field: "ids".into(),
329 problem: format!("multiple different ids of the same type ({})", desc),
330 })
331 } else {
332 *u = Some(v.clone());
333 Ok(())
334 }
335 }
336
337 for (i, id) in self.ids.as_deref().unwrap_or_default().iter().enumerate() {
338 match id {
339 RelayId::Rsa(rsa) => store_id(&mut rsa_id, "RSA", rsa)?,
340 RelayId::Ed25519(ed) => store_id(&mut ed_id, "ed25519", ed)?,
341 other => {
342 return Err(unsupported(
343 format!("ids.{}", i),
344 &format_args!("unsupported bridge id type {}", other.id_type()),
345 ))
346 }
347 }
348 }
349
350 let rsa_id = rsa_id.ok_or_else(|| ConfigBuildError::Invalid {
351 field: "ids".into(),
352 problem: "need an RSA identity".into(),
353 })?;
354
355 Ok(BridgeConfig(
356 Inner {
357 addrs,
358 rsa_id,
359 ed_id,
360 }
361 .into(),
362 ))
363 }
364}
365
366impl FromStr for BridgeConfigBuilder {
374 type Err = BridgeParseError;
375
376 fn from_str(s: &str) -> Result<Self, Self::Err> {
377 let bridge: Inner = s.parse()?;
378
379 let (transport, addrs, settings) = match bridge.addrs {
380 ChannelMethod::Direct(addrs) => (
381 "".into(),
382 addrs
383 .into_iter()
384 .map(BridgeAddr::new_addr_from_sockaddr)
385 .collect(),
386 vec![],
387 ),
388 #[cfg(feature = "pt-client")]
389 ChannelMethod::Pluggable(target) => {
390 let (transport, addr, settings) = target.into_parts();
391 let addr: Option<BridgeAddr> = addr.into();
392 let addrs = addr.into_iter().collect_vec();
393 (transport.to_string(), addrs, settings.into_inner())
397 }
398 other => {
399 return Err(BridgeParseError::UnsupportedChannelMethod {
400 method: Box::new(other),
401 });
402 }
403 };
404
405 let ids = chain!(
406 iter::once(bridge.rsa_id.into()),
407 bridge.ed_id.into_iter().map(Into::into),
408 )
409 .collect_vec();
410
411 Ok(BridgeConfigBuilder {
412 transport: Some(transport),
413 addrs: Some(addrs),
414 settings: Some(settings),
415 ids: Some(ids),
416 })
417 }
418}
419
420define_list_builder_accessors! {
421 struct BridgeConfigBuilder {
422 pub addrs: [BridgeAddr],
423 pub ids: [RelayId],
424 pub settings: [(String,String)],
425 }
426}
427
428impl FromStr for BridgeConfig {
429 type Err = BridgeParseError;
430
431 fn from_str(s: &str) -> Result<Self, Self::Err> {
432 let inner = s.parse()?;
433 Ok(BridgeConfig(Arc::new(inner)))
434 }
435}
436
437impl FromStr for Inner {
438 type Err = BridgeParseError;
439
440 fn from_str(s: &str) -> Result<Self, Self::Err> {
441 use BridgeParseError as BPE;
442
443 let mut s = s.trim().split_ascii_whitespace().peekable();
444
445 let bridge_word = s.peek().ok_or(BPE::Empty)?;
451 if bridge_word.eq_ignore_ascii_case("bridge") {
452 s.next();
453 }
454
455 #[cfg_attr(not(feature = "pt-client"), allow(unused_mut))]
459 let mut method = {
460 let word = s.next().ok_or(BPE::Empty)?;
461 if word.contains(':') {
462 let addr = word.parse().map_err(|addr_error| BPE::InvalidIpAddrOrPt {
464 word: word.to_string(),
465 addr_error,
466 })?;
467 ChannelMethod::Direct(vec![addr])
468 } else {
469 #[cfg(not(feature = "pt-client"))]
470 return Err(BPE::PluggableTransportsNotSupported {
471 word: word.to_string(),
472 });
473
474 #[cfg(feature = "pt-client")]
475 {
476 let pt_name = word.parse().map_err(|pt_error| BPE::InvalidPtOrAddr {
477 word: word.to_string(),
478 pt_error,
479 })?;
480 let addr = s
481 .next()
482 .map(|s| s.parse())
483 .transpose()
484 .map_err(|source| BPE::InvalidIPtHostAddr {
485 word: word.to_string(),
486 source,
487 })?
488 .unwrap_or(PtTargetAddr::None);
489 ChannelMethod::Pluggable(PtTarget::new(pt_name, addr))
490 }
491 }
492 };
493
494 let mut rsa_id = None;
497 let mut ed_id = None;
498
499 while let Some(word) = s.peek() {
500 let check_several = |was_some| {
502 if was_some {
503 Err(BPE::MultipleIdentitiesOfSameType {
504 word: word.to_string(),
505 })
506 } else {
507 Ok(())
508 }
509 };
510
511 match word.parse() {
512 Err(id_error) => {
513 if word.contains('=') {
514 break;
516 }
517 return Err(BPE::InvalidIdentityOrParameter {
518 word: word.to_string(),
519 id_error,
520 });
521 }
522 Ok(RelayId::Ed25519(id)) => check_several(ed_id.replace(id).is_some())?,
523 Ok(RelayId::Rsa(id)) => check_several(rsa_id.replace(id).is_some())?,
524 Ok(_) => {
525 return Err(BPE::UnsupportedIdentityType {
526 word: word.to_string(),
527 })?
528 }
529 }
530 s.next();
531 }
532
533 #[cfg(not(feature = "pt-client"))]
537 if s.next().is_some() {
538 return Err(BPE::DirectParametersNotAllowed);
539 }
540
541 #[cfg(feature = "pt-client")]
542 for word in s {
543 let (k, v) = word.split_once('=').ok_or_else(|| BPE::InvalidPtKeyValue {
544 word: word.to_string(),
545 })?;
546
547 match &mut method {
548 ChannelMethod::Direct(_) => return Err(BPE::DirectParametersNotAllowed),
549 ChannelMethod::Pluggable(t) => t.push_setting(k, v).map_err(|source| {
550 BPE::InvalidPluggableTransportSetting {
551 word: word.to_string(),
552 source,
553 }
554 })?,
555 other => panic!("made ourselves an unsupported ChannelMethod {:?}", other),
556 }
557 }
558
559 let rsa_id = rsa_id.ok_or(BPE::NoRsaIdentity)?;
560 Ok(Inner {
561 addrs: method,
562 rsa_id,
563 ed_id,
564 })
565 }
566}
567
568impl Display for BridgeConfig {
569 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
570 let Inner {
571 addrs,
572 rsa_id,
573 ed_id,
574 } = &*self.0;
575
576 let settings = match addrs {
580 ChannelMethod::Direct(a) => {
581 if a.len() == 1 {
582 write!(f, "{}", a[0])?;
583 } else {
584 panic!("Somehow created a Bridge config with multiple addrs.");
585 }
586 None
587 }
588
589 #[cfg(feature = "pt-client")]
590 ChannelMethod::Pluggable(target) => {
591 write!(f, "{} {}", target.transport(), target.addr())?;
592 Some(target.settings())
593 }
594
595 _ => {
596 write!(f, "[unsupported channel method, cannot display properly]")?;
598 return Ok(());
599 }
600 };
601
602 write!(f, " {}", rsa_id)?;
605 if let Some(ed_id) = ed_id {
606 write!(f, " ed25519:{}", ed_id)?;
607 }
608
609 #[cfg(not(feature = "pt-client"))]
613 let _: Option<()> = settings;
614
615 #[cfg(feature = "pt-client")]
616 for (k, v) in settings.into_iter().flatten() {
617 write!(f, " {}={}", k, v)?;
618 }
619
620 Ok(())
621 }
622}
623
624#[cfg(test)]
625mod test {
626 #![allow(clippy::bool_assert_comparison)]
628 #![allow(clippy::clone_on_copy)]
629 #![allow(clippy::dbg_macro)]
630 #![allow(clippy::mixed_attributes_style)]
631 #![allow(clippy::print_stderr)]
632 #![allow(clippy::print_stdout)]
633 #![allow(clippy::single_char_pattern)]
634 #![allow(clippy::unwrap_used)]
635 #![allow(clippy::unchecked_duration_subtraction)]
636 #![allow(clippy::useless_vec)]
637 #![allow(clippy::needless_pass_by_value)]
638 use super::*;
640
641 #[cfg(feature = "pt-client")]
642 fn mk_pt_target(name: &str, addr: PtTargetAddr, params: &[(&str, &str)]) -> ChannelMethod {
643 let mut target = PtTarget::new(name.parse().unwrap(), addr);
644 for &(k, v) in params {
645 target.push_setting(k, v).unwrap();
646 }
647 ChannelMethod::Pluggable(target)
648 }
649
650 fn mk_direct(s: &str) -> ChannelMethod {
651 ChannelMethod::Direct(vec![s.parse().unwrap()])
652 }
653
654 fn mk_rsa(s: &str) -> RsaIdentity {
655 match s.parse().unwrap() {
656 RelayId::Rsa(y) => y,
657 _ => panic!("not rsa {:?}", s),
658 }
659 }
660 fn mk_ed(s: &str) -> Ed25519Identity {
661 match s.parse().unwrap() {
662 RelayId::Ed25519(y) => y,
663 _ => panic!("not ed {:?}", s),
664 }
665 }
666
667 #[test]
668 fn bridge_lines() {
669 let chk = |sl: &[&str], exp: Inner| {
670 for s in sl {
671 let got: BridgeConfig = s.parse().expect(s);
672 assert_eq!(*got.0, exp, "{:?}", s);
673
674 let display = got.to_string();
675 assert_eq!(display, sl[0]);
676 }
677 };
678
679 let chk_e = |sl: &[&str], exp: &str| {
680 for s in sl {
681 let got: Result<BridgeConfig, _> = s.parse();
682 let got = got.expect_err(s);
683 let got_s = got.to_string();
684 assert!(
685 got_s.contains(exp),
686 "{:?} => {:?} ({}) not {}",
687 s,
688 &got,
689 &got_s,
690 exp
691 );
692 }
693 };
694
695 #[cfg(feature = "pt-client")]
697 chk(&[
698 "obfs4 38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
699 "obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
700 "Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
701 ], Inner {
702 addrs: mk_pt_target(
703 "obfs4",
704 PtTargetAddr::IpPort("38.229.33.83:80".parse().unwrap()),
705 &[
706 ("cert", "VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op" ),
707 ("iat-mode", "1"),
708 ],
709 ),
710 rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
711 ed_id: None,
712 });
713
714 #[cfg(feature = "pt-client")]
715 chk(&[
716 "obfs4 some-host:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE iat-mode=1",
717 "obfs4 some-host:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE 0BAC39417268B96B9F514E7F63FA6FBA1A788955 iat-mode=1",
718 ], Inner {
719 addrs: mk_pt_target(
720 "obfs4",
721 PtTargetAddr::HostPort("some-host".into(), 80),
722 &[
723 ("iat-mode", "1"),
724 ],
725 ),
726 rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
727 ed_id: Some(mk_ed("dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")),
728 });
729
730 chk(
731 &[
732 "38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955",
733 "Bridge 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
734 ],
735 Inner {
736 addrs: mk_direct("38.229.33.83:80"),
737 rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
738 ed_id: None,
739 },
740 );
741
742 chk(
743 &[
744 "[2001:db8::42]:123 $0bac39417268b96b9f514e7f63fa6fba1a788955",
745 "[2001:0db8::42]:123 $0bac39417268b96b9f514e7f63fa6fba1a788955",
746 ],
747 Inner {
748 addrs: mk_direct("[2001:0db8::42]:123"),
749 rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
750 ed_id: None,
751 },
752 );
753
754 chk(&[
755 "38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
756 "38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
757 ], Inner {
758 addrs: mk_direct("38.229.33.83:80"),
759 rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
760 ed_id: Some(mk_ed("dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")),
761 });
762
763 chk_e(
764 &[
765 "38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
766 "Bridge 38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
767 ],
768 "lacks specification of RSA identity key",
769 );
770
771 chk_e(&["", "bridge"], "Bridge line was empty");
772
773 chk_e(
774 &["999.329.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955"],
775 r#"Cannot parse "999.329.33.83:80" as direct bridge IpAddress:ORPort"#,
778 );
779
780 chk_e(
781 &[
782 "38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value",
783 "Bridge 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value",
784 ],
785 "Parameters supplied but not valid without a pluggable transport",
786 );
787
788 chk_e(
789 &[
790 "bridge bridge some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
791 "yikes! some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
792 ],
793 #[cfg(feature = "pt-client")]
794 r" is not a valid pluggable transport ID), nor as direct bridge IpAddress:ORPort",
795 #[cfg(not(feature = "pt-client"))]
796 "is not an IpAddress:ORPort), but support disabled in cargo features",
797 );
798
799 #[cfg(feature = "pt-client")]
800 chk_e(
801 &["obfs4 garbage 0BAC39417268B96B9F514E7F63FA6FBA1A788955"],
802 "as pluggable transport Host:ORPort",
803 );
804
805 #[cfg(feature = "pt-client")]
806 chk_e(
807 &["obfs4 some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value garbage"],
808 r#"Expected PT key=value parameter, found "garbage" (which lacks an equals sign"#,
809 );
810
811 #[cfg(feature = "pt-client")]
812 chk_e(
813 &["obfs4 some-host:80 garbage"],
814 r#"Cannot parse "garbage" as identity key (Invalid base64 data), or PT key=value"#,
815 );
816
817 chk_e(
818 &[
819 "38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 23AC39417268B96B9F514E7F63FA6FBA1A788955",
820 "38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE xGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
821 ],
822 "More than one identity of the same type specified",
823 );
824 }
825
826 #[test]
827 fn config_api() {
828 let chk_bridgeline = |line: &str, jsons: &[&str], f: &dyn Fn(&mut BridgeConfigBuilder)| {
829 eprintln!(" ---- chk_bridgeline ----\n{}", line);
830
831 let mut bcb = BridgeConfigBuilder::default();
832 f(&mut bcb);
833 let built = bcb.build().unwrap();
834 assert_eq!(&built, &line.parse::<BridgeConfig>().unwrap());
835
836 let parsed_b: BridgeConfigBuilder = line.parse().unwrap();
837 assert_eq!(&built, &parsed_b.build().unwrap());
838
839 let re_serialized = serde_json::to_value(&bcb).unwrap();
840 assert_eq!(re_serialized, serde_json::Value::String(line.to_string()));
841
842 for json in jsons {
843 let from_dict: BridgeConfigBuilder = serde_json::from_str(json).unwrap();
844 assert_eq!(&from_dict, &bcb);
845 assert_eq!(&built, &from_dict.build().unwrap());
846 }
847 };
848
849 chk_bridgeline(
850 "38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
851 &[r#"{
852 "addrs": ["38.229.33.83:80"],
853 "ids": ["ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
854 "$0bac39417268b96b9f514e7f63fa6fba1a788955"]
855 }"#],
856 &|bcb| {
857 bcb.addrs().push("38.229.33.83:80".parse().unwrap());
858 bcb.ids().push("ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE".parse().unwrap());
859 bcb.ids().push("$0bac39417268b96b9f514e7f63fa6fba1a788955".parse().unwrap());
860 }
861 );
862
863 #[cfg(feature = "pt-client")]
864 chk_bridgeline(
865 "obfs4 some-host:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 iat-mode=1",
866 &[r#"{
867 "transport": "obfs4",
868 "addrs": ["some-host:80"],
869 "ids": ["$0bac39417268b96b9f514e7f63fa6fba1a788955"],
870 "settings": [["iat-mode", "1"]]
871 }"#],
872 &|bcb| {
873 bcb.transport("obfs4");
874 bcb.addrs().push("some-host:80".parse().unwrap());
875 bcb.ids()
876 .push("$0bac39417268b96b9f514e7f63fa6fba1a788955".parse().unwrap());
877 bcb.push_setting("iat-mode", "1");
878 },
879 );
880
881 let chk_broken = |emsg: &str, jsons: &[&str], f: &dyn Fn(&mut BridgeConfigBuilder)| {
882 eprintln!(" ---- chk_bridgeline ----\n{:?}", emsg);
883
884 let mut bcb = BridgeConfigBuilder::default();
885 f(&mut bcb);
886
887 for json in jsons {
888 let from_dict: BridgeConfigBuilder = serde_json::from_str(json).unwrap();
889 assert_eq!(&from_dict, &bcb);
890 }
891
892 let err = bcb.build().expect_err("succeeded?!");
893 let got_emsg = err.to_string();
894 assert!(
895 got_emsg.contains(emsg),
896 "wrong error message: got_emsg={:?} err={:?} expected={:?}",
897 &got_emsg,
898 &err,
899 emsg,
900 );
901
902 let toml_got = toml::to_string(&bcb).unwrap();
909 let json_got: serde_json::Value = toml::from_str(&toml_got).unwrap();
910 let json_exp: serde_json::Value = serde_json::from_str(jsons[0]).unwrap();
911 assert_eq!(&json_got, &json_exp);
912 };
913
914 chk_broken(
915 "Specified `settings` for a direct bridge connection",
916 &[r#"{
917 "settings": [["hi","there"]]
918 }"#],
919 &|bcb| {
920 bcb.settings().push(("hi".into(), "there".into()));
921 },
922 );
923
924 #[cfg(not(feature = "pt-client"))]
925 chk_broken(
926 "Not compiled with pluggable transport support",
927 &[r#"{
928 "transport": "obfs4"
929 }"#],
930 &|bcb| {
931 bcb.transport("obfs4");
932 },
933 );
934
935 #[cfg(feature = "pt-client")]
936 chk_broken(
937 "only numeric addresses are supported for a direct bridge connection",
938 &[r#"{
939 "transport": "bridge",
940 "addrs": ["some-host:80"]
941 }"#],
942 &|bcb| {
943 bcb.transport("bridge");
944 bcb.addrs().push("some-host:80".parse().unwrap());
945 },
946 );
947
948 chk_broken(
949 "Missing `addrs` for a direct bridge connection",
950 &[r#"{
951 "transport": "-"
952 }"#],
953 &|bcb| {
954 bcb.transport("-");
955 },
956 );
957
958 #[cfg(feature = "pt-client")]
959 chk_broken(
960 "only supports a single nominal address",
961 &[r#"{
962 "transport": "obfs4",
963 "addrs": ["some-host:80", "38.229.33.83:80"]
964 }"#],
965 &|bcb| {
966 bcb.transport("obfs4");
967 bcb.addrs().push("some-host:80".parse().unwrap());
968 bcb.addrs().push("38.229.33.83:80".parse().unwrap());
969 },
970 );
971
972 chk_broken(
973 "multiple different ids of the same type (ed25519)",
974 &[r#"{
975 "addrs": ["38.229.33.83:80"],
976 "ids": ["ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
977 "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISA"]
978 }"#],
979 &|bcb| {
980 bcb.addrs().push("38.229.33.83:80".parse().unwrap());
981 bcb.ids().push(
982 "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"
983 .parse()
984 .unwrap(),
985 );
986 bcb.ids().push(
987 "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISA"
988 .parse()
989 .unwrap(),
990 );
991 },
992 );
993
994 chk_broken(
995 "need an RSA identity",
996 &[r#"{
997 "addrs": ["38.229.33.83:80"],
998 "ids": ["ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"]
999 }"#],
1000 &|bcb| {
1001 bcb.addrs().push("38.229.33.83:80".parse().unwrap());
1002 bcb.ids().push(
1003 "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"
1004 .parse()
1005 .unwrap(),
1006 );
1007 },
1008 );
1009 }
1010}