Skip to main content

ptrs/
helpers.rs

1use crate::{
2    args::{Args, Opts},
3    debug,
4};
5
6#[cfg(unix)]
7use std::os::unix::fs::DirBuilderExt;
8use std::{env, fs::DirBuilder, io::Error, net::SocketAddr, str::FromStr};
9
10use itertools::Itertools;
11use tokio::net::TcpStream;
12use url::Url;
13
14/// PT-spec environment variable names and current protocol version.
15pub mod constants {
16    /// The only managed-transport protocol version we support.
17    pub const CURRENT_TRANSPORT_VER: &str = "1";
18
19    /// `TOR_PT_MANAGED_TRANSPORT_VER`
20    pub const MANAGED_VER: &str = "TOR_PT_MANAGED_TRANSPORT_VER";
21    /// `TOR_PT_STATE_LOCATION`
22    pub const STATE_LOCATION: &str = "TOR_PT_STATE_LOCATION";
23    /// `TOR_PT_CLIENT_TRANSPORTS`
24    pub const CLIENT_TRANSPORTS: &str = "TOR_PT_CLIENT_TRANSPORTS";
25    /// `TOR_PT_PROXY`
26    pub const PROXY: &str = "TOR_PT_PROXY";
27    /// `TOR_PT_SERVER_TRANSPORTS`
28    pub const SERVER_TRANSPORTS: &str = "TOR_PT_SERVER_TRANSPORTS";
29    /// `TOR_PT_SERVER_TRANSPORT_OPTIONS`
30    pub const SERVER_TRANSPORT_OPTIONS: &str = "TOR_PT_SERVER_TRANSPORT_OPTIONS";
31    /// `TOR_PT_SERVER_BINDADDR`
32    pub const SERVER_BINDADDR: &str = "TOR_PT_SERVER_BINDADDR";
33    /// `TOR_PT_AUTH_COOKIE_FILE`
34    pub const AUTH_COOKIE_FILE: &str = "TOR_PT_AUTH_COOKIE_FILE";
35    /// `TOR_PT_ORPORT`
36    pub const ORPORT: &str = "TOR_PT_ORPORT";
37    /// `TOR_PT_EXTENDED_SERVER_PORT`
38    pub const EXTENDED_SERVER_PORT: &str = "TOR_PT_EXTENDED_SERVER_PORT";
39    /// `TOR_PT_EXIT_ON_STDIN_CLOSE`
40    pub const EXIT_ON_STDIN_CLOSE: &str = "TOR_PT_EXIT_ON_STDIN_CLOSE";
41}
42
43/// Get a pluggable transports version offered by Tor and understood by us, if
44/// any. The only version we understand is "1". This function reads the
45/// environment variable `TOR_PT_MANAGED_TRANSPORT_VER`.
46pub(crate) fn get_managed_transport_ver() -> Result<String, Error> {
47    let managed_transport_ver = env::var(constants::MANAGED_VER).map_err(to_io_other)?;
48    for segment in managed_transport_ver.split(',') {
49        if segment == constants::CURRENT_TRANSPORT_VER {
50            return Ok(segment.into());
51        }
52    }
53
54    Err(to_io_other("no-version"))
55}
56
57/// Determines if the current program should be running as a client or server
58/// by checking the `TOR_PT_CLIENT_TRANSPORTS` and `TOR_PT_SERVER_TRANSPORTS`
59/// environment variables.
60pub fn is_client() -> Result<bool, Error> {
61    let is_client = env::var_os(constants::CLIENT_TRANSPORTS);
62    let is_server = env::var_os(constants::SERVER_TRANSPORTS);
63
64    match (is_client, is_server) {
65        (Some(_), Some(_)) => Err(to_io_other(
66            "ENV-ERROR TOR_PT_[CLIENT,SERVER]_TRANSPORTS both set",
67        )),
68        (Some(_), None) => Ok(true),
69        (None, Some(_)) => Ok(false),
70        (None, None) => Err(to_io_other("not launched as a managed transport")),
71    }
72}
73
74/// Get the state directory from env, create if it doesnt exist.
75///
76/// Return the directory name in the TOR_PT_STATE_LOCATION environment variable, creating it
77/// if it doesn't exist. Returns non-nil error if `TOR_PT_STATE_LOCATION` is not set or if
78/// there is an error creating the directory.
79pub fn make_state_dir() -> Result<String, Error> {
80    let path = env::var(constants::STATE_LOCATION)
81        .map_err(|_| to_io_other("missing required TOR_PT_STATE_LOCATION env var"))?;
82
83    let mut builder = DirBuilder::new();
84    builder.recursive(true);
85    #[cfg(unix)]
86    builder.mode(0o700);
87    builder.create(&path)?;
88    Ok(path)
89}
90
91/// Feature #15435 adds a new env var for determining if Tor keeps stdin
92/// open for use in termination detection.
93pub fn pt_should_exit_on_stdin_close() -> bool {
94    if let Ok(v) = env::var(constants::EXIT_ON_STDIN_CLOSE) {
95        v == "1"
96    } else {
97        false
98    }
99}
100
101// ================================================================ //
102//                            Client                                //
103// ================================================================ //
104
105/// Client-side PT configuration read from the environment.
106pub struct ClientInfo {
107    /// Transport names requested by the parent process.
108    pub methods: Vec<String>,
109    /// Optional upstream proxy URL.
110    pub uri: Option<Url>,
111}
112
113impl ClientInfo {
114    /// Read client info from `TOR_PT_*` environment variables.
115    pub fn new() -> Result<Self, Error> {
116        let _ver = get_managed_transport_ver()?;
117        debug!("VERSION {_ver}");
118
119        Ok(Self {
120            methods: get_client_transports()?,
121            uri: get_proxy_url()?,
122        })
123    }
124}
125
126pub(crate) fn get_client_transports() -> Result<Vec<String>, Error> {
127    let client_transports = env::var(constants::CLIENT_TRANSPORTS).map_err(to_io_other)?;
128    Ok(client_transports.split(',').map(String::from).collect_vec())
129}
130
131pub(crate) fn get_proxy_url() -> Result<Option<Url>, Error> {
132    let url_str = match env::var(constants::PROXY) {
133        Ok(s) => s,
134        Err(env::VarError::NotPresent) => return Ok(None),
135        Err(e) => return Err(to_io_other(format!("failed to parse proxy config: {e}"))),
136    };
137
138    // Url::parse() only works for absolute urls so we do not need to check for relative
139    let uri = Url::parse(&url_str)
140        .map_err(|e| to_io_other(format!("failed to parse proxy config \"{url_str}\": {e}")))?;
141
142    validate_proxy_url(&uri)?;
143
144    Ok(Some(uri))
145}
146
147/// When a client connects to the client side of the pluggable transport proxy
148/// they can optionally provide a proxy url in the `TOR_PT_PROXY` environment
149/// variable that will be used as a proxy dialer underneath the pluggable
150/// transport connection.
151///
152/// This function validates that a provided url:
153/// - uses one of `socks4a`, `socks5`, `http` protocols.
154/// - has a defined host field that DOES NOT require dns resolution. (i.e. an IP address)
155/// - DOES NOT have defined `path`, `query`, or `fragment` fields.
156/// - socks5 urls must have non-empty username and password fields.
157/// - if socks4 urls have a password they must have a username.
158///
159/// From `pt-spec.txt 3.5`:
160///
161/// ```txt
162///    On the client side, arguments are passed via the authentication
163///    fields that are part of the SOCKS protocol.
164///
165///    ... The arguments are transmitted when making the outgoing
166///    connection using the authentication mechanism specific to the
167///    SOCKS protocol version.
168///
169///     - In the case of SOCKS 4, the concatenated argument list is
170///       transmitted in the "USERID" field of the "CONNECT" request.
171///
172///     - In the case of SOCKS 5, the parent process must negotiate
173///       "Username/Password" authentication [RFC1929], and transmit
174///       the arguments encoded in the "UNAME" and "PASSWD" fields.
175///
176///       If the encoded argument list is less than 255 bytes in
177///       length, the "PLEN" field must be set to "1" and the "PASSWD"
178///       field must contain a single NUL character.
179/// ```
180#[allow(clippy::collapsible_if)]
181pub(crate) fn validate_proxy_url(spec: &Url) -> Result<(), Error> {
182    const SCHEMES: [&str; 3] = ["socks5", "socks4a", "http"];
183    if !SCHEMES.contains(&spec.scheme()) {
184        return Err(to_io_other(format!(
185            "proxy URI has invalid scheme: {}",
186            spec.scheme()
187        )));
188    }
189
190    // when spec = http the path defaults to "/" instead of empty -_-
191    if !spec.path().is_empty() {
192        if !(spec.scheme() == "http" && spec.path() == "/") {
193            return Err(to_io_other("proxy URI has a path defined "));
194        }
195    }
196    if spec.query().is_some() {
197        if !spec.query().unwrap().is_empty() {
198            return Err(to_io_other("proxy URI has a query defined"));
199        }
200    }
201    if spec.fragment().is_some() {
202        if !spec.fragment().unwrap().is_empty() {
203            return Err(to_io_other("proxy URI has a fragment defined"));
204        }
205    }
206    if spec.port().is_none() {
207        return Err(to_io_other("proxy URI lacks a port"));
208    }
209
210    match spec.scheme() {
211        "socks5" => {
212            let username = spec.username();
213            let passwd = spec.password();
214
215            // if either password or username is specified, then both must be non-empty
216            if !username.is_empty() || passwd.is_some() {
217                if username.is_empty() || username.len() > 255 {
218                    return Err(to_io_other("proxy URI specified a invalid SOCKS5 username"));
219                }
220                if passwd.is_none() {
221                    return Err(to_io_other("proxy URI specified a invalid SOCKS5 password"));
222                } else if let Some(p) = passwd {
223                    if p.is_empty() || p.len() > 255 {
224                        return Err(to_io_other("proxy URI specified a invalid SOCKS5 password"));
225                    }
226                }
227            }
228        }
229        "socks4a" => {
230            if spec.password().is_some() {
231                return Err(to_io_other("proxy URI specified SOCKS4a and a password"));
232            }
233        }
234        "http" => {}
235        _ => {
236            return Err(to_io_other(format!(
237                "proxy URI has invalid scheme: {}",
238                spec.scheme()
239            )));
240        }
241    }
242
243    if spec.host_str().is_none() {
244        return Err(to_io_other("proxy URI has missing host"));
245    }
246
247    // not sure how better to combine host port.
248    let mut sockaddr_string = String::from(spec.host_str().unwrap());
249    sockaddr_string.push(':');
250    sockaddr_string.push_str(&format!("{}", spec.port().unwrap()));
251    let _ = resolve_addr(&sockaddr_string)
252        .map_err(|e| to_io_other(format!("proxy URI has invalid host: {e}")))?;
253
254    Ok(())
255}
256
257// ================================================================ //
258//                            Server                                //
259// ================================================================ //
260
261/// Tor OR Server Information
262///
263/// Check the server pluggable transports environment, emitting an error message
264/// and returning a non-nil error if any error is encountered. Resolves the
265/// various requested bind addresses, the server ORPort and extended ORPort, and
266/// reads the auth cookie file. Returns a ServerInfo struct.
267///
268/// If your program needs to know whether to call ClientSetup or ServerSetup
269/// (i.e., if the same program can be run as either a client or a server), check
270/// whether the `TOR_PT_CLIENT_TRANSPORTS` environment variable is set:
271///
272/// ```text
273/// match std::env::var_os("TOR_PT_CLIENT_TRANSPORTS") {
274///     Some(_) => {
275///         // Client mode; call pt.ClientSetup.
276///     }
277///     None => {
278///         // Server mode; call pt.ServerSetup.
279///     }
280/// }
281///```
282#[derive(Clone, Debug, Default, PartialEq)]
283pub struct ServerInfo {
284    /// Parsed bind addresses for each server transport.
285    pub bind_addrs: Vec<Bindaddr>,
286    /// Tor ORPort address.
287    pub or_addr: Option<SocketAddr>,
288    /// Extended ORPort address.
289    pub extended_or_addr: Option<SocketAddr>,
290    /// Path to the auth cookie file.
291    pub auth_cookie_path: Option<String>,
292}
293
294impl ServerInfo {
295    /// Connect to the Tor ORPort (or extended ORPort if set).
296    pub async fn connect_to_or(&self) -> Result<TcpStream, Error> {
297        let conn = match self.or_addr {
298            Some(addr) => TcpStream::connect(addr).await?,
299            None => {
300                // Unify the None-check and the use into a single expression so
301                // there is no code path where `unwrap()` could theoretically panic.
302                let addr = self
303                    .extended_or_addr
304                    .ok_or_else(|| to_io_other("no OR addr provided"))?;
305                TcpStream::connect(addr).await?
306            }
307        };
308
309        Ok(conn)
310    }
311}
312
313impl ServerInfo {
314    /// Read server info from `TOR_PT_*` environment variables.
315    pub fn new() -> Result<Self, Error> {
316        let _ver = get_managed_transport_ver()?;
317        debug!("VERSION {_ver}");
318
319        let bind_addrs = Bindaddr::get_server_bindaddrs()?;
320
321        let or_addr = match env::var(constants::ORPORT) {
322            Ok(or_add_env) => Some(
323                resolve_addr(or_add_env)
324                    .map_err(|e| to_io_other(format!("cannot resolve TOR_PT_ORPORT: {e}")))?,
325            ),
326            Err(_) => None, // TOR_PT_ORPORT was not defined
327        };
328
329        let auth_cookie_path = env::var(constants::AUTH_COOKIE_FILE).ok();
330
331        let extended_or_addr = match env::var(constants::EXTENDED_SERVER_PORT) {
332            Ok(ext_or_addr_env) => Some(resolve_addr(ext_or_addr_env).map_err(|e| {
333                to_io_other(format!("cannot resolve TOR_PT_EXTENDED_SERVER_PORT: {e}"))
334            })?),
335            Err(_) => None, // TOR_PT_EXTENDED_SERVER_PORT was not defined
336        };
337
338        if extended_or_addr.is_some() && auth_cookie_path.is_none() {
339            return Err(to_io_other("need TOR_PT_AUTH_COOKIE_FILE environment variable with TOR_PT_EXTENDED_SERVER_PORT"));
340        }
341
342        // Need either OrAddr or ExtendedOrAddr.
343        if or_addr.is_none() && extended_or_addr.is_none() {
344            return Err(to_io_other(
345                "need TOR_PT_ORPORT or TOR_PT_EXTENDED_SERVER_PORT environment variable",
346            ));
347        }
348
349        Ok(Self {
350            bind_addrs,
351            or_addr,
352            extended_or_addr,
353            auth_cookie_path,
354        })
355    }
356}
357
358/// A combination of a method name and an address, as extracted from `TOR_PT_SERVER_BINDADDR`.
359#[derive(Clone, Debug, PartialEq)]
360pub struct Bindaddr {
361    /// Transport method name (e.g. `obfs4`).
362    pub method_name: String,
363    /// Address to bind/listen on.
364    pub addr: SocketAddr,
365    /// Per-transport options from `TOR_PT_SERVER_TRANSPORT_OPTIONS`.
366    pub options: Args,
367}
368
369impl Bindaddr {
370    /// Construct a new `Bindaddr` from its components.
371    pub fn new(method: &str, addr: SocketAddr, options: Args) -> Self {
372        Self {
373            method_name: method.into(),
374            addr,
375            options,
376        }
377    }
378
379    /// Return an array of Bindaddrs, being the contents of TOR_PT_SERVER_BINDADDR
380    /// with keys filtered by TOR_PT_SERVER_TRANSPORTS. Transport-specific options
381    /// from TOR_PT_SERVER_TRANSPORT_OPTIONS are assigned to the Options member.
382    pub(crate) fn get_server_bindaddrs() -> Result<Vec<Self>, Error> {
383        // parse the list of server transport options
384        let server_transport_opts =
385            env::var(constants::SERVER_TRANSPORT_OPTIONS).unwrap_or_default();
386
387        let mut options_map = Opts::parse_server_transport_options(&server_transport_opts)
388            .map_err(|e| {
389                to_io_other(format!(
390                    "TOR_PT_SERVER_TRANSPORT_OPTIONS: {server_transport_opts}: \"{e}\""
391                ))
392            })?;
393
394        // get the list of all requested bindaddrs
395        let server_bindaddr = env::var(constants::SERVER_BINDADDR).map_err(to_io_other)?;
396        if server_bindaddr.is_empty() {
397            return Err(to_io_other(format!(
398                "no \"{}\" environment variable value",
399                constants::SERVER_BINDADDR
400            )));
401        }
402
403        let mut results = Vec::new();
404        let mut seen_methods = Vec::new();
405        for spec in server_bindaddr.split(',') {
406            let parts = spec.split_once('-');
407            if parts.is_none() {
408                return Err(to_io_other(format!(
409                    "TPR_PT_SERVER_BINDADDR: {spec} doesn't contain \"-\""
410                )));
411            }
412            let (method_name, addr) = parts.unwrap();
413
414            // Check for duplicate method names: "Application MUST NOT set more
415            // than one <address>:<port> pair per PT name."
416            if seen_methods.contains(&method_name) {
417                return Err(to_io_other(format!(
418                    "TPR_PT_SERVER_BINDADDR: {spec} duplicate method name {method_name}"
419                )));
420            }
421            seen_methods.push(method_name);
422            let address = resolve_addr(addr)
423                .map_err(|e| to_io_other(format!("TOR_PT_SERVER_BINDADDR: {spec}: {e}")))?;
424
425            results.push(Bindaddr::new(
426                method_name,
427                address,
428                options_map.remove(method_name).unwrap_or_default(),
429            ));
430        }
431        let server_transports = env::var(constants::SERVER_TRANSPORTS).map_err(to_io_other)?;
432        if server_transports.is_empty() {
433            return Err(to_io_other(format!(
434                "no \"{}\" environment variable value",
435                constants::SERVER_TRANSPORTS
436            )));
437        }
438
439        let result = filter_bindaddrs(results, &server_transports.split(',').collect_vec());
440        Ok(result)
441    }
442}
443
444fn filter_bindaddrs(addrs: Vec<Bindaddr>, methods: &[&str]) -> Vec<Bindaddr> {
445    if methods.is_empty() {
446        return Vec::new();
447    }
448    addrs
449        .into_iter()
450        .filter(|b| methods.contains(&b.method_name.as_str()))
451        .collect()
452}
453
454/// Parse a `host:port` string into a `SocketAddr`, rejecting unspecified hosts and port 0.
455pub fn resolve_addr(addr: impl AsRef<str>) -> Result<SocketAddr, Error> {
456    let a = addr.as_ref();
457    match SocketAddr::from_str(a) {
458        Ok(sock_addr) => {
459            if sock_addr.ip().is_unspecified() {
460                return Err(to_io_other(format!("address string {a} lacks a host")));
461            }
462
463            if sock_addr.port() == 0 {
464                return Err(to_io_other(format!("address string {a} lacks a port")));
465            }
466            Ok(sock_addr)
467        }
468        Err(e) => Err(to_io_other(format!("\"{a}\" - {e}"))),
469    }
470}
471
472fn to_io_other(e: impl std::fmt::Display) -> Error {
473    Error::other(format!("{e}"))
474}
475
476#[cfg(test)]
477#[serial_test::serial]
478mod test {
479    use super::*;
480
481    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
482
483    #[test]
484    fn is_client_from_env() -> Result<(), Error> {
485        env::remove_var(constants::CLIENT_TRANSPORTS);
486        env::remove_var(constants::SERVER_TRANSPORTS);
487        assert!(is_client().is_err());
488
489        env::set_var(constants::CLIENT_TRANSPORTS, "trebuchet");
490        env::remove_var(constants::SERVER_TRANSPORTS);
491        let c = is_client();
492        assert!(c.is_ok());
493        assert!(c.unwrap());
494
495        env::remove_var(constants::CLIENT_TRANSPORTS);
496        env::set_var(constants::SERVER_TRANSPORTS, "trebuchet1");
497        let c = is_client();
498        assert!(c.is_ok());
499        assert!(!c.unwrap());
500
501        env::set_var(constants::CLIENT_TRANSPORTS, "trebuchet2");
502        env::set_var(constants::SERVER_TRANSPORTS, "trebuchet2");
503        assert!(is_client().is_err());
504
505        Ok(())
506    }
507
508    #[test]
509    fn statedir() -> Result<(), Error> {
510        // TOR_PT_STATE_LOCATION not set.
511        env::remove_var(constants::STATE_LOCATION);
512        if make_state_dir().is_ok() {
513            panic!("empty environment unexpectedly succeeded");
514        }
515
516        // Setup the scratch directory.
517        let temp_dir = tempfile::tempdir()?;
518        // temp_dir, err := ioutil.TempDir("", "testmake_state_dir")
519        // if err != nil {
520        //     t.Fatalf("ioutil.TempDir failed: %s", err)
521        // }
522        // defer os.RemoveAll(temp_dir)
523
524        let good = vec![
525            // Already existing directory.
526            temp_dir.path().to_path_buf(),
527            // Nonexistent directory, parent exists.
528            temp_dir.path().join("parentExists"),
529            // Nonexistent directory, parent doesn't exist.
530            temp_dir.path().join("missingParent").join("parentMissing"),
531        ];
532        for trial in good {
533            env::set_var("TOR_PT_STATE_LOCATION", trial.to_str().unwrap());
534            let dir = make_state_dir()?;
535            if dir != trial.to_str().unwrap() {
536                panic!("make_state_dir returned an unexpected path {dir} (expecting {trial:?})");
537            }
538        }
539
540        // Name already exists, but is an ordinary file.
541        let temp_file = temp_dir.path().join("file");
542        let _ = std::fs::File::create(&temp_file)?;
543
544        env::set_var("TOR_PT_STATE_LOCATION", &temp_file);
545        assert!(
546            make_state_dir().is_err(),
547            "make_state_dir with a file unexpectedly succeeded"
548        );
549
550        // Directory name that cannot be created. (Subdir of a file)
551        env::set_var("TOR_PT_STATE_LOCATION", temp_file.join("subDir"));
552        assert!(
553            make_state_dir().is_err(),
554            "make_state_dir with a subdirectory of a file unexpectedly succeeded"
555        );
556
557        Ok(())
558    }
559
560    #[test]
561    fn server_bindaddrs() -> Result<(), Error> {
562        // // test with env vars unset
563        // assert_eq!(Bindaddr::get_server_bindaddrs().unwrap_err(), env::VarError);
564        assert!(Bindaddr::get_server_bindaddrs().is_err());
565
566        let bad = vec![
567            // bad TOR_PT_SERVER_BINDADDR
568            ("alpha", "alpha", ""),
569            ("alpha-1.2.3.4", "alpha", ""),
570            // missing TOR_PT_SERVER_TRANSPORTS
571            ("alpha-1.2.3.4:1111", "", "alpha:key=value"),
572            // bad TOR_PT_SERVER_TRANSPORT_OPTIONS
573            ("alpha-1.2.3.4:1111", "alpha", "key=value"),
574            // no escaping is defined for TOR_PT_SERVER_TRANSPORTS or
575            // TOR_PT_SERVER_BINDADDR.
576            (r"alpha\,beta-1.2.3.4:1111", r"alpha\,beta", ""),
577            // duplicates in TOR_PT_SERVER_BINDADDR
578            // https://bugs.torproject.org/21261
579            (r"alpha-0.0.0.0:1234,alpha-[::]:1234", r"alpha", ""),
580            (r"alpha-0.0.0.0:1234,alpha-0.0.0.0:1234", r"alpha", ""),
581        ];
582
583        for trial in bad {
584            env::set_var(constants::SERVER_BINDADDR, trial.0);
585            env::set_var(constants::SERVER_TRANSPORTS, trial.1);
586            env::set_var(constants::SERVER_TRANSPORT_OPTIONS, trial.2);
587            assert!(
588                Bindaddr::get_server_bindaddrs().is_err(),
589                "{:?} unexpectedly succeeded",
590                trial
591            );
592        }
593
594        let good = vec![
595            (
596                "alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222",
597                "alpha,beta,gamma",
598                "alpha:k1=v1;beta:k2=v2;gamma:k3=v3",
599                vec![
600                    Bindaddr::new(
601                        "alpha",
602                        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 1111),
603                        args! {"k1"=>["v1"]},
604                    ),
605                    Bindaddr::new(
606                        "beta",
607                        SocketAddr::new(IpAddr::V6(Ipv6Addr::new(1, 2, 0, 0, 0, 0, 3, 4)), 2222),
608                        args! {"k2"=>["v2"]},
609                    ),
610                ],
611            ),
612            ("alpha-1.2.3.4:1111", "xxx", "", vec![]),
613            (
614                "alpha-1.2.3.4:1111",
615                "alpha,beta,gamma",
616                "",
617                vec![Bindaddr::new(
618                    "alpha",
619                    SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 1111),
620                    Args::default(),
621                )],
622            ),
623            (
624                "trebuchet-127.0.0.1:1984,ballista-127.0.0.1:4891",
625                "trebuchet,ballista",
626                "trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes",
627                vec![
628                    Bindaddr::new(
629                        "trebuchet",
630                        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1984),
631                        args! {"secret"=>["nou"], "cache"=>["/tmp/cache"]},
632                    ),
633                    Bindaddr::new(
634                        "ballista",
635                        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 4891),
636                        args!("secret"=>["yes"]),
637                    ),
638                ],
639            ),
640            // In the past, "*" meant to return all known transport names.
641            // But now it has no special meaning.
642            // https://bugs.torproject.org/15612
643            ("alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222", "*", "", vec![]),
644        ];
645
646        for trial in good {
647            env::set_var(constants::SERVER_BINDADDR, trial.0);
648            env::set_var(constants::SERVER_TRANSPORTS, trial.1);
649            env::set_var(constants::SERVER_TRANSPORT_OPTIONS, trial.2);
650
651            let out = Bindaddr::get_server_bindaddrs();
652            assert!(out.is_ok(), "{:?} unexpectedly failed: {out:?}", trial);
653
654            assert_eq!(out.unwrap(), trial.3);
655        }
656        Ok(())
657    }
658
659    #[test]
660    fn validate_url() -> Result<(), Error> {
661        env::remove_var(constants::PROXY);
662        let url = get_proxy_url();
663        assert!(url.is_ok());
664        assert!(url.unwrap().is_none());
665
666        let bad_url = vec![
667            "asdals;kdmma",
668            "http/example.com",
669            "127.0.0.1:8080",
670            "socks5://admin:admin@:9000", // No host
671        ];
672
673        let bad = vec![
674            "socks5://admin:admin@1.2.3.4:",
675            "ftp://127.0.0.1:8000",              // invalid protocol
676            "socks5://aaa:bbb@1.2.3.4:80/a/b/c", // includes path
677            "socks5://aaa:bbb@1.2.3.4:80/?labels=E-easy&state=open", // includes query
678            "socks5://aaa:bbb@1.2.3.4:80#row=4", // includes fragment
679            "socks5://myhost",                   // no username / password
680            "socks5://myproxy:8080",             // uses non-IP host alias
681            "socks5://aaa:bbb@myhost.com:8888",  // uses domain name
682            "socks5://aaa:bbb@myhost",           // uses non-IP host alias
683            "socks4a://:admin@1.2.3.4:8080",     // no username, but password defined
684            "http://admin:admin@example.com",
685            "socks5://1.2.3.4", // no port
686            "socks5://[1:2::3:4]",
687            "socks5://admin:admin@1.2.3.4",
688            "socks4a://1.2.3.4",
689            "socks4a://[1:2::3:4]",
690            "socks5://admin:admin@[1:2::3:4]",
691            "socks4a://admin:admin@1.2.3.4:8080", // socks4 with password
692            "socks4a://:admin@1.2.3.4:8080",      // socks4a with password
693            "socks5://admin@[1:2::3:4]:9000",     // socks5 with username, but no password
694            "socks5://:admin@[1:2::3:4]:9000",    // socks5 wuth password, but no username
695        ];
696
697        let good = vec![
698            "socks5://127.0.0.1:8080",
699            "socks5://1.2.3.4:8080",
700            "socks5://[1:2::3:4]:8080",
701            "socks5://admin:admin@1.2.3.4:8080",
702            "socks5://admin:admin@1.2.3.4:8080",
703            "socks5://admin:admin@[1:2::3:4]:9000",
704            "socks4a://1.2.3.4:8080",
705            "socks4a://[1:2::3:4]:8080",
706            "socks4a://admin@1.2.3.4:8080",
707            "http://1.2.3.4:8080",
708            "http://[1:2::3:4]:8080",
709            "http://admin@1.2.3.4:8080",
710            "http://admin:admin@1.2.3.4:8080",
711        ];
712
713        for trial in bad_url {
714            let url = Url::parse(trial);
715            assert!(
716                url.is_err(),
717                "\"{trial}\" unexpectedly succeeded in parsing: {url:?}"
718            );
719        }
720
721        for trial in bad {
722            let url = Url::parse(trial).unwrap();
723            assert!(
724                validate_proxy_url(&url).is_err(),
725                "\"{trial}\" unexpectedly succeeded validation: {url:?}"
726            );
727        }
728
729        for trial in good {
730            env::set_var(constants::PROXY, trial);
731
732            let res = get_proxy_url();
733            assert!(
734                res.is_ok(),
735                "\"{trial}\" unexpectedly failed to validate: {res:?}"
736            );
737        }
738
739        Ok(())
740    }
741
742    #[test]
743    fn client_transports() -> Result<(), Error> {
744        let tests: Vec<(&str, Vec<&str>)> = vec![
745            ("alpha", vec!["alpha"]),
746            ("alpha,beta", vec!["alpha", "beta"]),
747            ("alpha,beta,gamma", vec!["alpha", "beta", "gamma"]),
748            // In the past, "*" meant to return all known transport names.
749            // But now it has no special meaning.
750            // https://bugs.torproject.org/15612
751            ("*", vec!["*"]),
752            ("alpha,*,gamma", vec!["alpha", "*", "gamma"]),
753            // No escaping is defined for TOR_PT_CLIENT_TRANSPORTS.
754            ("alpha\\,beta", vec!["alpha\\", "beta"]),
755        ];
756
757        for trial in tests {
758            env::set_var(constants::CLIENT_TRANSPORTS, trial.0);
759            let result = get_client_transports()?;
760            assert_eq!(result, trial.1);
761        }
762
763        Ok(())
764    }
765
766    #[test]
767    fn resolve() -> Result<(), Error> {
768        let bad = vec![
769            "",
770            "1.2.3.4",
771            "1.2.3.4:",
772            "9999",
773            ":9999",
774            "[1:2::3:4]",
775            "[1:2::3:4]:",
776            "[1::2::3:4]",
777            "1:2::3:4::9999",
778            "1:2::3:4:9999", // moved from good cases since this is not proper format
779            "1:2:3:4::9999",
780            "localhost:9999",
781            "[localhost]:9999",
782            "1.2.3.4:http",
783            "1.2.3.4:0x50",
784            "1.2.3.4:-65456",
785            "1.2.3.4:65536",
786            "1.2.3.4:80\x00",
787            "1.2.3.4:80 ",
788            " 1.2.3.4:80",
789            "1.2.3.4 : 80",
790            "www.google.com", // not a socket address (domain)
791            "google.com:443", // not a socket address (domain)
792            "0.0.0.0:9000",   // no address specified
793            "[0::0000]:9000", // no address specified
794            "127.0.0.1:0",    // no port specified
795            "[1234::cdef]:0", // no port specified
796            "127.0.0",        // not an address
797        ];
798        let good: Vec<(&str, SocketAddr)> = vec![
799            (
800                "1.2.3.4:9999",
801                SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 9999),
802            ),
803            (
804                "[1:2::3:4]:9999",
805                SocketAddr::new(IpAddr::V6(Ipv6Addr::new(1, 2, 0, 0, 0, 0, 3, 4)), 9999),
806            ),
807            // // this is not a properly formatted ipv6 address
808            // ("1:2::3:4:9999",SocketAddr::new(IpAddr::V6(Ipv6Addr::new(1, 2, 0, 0, 0, 0, 3, 4)), 9999)),
809        ];
810
811        for trial in good {
812            let res = resolve_addr(trial.0).unwrap();
813            assert_eq!(res, trial.1);
814        }
815
816        for trial in bad {
817            assert!(resolve_addr(trial).is_err());
818        }
819        Ok(())
820    }
821
822    #[test]
823    fn managed_ver() -> Result<(), Error> {
824        let good = vec!["1", "1,1", "1,2", "2,1", "3,2,1", "3,1,2"];
825
826        for trial in good {
827            env::set_var(constants::MANAGED_VER, trial);
828            assert_eq!(
829                get_managed_transport_ver()?,
830                constants::CURRENT_TRANSPORT_VER
831            );
832        }
833
834        env::set_var(constants::MANAGED_VER, "");
835        assert!(get_managed_transport_ver().is_err());
836
837        env::set_var(constants::MANAGED_VER, "3,2");
838        assert!(get_managed_transport_ver().is_err());
839
840        Ok(())
841    }
842
843    // -- pt_should_exit_on_stdin_close --
844
845    #[test]
846    fn exit_on_stdin_close_returns_true_when_set() {
847        env::set_var(constants::EXIT_ON_STDIN_CLOSE, "1");
848        assert!(pt_should_exit_on_stdin_close());
849    }
850
851    #[test]
852    fn exit_on_stdin_close_returns_false_when_not_one() {
853        env::set_var(constants::EXIT_ON_STDIN_CLOSE, "0");
854        assert!(!pt_should_exit_on_stdin_close());
855
856        env::set_var(constants::EXIT_ON_STDIN_CLOSE, "yes");
857        assert!(!pt_should_exit_on_stdin_close());
858    }
859
860    #[test]
861    fn exit_on_stdin_close_returns_false_when_unset() {
862        env::remove_var(constants::EXIT_ON_STDIN_CLOSE);
863        assert!(!pt_should_exit_on_stdin_close());
864    }
865}