You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
685 lines
16 KiB
Rust
685 lines
16 KiB
Rust
#![allow(clippy::new_without_default)]
|
|
extern crate bunt;
|
|
extern crate dirs;
|
|
extern crate tempfile;
|
|
extern crate simple_input;
|
|
|
|
use std::env;
|
|
use std::path::Path;
|
|
use std::process::{exit, Command};
|
|
use std::thread::sleep;
|
|
use std::time::Duration;
|
|
use std::fs::{self, File};
|
|
use std::io::{Read, self};
|
|
|
|
use simple_input::input;
|
|
use tempfile::NamedTempFile;
|
|
|
|
mod config;
|
|
use config::{Config};
|
|
|
|
mod util;
|
|
use util::{usage, banner, dial, make_key, make_key_str, is_valid_directory_entry, view_telnet};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Link {
|
|
pub next: Option<Box<Link>>,
|
|
pub which: Option<char>,
|
|
pub key: usize,
|
|
pub host: String,
|
|
pub port: u16,
|
|
pub selector: String,
|
|
}
|
|
#[derive(Clone, Debug)]
|
|
pub struct BrowserState {
|
|
pub tmpfilename: String,
|
|
pub links: Option<Box<Link>>,
|
|
pub history: Option<Box<Link>>,
|
|
pub link_key: usize,
|
|
pub current_host: String,
|
|
pub current_port: u16,
|
|
pub current_selector: String,
|
|
pub parsed_host: String,
|
|
pub parsed_port: u16,
|
|
pub parsed_selector: String,
|
|
pub bookmarks: Vec<String>,
|
|
pub config: Config,
|
|
}
|
|
|
|
impl BrowserState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tmpfilename: String::new(),
|
|
links: None,
|
|
history: None,
|
|
link_key: 0,
|
|
current_host: String::new(),
|
|
current_port: 0,
|
|
current_selector: String::new(),
|
|
parsed_host: String::new(),
|
|
parsed_port: 0,
|
|
parsed_selector: String::new(),
|
|
bookmarks: Vec::new(),
|
|
config: Config::new(),
|
|
}
|
|
}
|
|
|
|
pub fn download_file(
|
|
&self,
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
file: &mut File,
|
|
) -> bool {
|
|
if self.config.verbose {
|
|
eprintln!("downloading [{}]...", selector);
|
|
}
|
|
let stream = dial(&host, port, selector);
|
|
if stream.is_none() {
|
|
eprintln!("error: downloading [{}] failed", selector);
|
|
return false;
|
|
}
|
|
// todo show progress
|
|
match io::copy(&mut stream.unwrap(), file) {
|
|
Ok(b) => eprintln!("downloaded {} bytes", b),
|
|
Err(e) => {
|
|
eprintln!("error: failed to download file: {}", e);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if self.config.verbose {
|
|
eprintln!("downloading [{}] complete", selector);
|
|
}
|
|
true
|
|
}
|
|
|
|
pub fn download_temp(
|
|
&mut self,
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
) -> bool {
|
|
let mut tmpfile = match NamedTempFile::new() {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
eprintln!("error: unable to create tmp file");
|
|
return false;
|
|
}
|
|
};
|
|
self.tmpfilename = tmpfile.path().display().to_string();
|
|
if !self.download_file(host, port, selector, tmpfile.as_file_mut()) {
|
|
fs::remove_file(&self.tmpfilename).expect("failed to delete temp file");
|
|
return false;
|
|
}
|
|
let _ = tmpfile.keep();
|
|
true
|
|
}
|
|
|
|
pub fn add_link(
|
|
&mut self,
|
|
which: char,
|
|
name: String,
|
|
host: String,
|
|
port: u16,
|
|
selector: String,
|
|
) {
|
|
let mut a: char = '\0';
|
|
let mut b: char = '\0';
|
|
let mut c: char = '\0';
|
|
if host.is_empty() || port == 0 || selector.is_empty() {
|
|
return;
|
|
}
|
|
let link = Link {
|
|
which: Some(which as u8 as char),
|
|
key: self.link_key,
|
|
host,
|
|
port,
|
|
selector,
|
|
next: if self.links.is_none() { None } else { self.links.clone() },
|
|
};
|
|
|
|
self.links = Some(Box::new(link));
|
|
make_key_str(self.link_key, &mut a, &mut b, &mut c);
|
|
self.link_key += 1;
|
|
bunt::println!("{[green]}{[green]}{[green]} {}", a, b, c, name);
|
|
}
|
|
|
|
pub fn clear_links(&mut self) {
|
|
self.links = None;
|
|
self.link_key = 0;
|
|
}
|
|
|
|
pub fn add_history(&mut self) {
|
|
let link: Link = Link {
|
|
which: None,
|
|
key: 0,
|
|
host: self.current_host.clone(),
|
|
port: self.current_port,
|
|
selector: self.current_selector.clone(),
|
|
next: if self.history.is_none() { None } else { self.history.clone() },
|
|
};
|
|
self.history = Some(Box::new(link));
|
|
}
|
|
|
|
pub fn handle_directory_line(&mut self, line: &str) {
|
|
let fields = {
|
|
line[1..].split('\t')
|
|
.enumerate()
|
|
.filter(|(i, x)| *i == 0 || !x.is_empty())
|
|
.map(|(_, x)| x)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
/* determine listing type */
|
|
match line.chars().next() {
|
|
Some('i') | Some('3') => println!(" {}", fields[0]),
|
|
Some('.') => println!("\0"), // some gopher servers use this
|
|
Some(w @ '0') | Some(w @ '1') | Some(w @ '5') | Some(w @ '7')
|
|
| Some(w @ '8') | Some(w @ '9') | Some(w @ 'g') | Some(w @ 'I')
|
|
| Some(w @ 'p') | Some(w @ 'h') | Some(w @ 's') => {
|
|
match fields.len() {
|
|
1 => self.add_link(
|
|
w,
|
|
fields[0].to_string(),
|
|
self.current_host.clone(),
|
|
self.current_port,
|
|
fields[0].to_string(),
|
|
),
|
|
2 => self.add_link(
|
|
w,
|
|
fields[0].to_string(),
|
|
self.current_host.clone(),
|
|
self.current_port,
|
|
fields[1].to_string(),
|
|
),
|
|
3 => self.add_link(
|
|
w,
|
|
fields[0].to_string(),
|
|
fields[2].to_string(),
|
|
self.current_port,
|
|
fields[1].to_string(),
|
|
),
|
|
x if x >= 4 => self.add_link(
|
|
w,
|
|
fields[0].to_string(),
|
|
fields[2].to_string(),
|
|
fields[3].parse().unwrap_or(70), // todo oof
|
|
fields[1].to_string(),
|
|
),
|
|
_ => (),
|
|
}
|
|
}
|
|
Some(x) => eprintln!("miss [{}]: {}", x, fields[0]),
|
|
None => (),
|
|
}
|
|
}
|
|
|
|
pub fn view_directory(
|
|
&mut self,
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
make_current: bool,
|
|
) {
|
|
let stream = dial(&host, port, selector);
|
|
let mut buffer = String::new();
|
|
self.clear_links();
|
|
let mut stream = match stream {
|
|
Some(s) => s,
|
|
None => return,
|
|
};
|
|
/* only adapt current prompt when successful */
|
|
/* make history entry */
|
|
if make_current {
|
|
self.add_history();
|
|
}
|
|
/* don't overwrite the current_* things... */
|
|
if host != self.current_host {
|
|
self.current_host = host.to_string();
|
|
} /* quit if not successful */
|
|
if port != self.current_port {
|
|
self.current_port = port;
|
|
}
|
|
if selector != self.current_selector {
|
|
self.current_selector = selector.to_string();
|
|
}
|
|
|
|
if let Err(e) = stream.read_to_string(&mut buffer) {
|
|
eprintln!("failed to read response body: {}", e);
|
|
}
|
|
|
|
for line in buffer.lines() {
|
|
if !is_valid_directory_entry(line.chars().next().unwrap_or('\0')) {
|
|
println!(
|
|
"invalid: [{}] {}",
|
|
line.chars().next().unwrap_or('\0'),
|
|
line.chars().skip(1).collect::<String>()
|
|
);
|
|
continue;
|
|
}
|
|
|
|
self.handle_directory_line(line);
|
|
}
|
|
}
|
|
|
|
pub fn view_file(
|
|
&mut self,
|
|
cmd: &str,
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
) {
|
|
if !self.download_temp(host, port, selector) {
|
|
return;
|
|
}
|
|
|
|
if self.config.verbose {
|
|
println!("h({}) p({}) s({})", host, port, selector);
|
|
}
|
|
if self.config.verbose {
|
|
println!("executing: {} {}", cmd, self.tmpfilename);
|
|
}
|
|
/* execute */
|
|
match Command::new(cmd).arg(&self.tmpfilename).spawn() {
|
|
Ok(mut c) =>
|
|
if let Err(e) = c.wait() {
|
|
eprintln!("failed to wait for command to exit: {}", e);
|
|
},
|
|
Err(e) => eprintln!("error: failed to run command: {}", e),
|
|
}
|
|
|
|
/* to wait for browsers etc. that return immediately */
|
|
sleep(Duration::from_secs(1));
|
|
fs::remove_file(&self.tmpfilename).expect("failed to delete temp file");
|
|
}
|
|
|
|
pub fn view_download(
|
|
&mut self,
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
) {
|
|
let mut filename: String =
|
|
Path::new(selector).file_name().unwrap_or_default().to_string_lossy().into();
|
|
let line: String = match input(&format!(
|
|
"enter filename for download [{}]: ",
|
|
filename
|
|
))
|
|
.as_str()
|
|
{
|
|
"" => {
|
|
println!("download aborted");
|
|
return;
|
|
}
|
|
x => x.into(),
|
|
};
|
|
filename = line; // TODO something stinky going on here
|
|
let mut file = match File::create(&filename) {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
println!("error: unable to create file [{}]: {}", filename, e,);
|
|
return;
|
|
}
|
|
};
|
|
if !self.download_file(host, port, selector, &mut file) {
|
|
println!("error: unable to download [{}]", selector);
|
|
fs::remove_file(filename).expect("failed to delete file");
|
|
}
|
|
}
|
|
|
|
pub fn view_search(
|
|
&mut self,
|
|
host: &str,
|
|
port: u16,
|
|
selector: &str,
|
|
) {
|
|
let search_selector: String = match input("enter search string: ").as_str() {
|
|
"" => {
|
|
println!("search aborted");
|
|
return;
|
|
}
|
|
s => format!("{}\t{}", selector, s),
|
|
};
|
|
self.view_directory(host, port, &search_selector, true);
|
|
}
|
|
|
|
pub fn view_history(&mut self, key: Option<usize>) {
|
|
let mut history_key: usize = 0;
|
|
let mut a: char = '\0';
|
|
let mut b: char = '\0';
|
|
let mut c: char = '\0';
|
|
let mut link: Option<Box<Link>>;
|
|
if self.history.is_none() {
|
|
println!("(empty history)");
|
|
return;
|
|
}
|
|
if key.is_none() {
|
|
println!("(history)");
|
|
link = self.history.clone();
|
|
while let Some(l) = link {
|
|
let fresh10 = history_key;
|
|
history_key += 1;
|
|
make_key_str(fresh10, &mut a, &mut b, &mut c);
|
|
bunt::println!("{[green]}{[green]}{[green]} {}:{}/{}", a, b, c, (*l).host, (*l).port, (*l).selector,);
|
|
link = (*l).next
|
|
}
|
|
} else if let Some(key) = key {
|
|
/* traverse history list */
|
|
link = self.history.clone();
|
|
while let Some(l) = link {
|
|
if history_key == key {
|
|
self.view_directory(
|
|
&(*l).host,
|
|
(*l).port,
|
|
&(*l).selector,
|
|
false,
|
|
);
|
|
return;
|
|
}
|
|
link = (*l).next;
|
|
history_key += 1
|
|
}
|
|
println!("history item not found");
|
|
};
|
|
}
|
|
|
|
pub fn view_bookmarks(&mut self, key: Option<usize>) {
|
|
let mut a: char = '\0';
|
|
let mut b: char = '\0';
|
|
let mut c: char = '\0';
|
|
if key.is_none() {
|
|
println!("(bookmarks)");
|
|
for (i, bookmark) in self.bookmarks.iter().enumerate() {
|
|
make_key_str(i, &mut a, &mut b, &mut c);
|
|
println!("{}{}{} {}", a, b, c, bookmark);
|
|
}
|
|
} else if let Some(key) = key {
|
|
for (i, bookmark) in self.bookmarks.clone().iter().enumerate() {
|
|
if i == key {
|
|
if self.parse_uri(bookmark) {
|
|
self.view_directory(
|
|
&self.parsed_host.clone(),
|
|
self.parsed_port,
|
|
&self.parsed_selector.clone(),
|
|
false,
|
|
);
|
|
} else {
|
|
println!("invalid gopher URI: {}", bookmark,);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn pop_history(&mut self) {
|
|
match &self.history.clone() {
|
|
None => println!("(empty history)"),
|
|
Some(h) => {
|
|
/* reload page from history (and don't count as history) */
|
|
self.view_directory(
|
|
&(*h).host,
|
|
(*h).port,
|
|
&(*h).selector,
|
|
false,
|
|
);
|
|
/* history is history... :) */
|
|
self.history = h.next.clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn follow_link(&mut self, key: usize) -> bool {
|
|
let mut link: Option<Box<Link>> = self.links.clone();
|
|
|
|
while let Some(ref l) = link {
|
|
if (*l).key != key {
|
|
link = (*l).next.clone()
|
|
} else if let Some(w) = (*l).which {
|
|
match w {
|
|
'0' => {
|
|
self.view_file(
|
|
&self.config.cmd_text.clone(),
|
|
&(*l).host,
|
|
(*l).port,
|
|
&(*l).selector,
|
|
);
|
|
}
|
|
'1' => {
|
|
self.view_directory(
|
|
&(*l).host,
|
|
(*l).port,
|
|
&(*l).selector,
|
|
true,
|
|
);
|
|
}
|
|
'7' => {
|
|
self.view_search(&(*l).host, (*l).port, &(*l).selector);
|
|
}
|
|
'5' | '9' => {
|
|
self.view_download(&(*l).host, (*l).port, &(*l).selector);
|
|
}
|
|
'8' => {
|
|
view_telnet(&(*l).host, (*l).port);
|
|
}
|
|
'f' | 'I' | 'p' => {
|
|
self.view_file(
|
|
&self.config.cmd_image.clone(),
|
|
&(*l).host,
|
|
(*l).port,
|
|
&(*l).selector,
|
|
);
|
|
}
|
|
'h' => {
|
|
self.view_file(
|
|
&self.config.cmd_browser.clone(),
|
|
&(*l).host,
|
|
(*l).port,
|
|
&(*l).selector,
|
|
);
|
|
}
|
|
's' => {
|
|
self.view_file(
|
|
&self.config.cmd_player.clone(),
|
|
&(*l).host,
|
|
(*l).port,
|
|
&(*l).selector,
|
|
);
|
|
}
|
|
_ => {
|
|
println!("missing handler [{}]", w);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
pub fn download_link(&mut self, key: usize) {
|
|
let mut link: Option<Box<Link>> = self.links.clone();
|
|
while let Some(l) = link {
|
|
if (*l).key != key {
|
|
link = (*l).next
|
|
} else {
|
|
self.view_download(&(*l).host, (*l).port, &(*l).selector);
|
|
return;
|
|
}
|
|
}
|
|
println!("link not found");
|
|
}
|
|
|
|
/* function prototypes */
|
|
pub fn parse_uri(&mut self, uri: &str) -> bool {
|
|
let mut tmp: &str = &uri[..];
|
|
/* strip gopher:// */
|
|
if uri.starts_with("gopher://") {
|
|
tmp = &uri[9..];
|
|
}
|
|
self.parsed_host =
|
|
tmp.chars().take_while(|x| !(*x == ':' || *x == '/')).collect();
|
|
|
|
if self.parsed_host.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
if tmp.contains(':') {
|
|
let port_string: String = tmp
|
|
.chars()
|
|
.skip_while(|x| *x != ':')
|
|
.skip(1)
|
|
.take_while(|x| *x != '/')
|
|
.collect();
|
|
|
|
if port_string.is_empty() {
|
|
self.parsed_port = 70;
|
|
} else {
|
|
self.parsed_port = match port_string.parse() {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
eprintln!("failed to parse port");
|
|
exit(1);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
tmp = &tmp[tmp.find('/').unwrap_or(0)..];
|
|
/* parse selector (ignore slash and selector type) */
|
|
if tmp.is_empty() || tmp.find('/').is_none() {
|
|
self.parsed_selector = "/".to_string();
|
|
} else {
|
|
self.parsed_selector = tmp.into();
|
|
}
|
|
true
|
|
}
|
|
|
|
pub fn init(&mut self, argv: Vec<String>) -> i32 {
|
|
let mut i: usize = 1;
|
|
/* copy defaults */
|
|
self.config.init();
|
|
self.bookmarks = self.config.bookmarks.clone();
|
|
|
|
let mut uri: String = self.config.start_uri.clone();
|
|
/* parse command line */
|
|
while i < argv.len() {
|
|
if argv[i].starts_with('-') {
|
|
match argv[i].chars().nth(1) {
|
|
Some('H') => {
|
|
usage();
|
|
}
|
|
Some('v') => {
|
|
banner(true);
|
|
exit(0);
|
|
}
|
|
_ => {
|
|
usage();
|
|
}
|
|
}
|
|
} else {
|
|
uri = argv[i].clone();
|
|
}
|
|
i += 1
|
|
}
|
|
/* parse uri */
|
|
if !self.parse_uri(&uri) {
|
|
banner(false);
|
|
eprintln!("invalid gopher URI: {}", argv[i],);
|
|
exit(1);
|
|
}
|
|
/* main loop */
|
|
self.view_directory(
|
|
&self.parsed_host.clone(),
|
|
self.parsed_port,
|
|
&self.parsed_selector.clone(),
|
|
false,
|
|
); /* to display the prompt */
|
|
loop {
|
|
bunt::print!(
|
|
"{[blue]}:{[blue]}{[blue]} ",
|
|
self.current_host, self.current_port, &self.current_selector
|
|
);
|
|
let line = input("");
|
|
|
|
match line.chars().next() {
|
|
Some('?') => {
|
|
bunt::println!(
|
|
"{$yellow}?{/$} help\
|
|
\n{$yellow}*{/$} reload directory\
|
|
\n{$yellow}<{/$} go back in history\
|
|
\n{$yellow}.[LINK]{/$} download the given link\
|
|
\n{$yellow}H{/$} show history\
|
|
\n{$yellow}H[LINK]{/$} jump to the specified history item\
|
|
\n{$yellow}G[URI]{/$} jump to the given gopher URI\
|
|
\n{$yellow}B{/$} show bookmarks\
|
|
\n{$yellow}B[LINK]{/$} jump to the specified bookmark item\
|
|
\n{$yellow}C^d{/$} quit");
|
|
}
|
|
Some('<') => {
|
|
self.pop_history();
|
|
}
|
|
Some('*') => {
|
|
self.view_directory(
|
|
&self.current_host.clone(),
|
|
self.current_port,
|
|
&self.current_selector.clone(),
|
|
false,
|
|
);
|
|
}
|
|
Some('.') => {
|
|
self.download_link(
|
|
make_key(
|
|
line.chars().next().unwrap(),
|
|
line.chars().nth(1).unwrap_or('\0'),
|
|
line.chars().nth(2).unwrap_or('\0'))
|
|
.unwrap_or(0),
|
|
);
|
|
}
|
|
Some('H') =>
|
|
if i == 1 || i == 3 || i == 4 {
|
|
self.view_history(make_key(
|
|
line.chars().next().unwrap(),
|
|
line.chars().nth(1).unwrap_or('\0'),
|
|
line.chars().nth(2).unwrap_or('\0'),
|
|
));
|
|
},
|
|
Some('G') => {
|
|
if self.parse_uri(&line[1..]) {
|
|
self.view_directory(
|
|
&self.parsed_host.clone(),
|
|
self.parsed_port,
|
|
&self.parsed_selector.clone(),
|
|
true,
|
|
);
|
|
} else {
|
|
println!("invalid gopher URI");
|
|
}
|
|
}
|
|
Some('B') =>
|
|
if i == 1 || i == 3 || i == 4 {
|
|
self.view_bookmarks(make_key(
|
|
line.chars().next().unwrap(),
|
|
line.chars().nth(1).unwrap_or('\0'),
|
|
line.chars().nth(2).unwrap_or('\0'),
|
|
));
|
|
},
|
|
x if x.is_some() => {
|
|
self.follow_link(
|
|
make_key(
|
|
line.chars().next().unwrap(),
|
|
line.chars().nth(1).unwrap_or('\0'),
|
|
line.chars().nth(2).unwrap_or('\0'))
|
|
.unwrap_or(0),
|
|
);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let mut state = BrowserState::new();
|
|
let args: Vec<String> = env::args().collect();
|
|
exit(state.init(args))
|
|
}
|