diff --git a/Cargo.lock b/Cargo.lock index d68b980..7bc3a74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,11 @@ name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "byteorder" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "c2-chacha" version = "0.2.3" @@ -142,6 +147,7 @@ dependencies = [ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-stream 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -240,6 +246,17 @@ dependencies = [ "core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "socks" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tempfile" version = "3.1.0" @@ -264,6 +281,15 @@ dependencies = [ "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tor-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "socks 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vcpkg" version = "0.2.8" @@ -274,6 +300,11 @@ name = "wasi" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.3.8" @@ -283,6 +314,11 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -293,9 +329,19 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" "checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" @@ -324,10 +370,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum schannel 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021" "checksum security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8ef2429d7cefe5fd28bd1d2ed41c944547d4ff84776f5935b456da44593a16df" "checksum security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e31493fc37615debb8c5090a7aeb4a9730bc61e77ab10b9af59f1a202284f895" +"checksum socks 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e6a64cfa9346d26e836a49fcc1ddfcb4d3df666b6787b6864db61d4918e1cbc2" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" +"checksum tor-stream 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5865109fc90e0bc0f8c299f3794ca0fd5771df988aa6b962d4c9129c39674746" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" diff --git a/Cargo.toml b/Cargo.toml index dd2e3df..f63244d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,4 @@ dev-version-ext = "dev" termion = "1.5.3" native-tls = "0.2" libc = "0.2.66" +tor-stream = "0.2.0" \ No newline at end of file diff --git a/README.md b/README.md index cc90d45..095093c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ the gophersphere. Options: -t, --tls Try to open all pages w/ TLS + -T, --tor Try to open all pages w/ Tor + Set the TOR_PROXY env variable to use + an address other than the default :9050 -r, --raw Print raw Gopher response only -p, --print Print rendered Gopher response only -l, --local Connect to 127.0.0.1:7070 diff --git a/src/config.rs b/src/config.rs index 63f2c4f..74cdc3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,9 @@ start gopher://phetch/1/home # Always use TLS mode. (--tls) tls no +# Connect using local TOR proxy. (--tor) +tor no + # Always start in wide mode. (--wide) wide no "; @@ -26,6 +29,7 @@ wide no pub struct Config { pub start: String, pub tls: bool, + pub tor: bool, pub wide: bool, } @@ -60,6 +64,7 @@ pub fn parse(text: &str) -> Result { let mut cfg = Config { start: String::new(), tls: false, + tor: false, wide: false, }; @@ -89,6 +94,7 @@ pub fn parse(text: &str) -> Result { match key { "start" => cfg.start.push_str(val), "tls" => cfg.tls = to_bool(val)?, + "tor" => cfg.tor = to_bool(val)?, "wide" => cfg.wide = to_bool(val)?, _ => return Err(error!("Unknown key on line {}: {}", linenum, key)), } @@ -116,6 +122,7 @@ mod tests { fn test_parse_default() { let config = parse(DEFAULT_CONFIG).expect("Couldn't parse config"); assert_eq!(config.tls, false); + assert_eq!(config.tor, false); assert_eq!(config.wide, false); assert_eq!(config.start, "gopher://phetch/1/home"); } @@ -148,8 +155,9 @@ mod tests { #[test] fn test_no_or_false() { - let cfg = parse("tls false\nwide no").unwrap(); + let cfg = parse("tls false\nwide no\ntor n").unwrap(); assert_eq!(cfg.tls, false); + assert_eq!(cfg.tor, false); assert_eq!(cfg.wide, false); } #[test] diff --git a/src/gopher.rs b/src/gopher.rs index 20af3f7..7f841f2 100644 --- a/src/gopher.rs +++ b/src/gopher.rs @@ -1,4 +1,5 @@ use std::{ + env, io::{Read, Result, Write}, net::TcpStream, net::ToSocketAddrs, @@ -6,6 +7,7 @@ use std::{ time::Duration, }; use termion::input::TermRead; +use tor_stream::TorStream; #[cfg(not(feature = "disable-tls"))] use native_tls::TlsConnector; @@ -51,15 +53,21 @@ pub const TCP_TIMEOUT_DURATION: Duration = Duration::from_secs(TCP_TIMEOUT_IN_SE /// Fetches a gopher URL and returns a tuple of: /// (did tls work?, raw Gopher response) -pub fn fetch_url(url: &str, try_tls: bool) -> Result<(bool, String)> { +pub fn fetch_url(url: &str, tls: bool, tor: bool) -> Result<(bool, String)> { let (_, host, port, sel) = parse_url(url); - fetch(host, port, sel, try_tls) + fetch(host, port, sel, tls, tor) } /// Fetches a gopher URL by its component parts and returns a tuple of: /// (did tls work?, raw Gopher response) -pub fn fetch(host: &str, port: &str, selector: &str, try_tls: bool) -> Result<(bool, String)> { - let mut stream = request(host, port, selector, try_tls)?; +pub fn fetch( + host: &str, + port: &str, + selector: &str, + tls: bool, + tor: bool, +) -> Result<(bool, String)> { + let mut stream = request(host, port, selector, tls, tor)?; let mut body = Vec::new(); stream.read_to_end(&mut body)?; let out = clean_response(&String::from_utf8_lossy(&body)); @@ -81,7 +89,7 @@ fn clean_response(res: &str) -> String { /// Downloads a binary to disk. Allows canceling with Ctrl-c. /// Returns a tuple of: /// (path it was saved to, the size in bytes) -pub fn download_url(url: &str, try_tls: bool) -> Result<(String, usize)> { +pub fn download_url(url: &str, tls: bool, tor: bool) -> Result<(String, usize)> { let (_, host, port, sel) = parse_url(url); let filename = sel .split_terminator('/') @@ -93,7 +101,7 @@ pub fn download_url(url: &str, try_tls: bool) -> Result<(String, usize)> { let stdin = termion::async_stdin(); let mut keys = stdin.keys(); - let mut stream = request(host, port, sel, try_tls)?; + let mut stream = request(host, port, sel, tls, tor)?; let mut file = std::fs::OpenOptions::new() .write(true) .create(true) @@ -119,36 +127,54 @@ pub fn download_url(url: &str, try_tls: bool) -> Result<(String, usize)> { /// Make a Gopher request and return a TcpStream ready to be read()'d. /// Will attempt a TLS connection first, then retry a regular /// connection if it fails. -pub fn request(host: &str, port: &str, selector: &str, try_tls: bool) -> Result { +pub fn request(host: &str, port: &str, selector: &str, tls: bool, tor: bool) -> Result { let selector = selector.replace('?', "\t"); // search queries let sock = format!("{}:{}", host, port) .to_socket_addrs() .and_then(|mut socks| socks.next().ok_or_else(|| error!("Can't create socket")))?; // attempt tls connection - #[cfg(not(feature = "disable-tls"))] - { - if try_tls { - if let Ok(connector) = TlsConnector::new() { - let stream = TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)?; - stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION))?; - if let Ok(mut stream) = connector.connect(host, stream) { - stream.write(format!("{}\r\n", selector).as_ref())?; - return Ok(Stream { - io: Box::new(stream), - tls: true, - }); + if tls { + #[cfg(not(feature = "disable-tls"))] + { + { + if let Ok(connector) = TlsConnector::new() { + let stream = TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)?; + stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION))?; + if let Ok(mut stream) = connector.connect(host, stream) { + stream.write(format!("{}\r\n", selector).as_ref())?; + return Ok(Stream { + io: Box::new(stream), + tls: true, + }); + } } } } } - let mut stream = TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)?; - stream.write(format!("{}\r\n", selector).as_ref())?; - Ok(Stream { - io: Box::new(stream), - tls: false, - }) + // tls didn't work, try regular + if tor { + let proxy = env::var("TOR_PROXY") + .unwrap_or("127.0.0.1:9050".into()) + .to_socket_addrs()? + .nth(0) + .unwrap(); + let mut stream = TorStream::connect_with_address(proxy, sock)?; + stream.write(format!("{}\r\n", selector).as_ref())?; + Ok(Stream { + io: Box::new(stream), + tls: false, + }) + } else { + let mut stream = TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)?; + stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION))?; + stream.write(format!("{}\r\n", selector).as_ref())?; + Ok(Stream { + io: Box::new(stream), + tls: false, + }) + } } /// Parses gopher URL into parts. diff --git a/src/main.rs b/src/main.rs index f5a56cd..6c3c215 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ fn run() -> i32 { return 1; } } + "-T" | "--tor" | "-tor" => cfg.tor = true, arg => { if arg.starts_with('-') { print_version(); @@ -75,13 +76,19 @@ fn run() -> i32 { } } + if cfg.tor && cfg.tls { + eprintln!("Can't set both --tor and --tls."); + return 1; + } + if mode == Mode::Raw { - print_raw(&cfg.start, cfg.tls); + print_raw(&cfg.start, cfg.tls, cfg.tor); return 0; } - let mut ui = UI::new(cfg.tls); - if let Err(e) = ui.open(&cfg.start, &cfg.start) { + let start = cfg.start.clone(); + let mut ui = UI::new(cfg); + if let Err(e) = ui.open(&start, &start) { eprintln!("{}", e); return 1; } @@ -127,6 +134,9 @@ Usage: Options: -t, --tls Try to open all pages w/ TLS + -T, --tor Try to open all pages w/ Tor + Set the TOR_PROXY env variable to use + an address other than the default :9050 -r, --raw Print raw Gopher response only -p, --print Print rendered Gopher response only -l, --local Connect to 127.0.0.1:7070 @@ -138,8 +148,8 @@ Once you've launched phetch, use `ctrl-h` to view the on-line help." ); } -fn print_raw(url: &str, try_tls: bool) { - match gopher::fetch_url(url, try_tls) { +fn print_raw(url: &str, tls: bool, tor: bool) { + match gopher::fetch_url(url, tls, tor) { Ok((_, response)) => println!("{}", response), Err(e) => { eprintln!("{}", e); diff --git a/src/menu.rs b/src/menu.rs index 7e589b6..594da46 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -14,6 +14,7 @@ pub struct Menu { pub scroll: usize, // scrolling offset pub searching: bool, // search mode? pub tls: bool, // retrieved via tls? + pub tor: bool, // retrieved via tor? pub size: (usize, usize), // cols, rows pub wide: bool, // in wide mode? } @@ -44,6 +45,10 @@ impl View for Menu { self.tls } + fn is_tor(&self) -> bool { + self.tor + } + fn raw(&self) -> String { self.raw.to_string() } @@ -66,10 +71,12 @@ impl View for Menu { } impl Menu { - pub fn from(url: String, response: String, tls: bool) -> Menu { - let mut menu = Self::parse(url, response); - menu.tls = tls; - menu + pub fn from(url: String, response: String, tls: bool, tor: bool) -> Menu { + Menu { + tls, + tor, + ..Self::parse(url, response) + } } fn cols(&self) -> usize { @@ -797,6 +804,7 @@ impl Menu { searching: false, size: (0, 0), tls: false, + tor: false, wide: false, } } diff --git a/src/text.rs b/src/text.rs index 536df06..b52ae32 100644 --- a/src/text.rs +++ b/src/text.rs @@ -10,6 +10,7 @@ pub struct Text { longest: usize, // longest line size: (usize, usize), // cols, rows pub tls: bool, // retrieved via tls? + pub tor: bool, // retrieved via tor? pub wide: bool, // in wide mode? turns off margins } @@ -24,6 +25,10 @@ impl View for Text { self.tls } + fn is_tor(&self) -> bool { + self.tor + } + fn url(&self) -> String { self.url.to_string() } @@ -135,7 +140,7 @@ impl View for Text { } impl Text { - pub fn from(url: String, response: String, tls: bool) -> Text { + pub fn from(url: String, response: String, tls: bool, tor: bool) -> Text { let mut lines = 0; let mut longest = 0; for line in response.split_terminator('\n') { @@ -153,6 +158,7 @@ impl Text { longest, size: (0, 0), tls, + tor, wide: false, } } diff --git a/src/ui.rs b/src/ui.rs index 2f7621a..c1eda50 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -5,6 +5,7 @@ pub use self::view::View; use crate::{ bookmarks, color, + config::Config, gopher::{self, Type}, help, history, menu::Menu, @@ -39,12 +40,12 @@ pub struct UI { running: bool, // main ui loop running? pub size: (usize, usize), // cols, rows status: String, // status message, if any - tls: bool, // tls mode? + config: Config, // user config out: RefCell>>, } impl UI { - pub fn new(tls: bool) -> UI { + pub fn new(config: Config) -> UI { let mut size = (0, 0); if let Ok((cols, rows)) = terminal_size() { size = (cols as usize, rows as usize); @@ -66,8 +67,8 @@ impl UI { dirty: true, running: true, size, + config, status: String::new(), - tls, out: RefCell::new(out), } } @@ -163,9 +164,9 @@ impl UI { fn download(&mut self, url: &str) -> Result<()> { let url = url.to_string(); - let tls = self.tls; + let (tls, tor) = (self.config.tls, self.config.tor); self.spinner(&format!("Downloading {}", url), move || { - gopher::download_url(&url, tls) + gopher::download_url(&url, tls, tor) }) .and_then(|res| res) .and_then(|(path, bytes)| { @@ -189,17 +190,17 @@ impl UI { thread::spawn(move || history::save(&hname, &hurl)); // request thread let thread_url = url.to_string(); - let try_tls = self.tls; + let (tls, tor) = (self.config.tls, self.config.tor); // don't spin on first ever request let (tls, res) = if self.views.is_empty() { - gopher::fetch_url(&thread_url, try_tls)? + gopher::fetch_url(&thread_url, tls, tor)? } else { - self.spinner("", move || gopher::fetch_url(&thread_url, try_tls))?? + self.spinner("", move || gopher::fetch_url(&thread_url, tls, tor))?? }; let (typ, _, _, _) = gopher::parse_url(&url); match typ { - Type::Menu | Type::Search => Ok(Box::new(Menu::from(url.to_string(), res, tls))), - Type::Text | Type::HTML => Ok(Box::new(Text::from(url.to_string(), res, tls))), + Type::Menu | Type::Search => Ok(Box::new(Menu::from(url.to_string(), res, tls, tor))), + Type::Text | Type::HTML => Ok(Box::new(Text::from(url.to_string(), res, tls, tor))), _ => Err(error!("Unsupported Gopher Response: {:?}", typ)), } } @@ -210,7 +211,7 @@ impl UI { &url.trim_start_matches("gopher://phetch/") .trim_start_matches("1/"), ) { - Ok(Box::new(Menu::from(url.to_string(), source, false))) + Ok(Box::new(Menu::from(url.to_string(), source, false, false))) } else { Err(error!("phetch URL not found: {}", url)) } @@ -296,12 +297,15 @@ impl UI { let page = self.views.get(self.focused)?; if page.is_tls() { return Some(format!( - "{}{}{}{}{}", + "{}{}", termion::cursor::Goto(self.cols() - 3, self.rows()), - color::Black, - color::GreenBG, - "TLS", - "\x1b[0m" + color!("TLS", Black, GreenBG), + )); + } else if page.is_tor() { + return Some(format!( + "{}{}", + termion::cursor::Goto(self.cols() - 3, self.rows()), + color!("TOR", Bold, White, MagentaBG), )); } None @@ -521,7 +525,7 @@ impl UI { if let Some(page) = self.views.get(self.focused) { let url = page.url(); let raw = page.raw(); - let mut text = Text::from(url, raw, page.is_tls()); + let mut text = Text::from(url, raw, page.is_tls(), page.is_tor()); text.wide = true; self.add_page(Box::new(text)); } @@ -561,12 +565,6 @@ impl UI { } } -impl Default for UI { - fn default() -> Self { - UI::new(false) - } -} - impl Drop for UI { fn drop(&mut self) { let mut out = self.out.borrow_mut(); diff --git a/src/ui/view.rs b/src/ui/view.rs index 1827ff4..52754ed 100644 --- a/src/ui/view.rs +++ b/src/ui/view.rs @@ -5,6 +5,7 @@ pub trait View: fmt::Display { fn respond(&mut self, key: ui::Key) -> ui::Action; fn render(&self) -> String; fn is_tls(&self) -> bool; + fn is_tor(&self) -> bool; fn url(&self) -> String; fn raw(&self) -> String; fn term_size(&mut self, cols: usize, rows: usize);