tor_guardmgr/bridge/
config.rs

1//! Configuration logic and types for bridges.
2
3use 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/// A relay not listed on the main tor network, used for anticensorship.
30///
31/// This object represents a bridge as configured by the user or by software
32/// running on the user's behalf.
33///
34/// # Pieces of a bridge configuration.
35///
36/// A bridge configuration contains:
37///   * Optionally, the name of a pluggable transport (q.v.) to use.
38///   * Zero or more addresses at which to contact the bridge.
39///     These can either be regular IP addresses, hostnames, or arbitrary strings
40///     to be interpreted by the pluggable transport.
41///   * One or more cryptographic [identities](tor_linkspec::RelayId) for the bridge.
42///   * Zero or more optional "key=value" string parameters to pass to the pluggable
43///     transport when contacting to this bridge.
44///
45/// # String representation
46///
47/// Can be parsed from, and represented as, a "bridge line" string,
48/// using the [`FromStr`] and [`Display`] implementations.
49///
50/// The syntax supported is a sequence of words,
51/// separated by ASCII whitespace,
52/// in the following order:
53///
54///  * Optionally, the word `Bridge` (or a case variant thereof).
55///    (`Bridge` is not part of a bridge line, but is ignored here
56///    for convenience when copying a line out of a C Tor `torrc`.)
57///
58///  * Optionally, the name of the pluggable transport to use.
59///    If not supplied, Arti will make the connection directly, itself.
60///
61///  * The `Host:ORPort` to connect to.
62///    `Host` can be an IPv4 address, or an IPv6 address in brackets `[ ]`.
63///    When a pluggable transport is in use, `Host` can also be a hostname;
64///    or
65///    if the transport supports operating without a specified address.
66///    `Host:ORPort` can be omitted and replaced with `-`.
67///
68///  * One or more identity key fingerprints,
69///    each in one of the supported (RSA or ed25519) fingerprint formats.
70///    Currently, supplying an RSA key is required; an ed25519 key is optional.
71///
72///  * When a pluggable transport is in use,
73///    zero or more `key=value` parameters to pass to the transport
74///    (smuggled in the SOCKS handshake, as described in the Tor PT specification).
75///
76/// This type is cheap to clone: it is a newtype around an `Arc`.
77#[derive(Debug, Clone, Eq, PartialEq, Hash)]
78pub struct BridgeConfig(Arc<Inner>);
79
80/// Configuration for a bridge - actual data
81#[derive(Debug, Clone, Eq, PartialEq, Hash)]
82struct Inner {
83    /// Address and transport via which the bridge can be reached, and
84    /// the parameters for those transports.
85    ///
86    /// Restriction: This `addrs` may NOT contain more than one address,
87    /// and it must be a variant supported by the code in this crate:
88    /// ie, currently, `Direct` or `Pluggable`.
89    addrs: ChannelMethod,
90
91    /// The RSA identity of the bridge.
92    rsa_id: RsaIdentity,
93
94    /// The Ed25519 identity of the bridge.
95    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/// Builder for a `BridgeConfig`.
124///
125/// Construct this with [`BridgeConfigBuilder::default()`] or [`BridgeConfig::builder()`],
126/// call setter methods, and then call `build().`
127//
128// `BridgeConfig` contains a `ChannelMethod`.  This is convenient for its users,
129// but means we can't use `#[derive(Builder)]` to autogenerate this.
130#[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    /// The `PtTransportName`, but not yet parsed or checked.
135    ///
136    /// `""` and `"-"` and `"bridge"` all mean "do not use a pluggable transport".
137    transport: Option<String>,
138
139    /// Host:ORPort
140    ///
141    /// When using a pluggable transport, only one address is allowed.
142    addrs: Option<Vec<BridgeAddr>>,
143
144    /// IDs
145    ///
146    /// No more than one ID of each type is permitted.
147    ids: Option<Vec<RelayId>>,
148
149    /// Settings (for the transport)
150    settings: Option<Vec<(String, String)>>,
151}
152}
153impl_standard_builder! { BridgeConfig: !Default }
154
155/// serde representation of a `BridgeConfigBuilder`
156#[derive(Serialize, Deserialize)]
157#[serde(untagged)]
158enum BridgeConfigBuilderSerde {
159    /// We understand a bridge line
160    BridgeLine(String),
161    /// We understand a dictionary matching BridgeConfigBuilder
162    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        // Try to serialize as a bridge line if we can
180        match input.build() {
181            Ok(bridge) => BridgeLine(bridge.to_string()),
182            Err(_) => Dict(input),
183        }
184    }
185}
186
187impl BridgeConfigBuilder {
188    /// Set the transport protocol name (eg, a pluggable transport) to use.
189    ///
190    /// The empty string `""`, a single hyphen `"-"`, and the word `"bridge"`,
191    /// all mean to connect directly;
192    /// i.e., passing one of this is equivalent to
193    /// calling [`direct()`](BridgeConfigBuilder::direct).
194    ///
195    /// The value is not checked at this point.
196    pub fn transport(&mut self, transport: impl Into<String>) -> &mut Self {
197        self.transport = Some(transport.into());
198        self
199    }
200
201    /// Specify to use a direct connection.
202    pub fn direct(&mut self) -> &mut Self {
203        self.transport("")
204    }
205
206    /// Add a pluggable transport setting
207    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    /// Inspect the transport name (ie, the protocol)
213    ///
214    /// Has not necessarily been validated, so not a `PtTransportName`.
215    /// If none has yet been specified, returns `None`.
216    pub fn get_transport(&self) -> Option<&str> {
217        self.transport.as_deref()
218    }
219}
220
221impl BridgeConfigBuilder {
222    /// Build a `BridgeConfig`
223    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        // Error construction helpers
229        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        // This match seems redundant, but it allows us to apply #[cfg] to the branches,
249        // which isn't possible with `if ... else ...`.
250        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)] // for consistency
259                let addrs = addrs.iter().filter_map(|ba| {
260                    #[allow(clippy::redundant_pattern_matching)] // for consistency
261                    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                    // Using PtTargetSettings TryFrom would prevent us reporting the index i
298                    target
299                        .push_setting(k, v)
300                        .map_err(|e| invalid(format!("settings.{}", i), &e))?;
301                }
302                ChannelMethod::Pluggable(target)
303            }
304
305            () => {
306                // With current code, this can only happen if tor-linkspec has pluggable
307                // transports enabled, but we don't.  But if `TransportId` gains other
308                // inner variants, it would trigger.
309                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        /// Helper to store an id in `rsa_id` or `ed_id`
321        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
366/// `BridgeConfigBuilder` parses the same way as `BridgeConfig`
367//
368// We implement it this way round (rather than having the `impl FromStr for BridgeConfig`
369// call this and then `build`, because the `BridgeConfig` parser
370// does a lot of bespoke checking of the syntax and semantics.
371// Doing it the other way, we'd have to unwrap a supposedly-never-existing `ConfigBuildError`,
372// in `BridgeConfig`'s `FromStr` impl.
373impl 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                // TODO transport.to_string() clones transport and then drops it
394                // PtTransportName::into_inner ought to exist but was deleted
395                // in 119e5f6f754251e0d2db7731f9a7044764f4653e
396                (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        // This implements the parsing of bridge lines.
446        // Refer to the specification in the rustdoc comment for `Bridge`.
447
448        //  * Optionally, the word `Bridge` ...
449
450        let bridge_word = s.peek().ok_or(BPE::Empty)?;
451        if bridge_word.eq_ignore_ascii_case("bridge") {
452            s.next();
453        }
454
455        //  * Optionally, the name of the pluggable transport to use.
456        //  * The `Host:ORPort` to connect to.
457
458        #[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                // Not a PT name.  Hope it's an address:port.
463                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        //  * One or more identity key fingerprints,
495
496        let mut rsa_id = None;
497        let mut ed_id = None;
498
499        while let Some(word) = s.peek() {
500            // Helper to generate the errors if the same key type is specified more than once
501            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                        // Not a fingerprint, then, but a key=value.
515                        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        //  * When a pluggable transport is in use,
534        //    zero or more `key=value` parameters to pass to the transport
535
536        #[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        //  * Optionally, the name of the pluggable transport to use.
577        //  * The `Host:ORPort` to connect to.
578
579        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                // This shouldn't happen, but panicking seems worse than outputting this
597                write!(f, "[unsupported channel method, cannot display properly]")?;
598                return Ok(());
599            }
600        };
601
602        //  * One or more identity key fingerprints,
603
604        write!(f, " {}", rsa_id)?;
605        if let Some(ed_id) = ed_id {
606            write!(f, " ed25519:{}", ed_id)?;
607        }
608
609        //  * When a pluggable transport is in use,
610        //    zero or more `key=value` parameters to pass to the transport
611
612        #[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    // @@ begin test lint list maintained by maint/add_warning @@
627    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
639    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        // example from https://tb-manual.torproject.org/bridges/, with cert= truncated
696        #[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            // Some Rust versions say "invalid socket address syntax",
776            // some "invalid IP address syntax"
777            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            // This is a kludge.  When we serialize `Option<Vec<_>>` as JSON,
903            // we get a `Null` entry.  These `Null`s aren't in our test cases and we don't
904            // really want them, although it's OK that they're there in the JSON.
905            // The TOML serialization omits them completely, though.
906            // So, we serialize the builder as TOML, and then convert the TOML to JSON Value.
907            // That launders out the `Null`s and gives us the same Value as our original JSON.
908            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}