Skip to main content

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::{Itertools, chain};
10use serde::{Deserialize, Serialize};
11
12use tor_basic_utils::derive_serde_raw;
13use tor_config::define_list_builder_accessors;
14use tor_config::{ConfigBuildError, impl_standard_builder};
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) -> impl Iterator<Item = 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 = match addrs {
287                    [] => PtTargetAddr::None,
288                    [addr] => Some(addr.clone()).into(),
289                    [_, _, ..] => {
290                        return Err(inconsist_transp(
291                            "addrs",
292                            "Transport (non-direct bridge) only supports a single nominal address",
293                        ));
294                    }
295                };
296                let mut target = PtTarget::new(transport, addr);
297                for (i, (k, v)) in settings.iter().enumerate() {
298                    // Using PtTargetSettings TryFrom would prevent us reporting the index i
299                    target
300                        .push_setting(k, v)
301                        .map_err(|e| invalid(format!("settings.{}", i), &e))?;
302                }
303                ChannelMethod::Pluggable(target)
304            }
305
306            () => {
307                // With current code, this can only happen if tor-linkspec has pluggable
308                // transports enabled, but we don't.  But if `TransportId` gains other
309                // inner variants, it would trigger.
310                return Err(unsupported(
311                    "transport".into(),
312                    &format_args!(
313                        "support for selected transport '{}' disabled in tor-guardmgr cargo features",
314                        transp
315                    ),
316                ));
317            }
318        };
319
320        let mut rsa_id = None;
321        let mut ed_id = None;
322
323        /// Helper to store an id in `rsa_id` or `ed_id`
324        fn store_id<T: Clone>(
325            u: &mut Option<T>,
326            desc: &str,
327            v: &T,
328        ) -> Result<(), ConfigBuildError> {
329            if u.is_some() {
330                Err(ConfigBuildError::Invalid {
331                    field: "ids".into(),
332                    problem: format!("multiple different ids of the same type ({})", desc),
333                })
334            } else {
335                *u = Some(v.clone());
336                Ok(())
337            }
338        }
339
340        for (i, id) in self.ids.as_deref().unwrap_or_default().iter().enumerate() {
341            match id {
342                RelayId::Rsa(rsa) => store_id(&mut rsa_id, "RSA", rsa)?,
343                RelayId::Ed25519(ed) => store_id(&mut ed_id, "ed25519", ed)?,
344                other => {
345                    return Err(unsupported(
346                        format!("ids.{}", i),
347                        &format_args!("unsupported bridge id type {}", other.id_type()),
348                    ));
349                }
350            }
351        }
352
353        let rsa_id = rsa_id.ok_or_else(|| ConfigBuildError::Invalid {
354            field: "ids".into(),
355            problem: "need an RSA identity".into(),
356        })?;
357
358        Ok(BridgeConfig(
359            Inner {
360                addrs,
361                rsa_id,
362                ed_id,
363            }
364            .into(),
365        ))
366    }
367}
368
369/// `BridgeConfigBuilder` parses the same way as `BridgeConfig`
370//
371// We implement it this way round (rather than having the `impl FromStr for BridgeConfig`
372// call this and then `build`, because the `BridgeConfig` parser
373// does a lot of bespoke checking of the syntax and semantics.
374// Doing it the other way, we'd have to unwrap a supposedly-never-existing `ConfigBuildError`,
375// in `BridgeConfig`'s `FromStr` impl.
376impl FromStr for BridgeConfigBuilder {
377    type Err = BridgeParseError;
378
379    fn from_str(s: &str) -> Result<Self, Self::Err> {
380        let bridge: Inner = s.parse()?;
381
382        let (transport, addrs, settings) = match bridge.addrs {
383            ChannelMethod::Direct(addrs) => (
384                "".into(),
385                addrs
386                    .into_iter()
387                    .map(BridgeAddr::new_addr_from_sockaddr)
388                    .collect(),
389                vec![],
390            ),
391            #[cfg(feature = "pt-client")]
392            ChannelMethod::Pluggable(target) => {
393                let (transport, addr, settings) = target.into_parts();
394                let addr: Option<BridgeAddr> = addr.into();
395                let addrs = addr.into_iter().collect_vec();
396                // TODO transport.to_string() clones transport and then drops it
397                // PtTransportName::into_inner ought to exist but was deleted
398                // in 119e5f6f754251e0d2db7731f9a7044764f4653e
399                (transport.to_string(), addrs, settings.into_inner())
400            }
401            other => {
402                return Err(BridgeParseError::UnsupportedChannelMethod {
403                    method: Box::new(other),
404                });
405            }
406        };
407
408        let ids = chain!(
409            iter::once(bridge.rsa_id.into()),
410            bridge.ed_id.into_iter().map(Into::into),
411        )
412        .collect_vec();
413
414        Ok(BridgeConfigBuilder {
415            transport: Some(transport),
416            addrs: Some(addrs),
417            settings: Some(settings),
418            ids: Some(ids),
419        })
420    }
421}
422
423define_list_builder_accessors! {
424    struct BridgeConfigBuilder {
425        pub addrs: [BridgeAddr],
426        pub ids: [RelayId],
427        pub settings: [(String,String)],
428    }
429}
430
431impl FromStr for BridgeConfig {
432    type Err = BridgeParseError;
433
434    fn from_str(s: &str) -> Result<Self, Self::Err> {
435        let inner = s.parse()?;
436        Ok(BridgeConfig(Arc::new(inner)))
437    }
438}
439
440impl FromStr for Inner {
441    type Err = BridgeParseError;
442
443    fn from_str(s: &str) -> Result<Self, Self::Err> {
444        use BridgeParseError as BPE;
445
446        let mut s = s.trim().split_ascii_whitespace().peekable();
447
448        // This implements the parsing of bridge lines.
449        // Refer to the specification in the rustdoc comment for `Bridge`.
450
451        //  * Optionally, the word `Bridge` ...
452
453        let bridge_word = s.peek().ok_or(BPE::Empty)?;
454        if bridge_word.eq_ignore_ascii_case("bridge") {
455            s.next();
456        }
457
458        //  * Optionally, the name of the pluggable transport to use.
459        //  * The `Host:ORPort` to connect to.
460
461        #[cfg_attr(not(feature = "pt-client"), allow(unused_mut))]
462        let mut method = {
463            let word = s.next().ok_or(BPE::Empty)?;
464            if word.contains(':') {
465                // Not a PT name.  Hope it's an address:port.
466                let addr = word.parse().map_err(|addr_error| BPE::InvalidIpAddrOrPt {
467                    word: word.to_string(),
468                    addr_error,
469                })?;
470                ChannelMethod::Direct(vec![addr])
471            } else {
472                #[cfg(not(feature = "pt-client"))]
473                return Err(BPE::PluggableTransportsNotSupported {
474                    word: word.to_string(),
475                });
476
477                #[cfg(feature = "pt-client")]
478                {
479                    let pt_name = word.parse().map_err(|pt_error| BPE::InvalidPtOrAddr {
480                        word: word.to_string(),
481                        pt_error,
482                    })?;
483                    let addr = s
484                        .next()
485                        .map(|s| s.parse())
486                        .transpose()
487                        .map_err(|source| BPE::InvalidIPtHostAddr {
488                            word: word.to_string(),
489                            source,
490                        })?
491                        .unwrap_or(PtTargetAddr::None);
492                    ChannelMethod::Pluggable(PtTarget::new(pt_name, addr))
493                }
494            }
495        };
496
497        //  * One or more identity key fingerprints,
498
499        let mut rsa_id = None;
500        let mut ed_id = None;
501
502        while let Some(word) = s.peek() {
503            // Helper to generate the errors if the same key type is specified more than once
504            let check_several = |was_some| {
505                if was_some {
506                    Err(BPE::MultipleIdentitiesOfSameType {
507                        word: word.to_string(),
508                    })
509                } else {
510                    Ok(())
511                }
512            };
513
514            match word.parse() {
515                Err(id_error) => {
516                    if word.contains('=') {
517                        // Not a fingerprint, then, but a key=value.
518                        break;
519                    }
520                    return Err(BPE::InvalidIdentityOrParameter {
521                        word: word.to_string(),
522                        id_error,
523                    });
524                }
525                Ok(RelayId::Ed25519(id)) => check_several(ed_id.replace(id).is_some())?,
526                Ok(RelayId::Rsa(id)) => check_several(rsa_id.replace(id).is_some())?,
527                Ok(_) => {
528                    return Err(BPE::UnsupportedIdentityType {
529                        word: word.to_string(),
530                    })?;
531                }
532            }
533            s.next();
534        }
535
536        //  * When a pluggable transport is in use,
537        //    zero or more `key=value` parameters to pass to the transport
538
539        #[cfg(not(feature = "pt-client"))]
540        if s.next().is_some() {
541            return Err(BPE::DirectParametersNotAllowed);
542        }
543
544        #[cfg(feature = "pt-client")]
545        for word in s {
546            let (k, v) = word.split_once('=').ok_or_else(|| BPE::InvalidPtKeyValue {
547                word: word.to_string(),
548            })?;
549
550            match &mut method {
551                ChannelMethod::Direct(_) => return Err(BPE::DirectParametersNotAllowed),
552                ChannelMethod::Pluggable(t) => t.push_setting(k, v).map_err(|source| {
553                    BPE::InvalidPluggableTransportSetting {
554                        word: word.to_string(),
555                        source,
556                    }
557                })?,
558                other => panic!("made ourselves an unsupported ChannelMethod {:?}", other),
559            }
560        }
561
562        let rsa_id = rsa_id.ok_or(BPE::NoRsaIdentity)?;
563        Ok(Inner {
564            addrs: method,
565            rsa_id,
566            ed_id,
567        })
568    }
569}
570
571impl Display for BridgeConfig {
572    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
573        let Inner {
574            addrs,
575            rsa_id,
576            ed_id,
577        } = &*self.0;
578
579        //  * Optionally, the name of the pluggable transport to use.
580        //  * The `Host:ORPort` to connect to.
581
582        let settings = match addrs {
583            ChannelMethod::Direct(a) => {
584                if a.len() == 1 {
585                    write!(f, "{}", a[0])?;
586                } else {
587                    panic!("Somehow created a Bridge config with multiple addrs.");
588                }
589                None
590            }
591
592            #[cfg(feature = "pt-client")]
593            ChannelMethod::Pluggable(target) => {
594                write!(f, "{} {}", target.transport(), target.addr())?;
595                Some(target.settings())
596            }
597
598            _ => {
599                // This shouldn't happen, but panicking seems worse than outputting this
600                write!(f, "[unsupported channel method, cannot display properly]")?;
601                return Ok(());
602            }
603        };
604
605        //  * One or more identity key fingerprints,
606
607        write!(f, " {}", rsa_id)?;
608        if let Some(ed_id) = ed_id {
609            write!(f, " ed25519:{}", ed_id)?;
610        }
611
612        //  * When a pluggable transport is in use,
613        //    zero or more `key=value` parameters to pass to the transport
614
615        #[cfg(not(feature = "pt-client"))]
616        let _: Option<()> = settings;
617
618        #[cfg(feature = "pt-client")]
619        for (k, v) in settings.into_iter().flatten() {
620            write!(f, " {}={}", k, v)?;
621        }
622
623        Ok(())
624    }
625}
626
627#[cfg(test)]
628mod test {
629    // @@ begin test lint list maintained by maint/add_warning @@
630    #![allow(clippy::bool_assert_comparison)]
631    #![allow(clippy::clone_on_copy)]
632    #![allow(clippy::dbg_macro)]
633    #![allow(clippy::mixed_attributes_style)]
634    #![allow(clippy::print_stderr)]
635    #![allow(clippy::print_stdout)]
636    #![allow(clippy::single_char_pattern)]
637    #![allow(clippy::unwrap_used)]
638    #![allow(clippy::unchecked_time_subtraction)]
639    #![allow(clippy::useless_vec)]
640    #![allow(clippy::needless_pass_by_value)]
641    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
642    use super::*;
643
644    #[cfg(feature = "pt-client")]
645    fn mk_pt_target(name: &str, addr: PtTargetAddr, params: &[(&str, &str)]) -> ChannelMethod {
646        let mut target = PtTarget::new(name.parse().unwrap(), addr);
647        for &(k, v) in params {
648            target.push_setting(k, v).unwrap();
649        }
650        ChannelMethod::Pluggable(target)
651    }
652
653    fn mk_direct(s: &str) -> ChannelMethod {
654        ChannelMethod::Direct(vec![s.parse().unwrap()])
655    }
656
657    fn mk_rsa(s: &str) -> RsaIdentity {
658        match s.parse().unwrap() {
659            RelayId::Rsa(y) => y,
660            _ => panic!("not rsa {:?}", s),
661        }
662    }
663    fn mk_ed(s: &str) -> Ed25519Identity {
664        match s.parse().unwrap() {
665            RelayId::Ed25519(y) => y,
666            _ => panic!("not ed {:?}", s),
667        }
668    }
669
670    #[test]
671    fn bridge_lines() {
672        let chk = |sl: &[&str], exp: Inner| {
673            for s in sl {
674                let got: BridgeConfig = s.parse().expect(s);
675                assert_eq!(*got.0, exp, "{:?}", s);
676
677                let display = got.to_string();
678                assert_eq!(display, sl[0]);
679            }
680        };
681
682        let chk_e = |sl: &[&str], exp: &str| {
683            for s in sl {
684                let got: Result<BridgeConfig, _> = s.parse();
685                let got = got.expect_err(s);
686                let got_s = got.to_string();
687                assert!(
688                    got_s.contains(exp),
689                    "{:?} => {:?} ({}) not {}",
690                    s,
691                    &got,
692                    &got_s,
693                    exp
694                );
695            }
696        };
697
698        // example from https://tb-manual.torproject.org/bridges/, with cert= truncated
699        #[cfg(feature = "pt-client")]
700        chk(
701            &[
702                "obfs4 38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
703                "obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
704                "Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
705            ],
706            Inner {
707                addrs: mk_pt_target(
708                    "obfs4",
709                    PtTargetAddr::IpPort("38.229.33.83:80".parse().unwrap()),
710                    &[
711                        (
712                            "cert",
713                            "VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op",
714                        ),
715                        ("iat-mode", "1"),
716                    ],
717                ),
718                rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
719                ed_id: None,
720            },
721        );
722
723        #[cfg(feature = "pt-client")]
724        chk(
725            &[
726                "obfs4 some-host:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE iat-mode=1",
727                "obfs4 some-host:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE 0BAC39417268B96B9F514E7F63FA6FBA1A788955 iat-mode=1",
728            ],
729            Inner {
730                addrs: mk_pt_target(
731                    "obfs4",
732                    PtTargetAddr::HostPort("some-host".into(), 80),
733                    &[("iat-mode", "1")],
734                ),
735                rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
736                ed_id: Some(mk_ed("dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")),
737            },
738        );
739
740        chk(
741            &[
742                "38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955",
743                "Bridge 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
744            ],
745            Inner {
746                addrs: mk_direct("38.229.33.83:80"),
747                rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
748                ed_id: None,
749            },
750        );
751
752        chk(
753            &[
754                "[2001:db8::42]:123 $0bac39417268b96b9f514e7f63fa6fba1a788955",
755                "[2001:0db8::42]:123 $0bac39417268b96b9f514e7f63fa6fba1a788955",
756            ],
757            Inner {
758                addrs: mk_direct("[2001:0db8::42]:123"),
759                rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
760                ed_id: None,
761            },
762        );
763
764        chk(
765            &[
766                "38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
767                "38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
768            ],
769            Inner {
770                addrs: mk_direct("38.229.33.83:80"),
771                rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
772                ed_id: Some(mk_ed("dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")),
773            },
774        );
775
776        chk_e(
777            &[
778                "38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
779                "Bridge 38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
780            ],
781            "lacks specification of RSA identity key",
782        );
783
784        chk_e(&["", "bridge"], "Bridge line was empty");
785
786        chk_e(
787            &["999.329.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955"],
788            // Some Rust versions say "invalid socket address syntax",
789            // some "invalid IP address syntax"
790            r#"Cannot parse "999.329.33.83:80" as direct bridge IpAddress:ORPort"#,
791        );
792
793        chk_e(
794            &[
795                "38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value",
796                "Bridge 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value",
797            ],
798            "Parameters supplied but not valid without a pluggable transport",
799        );
800
801        chk_e(
802            &[
803                "bridge bridge some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
804                "yikes! some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
805            ],
806            #[cfg(feature = "pt-client")]
807            r" is not a valid pluggable transport ID), nor as direct bridge IpAddress:ORPort",
808            #[cfg(not(feature = "pt-client"))]
809            "is not an IpAddress:ORPort), but support disabled in cargo features",
810        );
811
812        #[cfg(feature = "pt-client")]
813        chk_e(
814            &["obfs4 garbage 0BAC39417268B96B9F514E7F63FA6FBA1A788955"],
815            "as pluggable transport Host:ORPort",
816        );
817
818        #[cfg(feature = "pt-client")]
819        chk_e(
820            &["obfs4 some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value garbage"],
821            r#"Expected PT key=value parameter, found "garbage" (which lacks an equals sign"#,
822        );
823
824        #[cfg(feature = "pt-client")]
825        chk_e(
826            &["obfs4 some-host:80 garbage"],
827            r#"Cannot parse "garbage" as identity key (Invalid base64 data), or PT key=value"#,
828        );
829
830        chk_e(
831            &[
832                "38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 23AC39417268B96B9F514E7F63FA6FBA1A788955",
833                "38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE xGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
834            ],
835            "More than one identity of the same type specified",
836        );
837    }
838
839    #[test]
840    fn config_api() {
841        let chk_bridgeline = |line: &str, jsons: &[&str], f: &dyn Fn(&mut BridgeConfigBuilder)| {
842            eprintln!(" ---- chk_bridgeline ----\n{}", line);
843
844            let mut bcb = BridgeConfigBuilder::default();
845            f(&mut bcb);
846            let built = bcb.build().unwrap();
847            assert_eq!(&built, &line.parse::<BridgeConfig>().unwrap());
848
849            let parsed_b: BridgeConfigBuilder = line.parse().unwrap();
850            assert_eq!(&built, &parsed_b.build().unwrap());
851
852            let re_serialized = serde_json::to_value(&bcb).unwrap();
853            assert_eq!(re_serialized, serde_json::Value::String(line.to_string()));
854
855            for json in jsons {
856                let from_dict: BridgeConfigBuilder = serde_json::from_str(json).unwrap();
857                assert_eq!(&from_dict, &bcb);
858                assert_eq!(&built, &from_dict.build().unwrap());
859            }
860        };
861
862        chk_bridgeline(
863            "38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
864            &[r#"{
865                "addrs": ["38.229.33.83:80"],
866                "ids": ["ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
867                      "$0bac39417268b96b9f514e7f63fa6fba1a788955"]
868            }"#],
869            &|bcb| {
870                bcb.addrs().push("38.229.33.83:80".parse().unwrap());
871                bcb.ids().push(
872                    "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"
873                        .parse()
874                        .unwrap(),
875                );
876                bcb.ids()
877                    .push("$0bac39417268b96b9f514e7f63fa6fba1a788955".parse().unwrap());
878            },
879        );
880
881        #[cfg(feature = "pt-client")]
882        chk_bridgeline(
883            "obfs4 some-host:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 iat-mode=1",
884            &[r#"{
885                "transport": "obfs4",
886                "addrs": ["some-host:80"],
887                "ids": ["$0bac39417268b96b9f514e7f63fa6fba1a788955"],
888                "settings": [["iat-mode", "1"]]
889            }"#],
890            &|bcb| {
891                bcb.transport("obfs4");
892                bcb.addrs().push("some-host:80".parse().unwrap());
893                bcb.ids()
894                    .push("$0bac39417268b96b9f514e7f63fa6fba1a788955".parse().unwrap());
895                bcb.push_setting("iat-mode", "1");
896            },
897        );
898
899        let chk_broken = |emsg: &str, jsons: &[&str], f: &dyn Fn(&mut BridgeConfigBuilder)| {
900            eprintln!(" ---- chk_bridgeline ----\n{:?}", emsg);
901
902            let mut bcb = BridgeConfigBuilder::default();
903            f(&mut bcb);
904
905            for json in jsons {
906                let from_dict: BridgeConfigBuilder = serde_json::from_str(json).unwrap();
907                assert_eq!(&from_dict, &bcb);
908            }
909
910            let err = bcb.build().expect_err("succeeded?!");
911            let got_emsg = err.to_string();
912            assert!(
913                got_emsg.contains(emsg),
914                "wrong error message: got_emsg={:?} err={:?} expected={:?}",
915                &got_emsg,
916                &err,
917                emsg,
918            );
919
920            // This is a kludge.  When we serialize `Option<Vec<_>>` as JSON,
921            // we get a `Null` entry.  These `Null`s aren't in our test cases and we don't
922            // really want them, although it's OK that they're there in the JSON.
923            // The TOML serialization omits them completely, though.
924            // So, we serialize the builder as TOML, and then convert the TOML to JSON Value.
925            // That launders out the `Null`s and gives us the same Value as our original JSON.
926            let toml_got = toml::to_string(&bcb).unwrap();
927            let json_got: serde_json::Value = toml::from_str(&toml_got).unwrap();
928            let json_exp: serde_json::Value = serde_json::from_str(jsons[0]).unwrap();
929            assert_eq!(&json_got, &json_exp);
930        };
931
932        chk_broken(
933            "Specified `settings` for a direct bridge connection",
934            &[r#"{
935                "settings": [["hi","there"]]
936            }"#],
937            &|bcb| {
938                bcb.settings().push(("hi".into(), "there".into()));
939            },
940        );
941
942        #[cfg(not(feature = "pt-client"))]
943        chk_broken(
944            "Not compiled with pluggable transport support",
945            &[r#"{
946                "transport": "obfs4"
947            }"#],
948            &|bcb| {
949                bcb.transport("obfs4");
950            },
951        );
952
953        #[cfg(feature = "pt-client")]
954        chk_broken(
955            "only numeric addresses are supported for a direct bridge connection",
956            &[r#"{
957                "transport": "bridge",
958                "addrs": ["some-host:80"]
959            }"#],
960            &|bcb| {
961                bcb.transport("bridge");
962                bcb.addrs().push("some-host:80".parse().unwrap());
963            },
964        );
965
966        chk_broken(
967            "Missing `addrs` for a direct bridge connection",
968            &[r#"{
969                "transport": "-"
970            }"#],
971            &|bcb| {
972                bcb.transport("-");
973            },
974        );
975
976        #[cfg(feature = "pt-client")]
977        chk_broken(
978            "only supports a single nominal address",
979            &[r#"{
980                "transport": "obfs4",
981                "addrs": ["some-host:80", "38.229.33.83:80"]
982            }"#],
983            &|bcb| {
984                bcb.transport("obfs4");
985                bcb.addrs().push("some-host:80".parse().unwrap());
986                bcb.addrs().push("38.229.33.83:80".parse().unwrap());
987            },
988        );
989
990        chk_broken(
991            "multiple different ids of the same type (ed25519)",
992            &[r#"{
993                "addrs": ["38.229.33.83:80"],
994                "ids": ["ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
995                        "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISA"]
996            }"#],
997            &|bcb| {
998                bcb.addrs().push("38.229.33.83:80".parse().unwrap());
999                bcb.ids().push(
1000                    "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"
1001                        .parse()
1002                        .unwrap(),
1003                );
1004                bcb.ids().push(
1005                    "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISA"
1006                        .parse()
1007                        .unwrap(),
1008                );
1009            },
1010        );
1011
1012        chk_broken(
1013            "need an RSA identity",
1014            &[r#"{
1015                "addrs": ["38.229.33.83:80"],
1016                "ids": ["ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"]
1017            }"#],
1018            &|bcb| {
1019                bcb.addrs().push("38.229.33.83:80".parse().unwrap());
1020                bcb.ids().push(
1021                    "ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE"
1022                        .parse()
1023                        .unwrap(),
1024                );
1025            },
1026        );
1027    }
1028}