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.

834 lines
20 KiB
Rust

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::{Write, BufReader, BufRead, Read, self};
use std::net::TcpStream;
use simple_input::input;
use tempfile::NamedTempFile;
/* structs */
#[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 Config {
pub start_uri: String,
pub cmd_text: String,
pub cmd_image: String,
pub cmd_browser: String,
pub cmd_player: String,
pub color_prompt: String,
pub color_selector: String,
pub verbose: bool,
}
#[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 {
start_uri: String::from("gopher://gopher.floodgap.com:70"),
cmd_text: String::from("less"),
cmd_image: String::from("display"),
cmd_browser: String::from("firefox"),
cmd_player: String::from("mplayer"),
color_prompt: String::from("1;34"),
color_selector: String::from("1;32"),
verbose: false,
},
}
}
fn parse_config_line(&mut self, line: &str) {
let trimmed_line = line.trim_start();
let words = trimmed_line.split_whitespace().take(2).collect::<Vec<_>>();
match words[0] {
"start_uri" => self.config.start_uri = words[1].to_string(),
"cmd_text" => self.config.cmd_text = words[1].to_string(),
"cmd_browser" => self.config.cmd_browser = words[1].to_string(),
"cmd_image" => self.config.cmd_image = words[1].to_string(),
"cmd_player" => self.config.cmd_player = words[1].to_string(),
"color_prompt" => self.config.color_prompt = words[1].to_string(),
"color_selector" => self.config.color_selector = words[1].to_string(),
"verbose" => self.config.verbose = words[1].parse().unwrap_or_default(),
x if x.starts_with("bookmark") => self.bookmarks.push(words[1].to_string()),
x => {
eprintln!("invalid key in config: {}", x);
exit(1)
}
}
}
pub fn load_config<P: AsRef<Path>>(&mut self, filename: P) {
let file = match File::open(filename) {
Ok(f) => BufReader::new(f),
Err(_) => {
return;
}
};
file.lines()
.filter_map(|x| x.ok())
.filter(|x| !x.starts_with('#'))
.for_each(|line| self.parse_config_line(&line))
}
pub fn init_config(&mut self) {
/* read configs */
self.load_config("/etc/cgorc"); /* ignore incomplete selectors */
if let Some(dir) = dirs::home_dir() {
self.load_config(dir.join(".cgorc"));
}
}
pub fn download_file(
&mut 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;
}
return 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 = {
let mut v = line[1..].split('\t').collect::<Vec<_>>();
v.retain(|x| !x.is_empty());
v
};
/* 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!(
// "error: not a directory: [{}] {}",
// line.chars().next().unwrap_or('\0'),
// line.chars().skip(1).collect::<String>()
// );
// return;
// }
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;
}
};
// O_CREAT, O_NOCTTY, O_WRONLY, O_EXCL
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 = history_key + 1;
make_key_str(fresh10, &mut a, &mut b, &mut c);
println!("{}{}{} {}:{}/{}", 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.clone(),
&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;
}
/* return the array is broken after view! */
}
return 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, argc: usize, argv: Vec<String>) -> i32 {
let mut i: usize = 1;
/* copy defaults */
self.init_config();
let mut uri: String = self.config.start_uri.clone();
/* parse command line */
while i < argc {
if argv[i].chars().next() == Some('-') {
match argv[i].chars().skip(1).next() {
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.clone(),
&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('?') => {
println!("? - help\n* - reload directory\n< - go back in history\n.[LINK] - download the given link\nH - show history\nH[LINK] - jump to the specified history item\nG[URI] - jump to the given gopher URI\nB - show bookmarks\nB[LINK] - jump to the specified bookmark item\nC^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().skip(1).next().unwrap_or('\0'),
line.chars().skip(2).next().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().skip(1).next().unwrap_or('\0'),
line.chars().skip(2).next().unwrap_or('\0'),
));
},
Some('G') => {
if self.parse_uri(&line[1..]) {
self.view_directory(
&self.parsed_host.clone(),
self.parsed_port.clone(),
&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().skip(1).next().unwrap_or('\0'),
line.chars().skip(2).next().unwrap_or('\0'),
));
},
_ => {
self.follow_link(
make_key(
line.chars().next().unwrap(),
line.chars().skip(1).next().unwrap_or('\0'),
line.chars().skip(2).next().unwrap_or('\0'))
.unwrap_or(0),
);
}
}
}
/* never get's here but stops cc complaining */
}
}
/* implementation */
pub fn usage() {
eprintln!("usage: cgo [-v] [-H] [gopher URI]");
exit(0);
}
pub fn banner(to_error: bool) {
if to_error {
eprintln!("cgo 0.6.1 Copyright (c) 2020 Sebastian Steinhauer");
} else {
println!("cgo 0.6.1 Copyright (c) 2020 Sebastian Steinhauer");
}
}
pub fn dial(
host: &str,
port: u16,
selector: &str,
) -> Option<TcpStream> {
let mut stream = match TcpStream::connect((host, port)) {
Ok(s) => s,
Err(e) => {
eprintln!("failed to dial server: {}", e);
return None;
}
};
if let Err(e) = writeln!(stream, "{}", selector) {
eprintln!("failed to send request to server: {}", e);
return None;
};
Some(stream)
}
pub fn make_key(c1: char, c2: char, c3: char) -> Option<usize> {
if c1 == '\0' || c2 == '\0' {
return None;
}
if c3 == '\0' {
Some(
(c1 as u8 - 'a' as u8) as usize * ('z' as usize - 'a' as usize + 1)
+ (c2 as u8 - 'a' as u8) as usize,
)
} else {
Some(
((c1 as u8 - 'a' as u8) as usize + 1)
* ('z' as usize - 'a' as usize + 1)
* ('z' as usize - 'a' as usize + 1)
+ ((c2 as u8 - 'a' as u8) as usize) * ('z' as usize - 'a' as usize + 1)
+ ((c3 as u8 - 'a' as u8) as usize),
)
}
}
pub fn make_key_str(
key: usize,
c1: &mut char,
c2: &mut char,
c3: &mut char,
) {
if key
< ('z' as usize - 'a' as usize + 1 as usize)
* ('z' as usize - 'a' as usize + 1 as usize)
{
*c1 = ('a' as usize + key / ('z' as usize - 'a' as usize + 1 as usize)) as u8
as char;
*c2 = ('a' as usize + key % ('z' as usize - 'a' as usize + 1 as usize)) as u8
as char;
*c3 = 0 as usize as u8 as char
} else {
*c1 = ('a' as usize
+ key
/ (('z' as usize - 'a' as usize + 1 as usize)
* ('z' as usize - 'a' as usize + 1 as usize))
- 1 as usize) as u8 as char;
*c2 = ('a' as usize
+ key / ('z' as usize - 'a' as usize + 1 as usize)
% ('z' as usize - 'a' as usize + 1 as usize)) as u8 as char;
*c3 = ('a' as usize + key % ('z' as usize - 'a' as usize + 1 as usize)) as u8
as char
};
}
pub fn is_valid_directory_entry(kind: char) -> bool {
match kind {
'i' | '3' | '.' | '0' | '1' | '5' | '7' | '8' | '9' | 'g' | 'I' | 'p' | 'h'
| 's' => true,
_ => false,
}
}
pub fn view_telnet(host: &str, port: u16) {
println!("executing: telnet {} {}", host, port);
// TODO check stdio
match Command::new("telnet").arg(host).arg(port.to_string()).spawn() {
Ok(mut c) =>
if let Err(e) = c.wait() {
eprintln!("failed to wait for telnet: {}", e);
},
Err(e) => eprintln!("failed to start telnet: {}", e),
}
println!("(done)");
}
fn main() {
let mut state = BrowserState::new();
let args: Vec<String> = env::args().collect();
exit(state.init(args.len() - 1, args) as i32)
}