implement FromStr for Bridge, and test it
This commit is contained in:
parent
4b28c0e120
commit
0dfa69510e
|
@ -1,9 +1,17 @@
|
|||
//! Configuration logic and types for bridges.
|
||||
#![allow(dead_code)] // TODO pt-client: remove.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use tor_linkspec::ChannelMethod;
|
||||
use tor_linkspec::{RelayId, RelayIdError, TransportIdError};
|
||||
use tor_llcrypto::pk::{ed25519::Ed25519Identity, rsa::RsaIdentity};
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
use tor_linkspec::{PtAddrError, PtTarget, PtTargetAddr};
|
||||
|
||||
/// A relay not listed on the main tor network, used for anticensorship.
|
||||
///
|
||||
/// This object represents a bridge as configured by the user or by software
|
||||
|
@ -39,3 +47,322 @@ pub struct Bridge {
|
|||
// configuration object.
|
||||
//
|
||||
// (These last two might be part of the same configuration type.)
|
||||
|
||||
/// Error when parsing a bridge line from a string
|
||||
#[derive(Error, Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum BridgeParseError {
|
||||
/// Bridge line was empty
|
||||
#[error("Bridge line was empty")]
|
||||
Empty,
|
||||
|
||||
/// Cannot parse value as direct address
|
||||
#[error("Cannot parse value as PT name ({0}), or direct bridge Addr:ORPort")]
|
||||
InvalidPtOrAddr(#[from] TransportIdError),
|
||||
|
||||
/// Cannot parse value as direct bridge address
|
||||
#[error("Invalid direct bridge Address:ORPort (NB hostnames are not allowed)")]
|
||||
InvalidIDirectHostAddr(#[from] std::net::AddrParseError),
|
||||
|
||||
/// Cannot parse pluggable transport host address
|
||||
#[cfg(feature = "pt-client")]
|
||||
#[error("Invalid pluggable transport Host:ORPort")]
|
||||
InvalidIPtHostAddr(#[from] PtAddrError),
|
||||
|
||||
/// Cannot parse value as identity key, or PT key=value
|
||||
#[error("Cannot parse value as identity key ({0}), or PT key=value")]
|
||||
InvalidIdentityOrParameter(RelayIdError),
|
||||
|
||||
/// PT key=value parameter does not contain an equals sign
|
||||
#[cfg(feature = "pt-client")]
|
||||
#[error("Invalid PT key=value parameters (does not contain an equals sign)")]
|
||||
InvalidPtKeyValue,
|
||||
|
||||
/// More than one identity of the same type specified
|
||||
#[error("More than one identity of the same type specified, at {0}")]
|
||||
MultipleIdentitiesOfSameType(String),
|
||||
|
||||
/// Identity specified of unsupported type
|
||||
#[error("Identity specified but not of supported type, at {0}")]
|
||||
UnsupportedIdentityType(String),
|
||||
|
||||
/// Parameters may only be specified with a pluggable transport
|
||||
#[error("Parameters supplied but not valid without a pluggable transport")]
|
||||
DirectParametersNotAllowed,
|
||||
|
||||
/// Every bridge must have an RSA identity
|
||||
#[error("Bridge line lacks specification of RSA identity key")]
|
||||
NoRsaIdentity,
|
||||
|
||||
/// Pluggable transport support disabled in cargo features
|
||||
// We deliberately make this one *not* configured out if PT support is enabled
|
||||
#[error("Pluggable transport support disabled in cargo features")]
|
||||
PluggableTransportsNotSupported,
|
||||
}
|
||||
|
||||
impl FromStr for Bridge {
|
||||
type Err = BridgeParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use BridgeParseError as BPE;
|
||||
|
||||
let mut s = s.trim().split_ascii_whitespace().peekable();
|
||||
|
||||
let bridge_word = s.peek().ok_or(BPE::Empty)?;
|
||||
if bridge_word.eq_ignore_ascii_case("bridge") {
|
||||
s.next();
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "pt-client"), allow(unused_mut))]
|
||||
let mut method = {
|
||||
let word = s.next().ok_or(BPE::Empty)?;
|
||||
if word.contains(':') {
|
||||
let addr = word.parse()?;
|
||||
ChannelMethod::Direct(addr)
|
||||
} else {
|
||||
#[cfg(not(feature = "pt-client"))]
|
||||
return Err(BPE::PluggableTransportsNotSupported);
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
{
|
||||
let pt_name = word.parse()?;
|
||||
let addr = s
|
||||
.next()
|
||||
.map(|s| s.parse())
|
||||
.transpose()?
|
||||
.unwrap_or(PtTargetAddr::None);
|
||||
ChannelMethod::Pluggable(PtTarget::new(pt_name, addr))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut rsa_id = None;
|
||||
let mut ed_id = None;
|
||||
|
||||
while let Some(word) = s.peek() {
|
||||
let check_several = |was_some| {
|
||||
if was_some {
|
||||
Err(BPE::MultipleIdentitiesOfSameType(word.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
match word.parse() {
|
||||
Err(id_err) => {
|
||||
if word.contains('=') {
|
||||
break;
|
||||
}
|
||||
return Err(BPE::InvalidIdentityOrParameter(id_err));
|
||||
}
|
||||
Ok(RelayId::Ed25519(id)) => check_several(ed_id.replace(id).is_some())?,
|
||||
Ok(RelayId::Rsa(id)) => check_several(rsa_id.replace(id).is_some())?,
|
||||
Ok(_) => return Err(BPE::UnsupportedIdentityType(word.to_string()))?,
|
||||
}
|
||||
s.next();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "pt-client"))]
|
||||
if s.next().is_some() {
|
||||
return Err(BPE::DirectParametersNotAllowed);
|
||||
}
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
for word in s {
|
||||
let (k, v) = word.split_once('=').ok_or(BPE::InvalidPtKeyValue)?;
|
||||
|
||||
match &mut method {
|
||||
ChannelMethod::Direct(_) => return Err(BPE::DirectParametersNotAllowed),
|
||||
ChannelMethod::Pluggable(t) => t.push_setting(k.into(), v.into()),
|
||||
}
|
||||
}
|
||||
|
||||
let rsa_id = rsa_id.ok_or(BPE::NoRsaIdentity)?;
|
||||
Ok(Bridge {
|
||||
addrs: method,
|
||||
rsa_id,
|
||||
ed_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
#![allow(clippy::bool_assert_comparison)]
|
||||
#![allow(clippy::clone_on_copy)]
|
||||
#![allow(clippy::dbg_macro)]
|
||||
#![allow(clippy::print_stderr)]
|
||||
#![allow(clippy::print_stdout)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
fn mk_pt_target(name: &str, addr: PtTargetAddr, params: &[(&str, &str)]) -> ChannelMethod {
|
||||
let mut target = PtTarget::new(name.parse().unwrap(), addr);
|
||||
for &(k, v) in params {
|
||||
target.push_setting(k.into(), v.into());
|
||||
}
|
||||
ChannelMethod::Pluggable(target)
|
||||
}
|
||||
|
||||
fn mk_direct(s: &str) -> ChannelMethod {
|
||||
ChannelMethod::Direct(s.parse().unwrap())
|
||||
}
|
||||
|
||||
fn mk_rsa(s: &str) -> RsaIdentity {
|
||||
match s.parse().unwrap() {
|
||||
RelayId::Rsa(y) => y,
|
||||
_ => panic!("not rsa {:?}", s),
|
||||
}
|
||||
}
|
||||
fn mk_ed(s: &str) -> Ed25519Identity {
|
||||
match s.parse().unwrap() {
|
||||
RelayId::Ed25519(y) => y,
|
||||
_ => panic!("not ed {:?}", s),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_lines() {
|
||||
let chk = |sl: &[&str], exp: Bridge| {
|
||||
for s in sl {
|
||||
let got: Bridge = s.parse().expect(s);
|
||||
assert_eq!(got, exp, "{:?}", s);
|
||||
}
|
||||
};
|
||||
|
||||
let chk_e = |sl: &[&str], exp: &str| {
|
||||
for s in sl {
|
||||
let got: Result<Bridge, _> = s.parse();
|
||||
let got = got.expect_err(s);
|
||||
let got_s = got.to_string();
|
||||
assert!(
|
||||
got_s.contains(exp),
|
||||
"{:?} => {:?} ({}) not {}",
|
||||
s,
|
||||
&got,
|
||||
&got_s,
|
||||
exp
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// example from https://tb-manual.torproject.org/bridges/, with cert= truncated
|
||||
#[cfg(feature = "pt-client")]
|
||||
chk(&[
|
||||
"obfs4 38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
|
||||
"obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
|
||||
"Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op iat-mode=1",
|
||||
], Bridge {
|
||||
addrs: mk_pt_target(
|
||||
"obfs4",
|
||||
PtTargetAddr::IpPort("38.229.33.83:80".parse().unwrap()),
|
||||
&[
|
||||
("cert", "VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1Op" ),
|
||||
("iat-mode", "1"),
|
||||
],
|
||||
),
|
||||
rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
|
||||
ed_id: None,
|
||||
});
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
chk(&[
|
||||
"obfs4 some-host:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE iat-mode=1",
|
||||
"obfs4 some-host:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE 0BAC39417268B96B9F514E7F63FA6FBA1A788955 iat-mode=1",
|
||||
], Bridge {
|
||||
addrs: mk_pt_target(
|
||||
"obfs4",
|
||||
PtTargetAddr::HostPort("some-host".into(), 80),
|
||||
&[
|
||||
("iat-mode", "1"),
|
||||
],
|
||||
),
|
||||
rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
|
||||
ed_id: Some(mk_ed("dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")),
|
||||
});
|
||||
|
||||
chk(
|
||||
&[
|
||||
"38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955",
|
||||
"Bridge 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
|
||||
],
|
||||
Bridge {
|
||||
addrs: mk_direct("38.229.33.83:80"),
|
||||
rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
|
||||
ed_id: None,
|
||||
},
|
||||
);
|
||||
|
||||
chk(&[
|
||||
"38.229.33.83:80 $0bac39417268b96b9f514e7f63fa6fba1a788955 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
|
||||
"38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
|
||||
], Bridge {
|
||||
addrs: mk_direct("38.229.33.83:80"),
|
||||
rsa_id: mk_rsa("0BAC39417268B96B9F514E7F63FA6FBA1A788955"),
|
||||
ed_id: Some(mk_ed("dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE")),
|
||||
});
|
||||
|
||||
chk_e(
|
||||
&[
|
||||
"38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
|
||||
"Bridge 38.229.33.83:80 ed25519:dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
|
||||
],
|
||||
"lacks specification of RSA identity key",
|
||||
);
|
||||
|
||||
chk_e(&["", "bridge"], "Bridge line was empty");
|
||||
|
||||
chk_e(
|
||||
&["999.329.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955"],
|
||||
"Invalid direct bridge Address:ORPort",
|
||||
);
|
||||
|
||||
chk_e(
|
||||
&[
|
||||
"38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value",
|
||||
"Bridge 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value",
|
||||
],
|
||||
"Parameters supplied but not valid without a pluggable transport",
|
||||
);
|
||||
|
||||
chk_e(
|
||||
&[
|
||||
"bridge bridge some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
|
||||
"yikes! some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955",
|
||||
],
|
||||
#[cfg(feature = "pt-client")]
|
||||
"Cannot parse value as PT name",
|
||||
#[cfg(not(feature = "pt-client"))]
|
||||
"Pluggable transport support disabled in cargo features",
|
||||
);
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
chk_e(
|
||||
&["obfs4 garbage 0BAC39417268B96B9F514E7F63FA6FBA1A788955"],
|
||||
"Invalid pluggable transport Host:ORPort",
|
||||
);
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
chk_e(
|
||||
&["obfs4 some-host:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 key=value garbage"],
|
||||
"Invalid PT key=value parameters (does not contain an equals sign)",
|
||||
);
|
||||
|
||||
#[cfg(feature = "pt-client")]
|
||||
chk_e(
|
||||
&["obfs4 some-host:80 garbage"],
|
||||
"Cannot parse value as identity key (Invalid base64 data), or PT key=value",
|
||||
);
|
||||
|
||||
chk_e(
|
||||
&[
|
||||
"38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 23AC39417268B96B9F514E7F63FA6FBA1A788955",
|
||||
"38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 dGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE xGhpcyBpcyBpbmNyZWRpYmx5IHNpbGx5ISEhISEhISE",
|
||||
],
|
||||
"More than one identity of the same type specified",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue