2019-12-17 23:34:42 +00:00
|
|
|
use gopher;
|
2019-12-16 23:54:20 +00:00
|
|
|
use std::io;
|
2019-12-21 18:40:37 +00:00
|
|
|
use std::io::{Read, Result, Write};
|
2019-12-16 23:54:20 +00:00
|
|
|
use std::net::TcpStream;
|
2019-12-19 04:23:26 +00:00
|
|
|
use std::net::ToSocketAddrs;
|
2019-12-21 02:39:39 +00:00
|
|
|
use std::os::unix::fs::OpenOptionsExt;
|
2019-12-19 04:23:26 +00:00
|
|
|
use std::time::Duration;
|
|
|
|
|
2019-12-21 01:16:21 +00:00
|
|
|
pub const TCP_TIMEOUT_IN_SECS: u64 = 1;
|
2019-12-19 04:23:26 +00:00
|
|
|
pub const TCP_TIMEOUT_DURATION: Duration = Duration::from_secs(TCP_TIMEOUT_IN_SECS);
|
2019-12-16 23:54:20 +00:00
|
|
|
|
|
|
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
|
|
|
pub enum Type {
|
2019-12-19 02:07:47 +00:00
|
|
|
Text, // 0 | 96 | cyan
|
|
|
|
Menu, // 1 | 94 | blue
|
|
|
|
CSOEntity, // 2
|
|
|
|
Error, // 3 | 91 | red
|
|
|
|
Binhex, // 4 | 4 | white underline
|
|
|
|
DOSFile, // 5 | 4 | white underline
|
|
|
|
UUEncoded, // 6 | 4 | white underline
|
|
|
|
Search, // 7 | 0 | white
|
|
|
|
Telnet, // 8
|
|
|
|
Binary, // 9 | 4 | white underline
|
|
|
|
Mirror, // +
|
|
|
|
GIF, // g | 4 | white underline
|
|
|
|
Telnet3270, // T
|
|
|
|
HTML, // h | 92 | green
|
2019-12-19 02:08:45 +00:00
|
|
|
Image, // I | 4 | white underline
|
2019-12-20 20:58:41 +00:00
|
|
|
PNG, // p | 4 | white underline
|
2019-12-19 02:07:47 +00:00
|
|
|
Info, // i | 93 | yellow
|
|
|
|
Sound, // s | 4 | white underline
|
|
|
|
Document, // d | 4 | white underline
|
2019-12-17 21:23:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Type {
|
2019-12-18 22:47:36 +00:00
|
|
|
pub fn is_download(self) -> bool {
|
2019-12-17 21:23:59 +00:00
|
|
|
match self {
|
|
|
|
Type::Binhex
|
|
|
|
| Type::DOSFile
|
|
|
|
| Type::UUEncoded
|
|
|
|
| Type::Binary
|
|
|
|
| Type::GIF
|
2019-12-19 02:08:45 +00:00
|
|
|
| Type::Image
|
2019-12-20 20:58:41 +00:00
|
|
|
| Type::PNG
|
2019-12-17 21:23:59 +00:00
|
|
|
| Type::Sound
|
|
|
|
| Type::Document => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
2019-12-16 23:54:20 +00:00
|
|
|
}
|
|
|
|
|
2019-12-17 23:34:42 +00:00
|
|
|
pub fn type_for_char(c: char) -> Option<Type> {
|
|
|
|
match c {
|
|
|
|
'0' => Some(Type::Text),
|
|
|
|
'1' => Some(Type::Menu),
|
|
|
|
'2' => Some(Type::CSOEntity),
|
|
|
|
'3' => Some(Type::Error),
|
|
|
|
'4' => Some(Type::Binhex),
|
|
|
|
'5' => Some(Type::DOSFile),
|
|
|
|
'6' => Some(Type::UUEncoded),
|
|
|
|
'7' => Some(Type::Search),
|
|
|
|
'8' => Some(Type::Telnet),
|
|
|
|
'9' => Some(Type::Binary),
|
|
|
|
'+' => Some(Type::Mirror),
|
|
|
|
'g' => Some(Type::GIF),
|
|
|
|
'T' => Some(Type::Telnet3270),
|
|
|
|
'h' => Some(Type::HTML),
|
2019-12-19 01:41:19 +00:00
|
|
|
'I' => Some(Type::Image),
|
2019-12-20 20:58:41 +00:00
|
|
|
'p' => Some(Type::PNG),
|
2019-12-17 23:34:42 +00:00
|
|
|
'i' => Some(Type::Info),
|
|
|
|
's' => Some(Type::Sound),
|
|
|
|
'd' => Some(Type::Document),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 05:21:40 +00:00
|
|
|
// produces an io::Error more easily
|
2019-12-21 18:40:37 +00:00
|
|
|
pub fn error(msg: String) -> io::Error {
|
2019-12-19 05:21:40 +00:00
|
|
|
io::Error::new(io::ErrorKind::Other, msg)
|
|
|
|
}
|
|
|
|
|
2019-12-19 03:57:16 +00:00
|
|
|
// Fetches a gopher URL and returns a raw Gopher response.
|
2019-12-21 18:40:37 +00:00
|
|
|
pub fn fetch_url(url: &str) -> Result<String> {
|
2019-12-17 05:47:33 +00:00
|
|
|
let (_, host, port, sel) = parse_url(url);
|
|
|
|
fetch(host, port, sel)
|
|
|
|
}
|
|
|
|
|
2019-12-19 03:57:16 +00:00
|
|
|
// Fetches a gopher URL by its component parts and returns a raw Gopher response.
|
2019-12-21 18:40:37 +00:00
|
|
|
pub fn fetch(host: &str, port: &str, selector: &str) -> Result<String> {
|
2019-12-16 23:54:20 +00:00
|
|
|
let mut body = String::new();
|
2019-12-19 00:55:02 +00:00
|
|
|
let selector = selector.replace('?', "\t"); // search queries
|
2019-12-16 23:54:20 +00:00
|
|
|
|
2019-12-19 04:23:26 +00:00
|
|
|
format!("{}:{}", host, port)
|
|
|
|
.to_socket_addrs()
|
|
|
|
.and_then(|mut socks| {
|
|
|
|
socks
|
|
|
|
.next()
|
2019-12-21 18:40:37 +00:00
|
|
|
.ok_or_else(|| error("Can't create socket".to_string()))
|
2019-12-21 16:50:23 +00:00
|
|
|
})
|
|
|
|
.and_then(|sock| TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION))
|
|
|
|
.and_then(|mut stream| {
|
|
|
|
stream.write(format!("{}\r\n", selector).as_ref());
|
|
|
|
Ok(stream)
|
|
|
|
})
|
|
|
|
.and_then(|mut stream| {
|
|
|
|
stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION));
|
|
|
|
stream.read_to_string(&mut body)?;
|
|
|
|
Ok(body)
|
2019-12-19 04:23:26 +00:00
|
|
|
})
|
2019-12-16 23:54:20 +00:00
|
|
|
}
|
2019-12-17 01:01:23 +00:00
|
|
|
|
2019-12-21 02:11:20 +00:00
|
|
|
// Downloads a binary to disk and returns the path it was saved to.
|
2019-12-21 18:40:37 +00:00
|
|
|
pub fn download_url(url: &str) -> Result<String> {
|
2019-12-21 02:11:20 +00:00
|
|
|
let (_, host, port, sel) = parse_url(url);
|
|
|
|
let sel = sel.replace('?', "\t"); // search queries
|
2019-12-21 02:32:08 +00:00
|
|
|
let filename = sel
|
|
|
|
.split_terminator('/')
|
|
|
|
.rev()
|
|
|
|
.nth(0)
|
|
|
|
.unwrap_or(&"download");
|
2019-12-21 02:22:33 +00:00
|
|
|
let mut path = std::path::PathBuf::from(".");
|
|
|
|
path.push(filename);
|
2019-12-21 02:11:20 +00:00
|
|
|
|
|
|
|
format!("{}:{}", host, port)
|
|
|
|
.to_socket_addrs()
|
|
|
|
.and_then(|mut socks| {
|
|
|
|
socks
|
|
|
|
.next()
|
2019-12-21 18:40:37 +00:00
|
|
|
.ok_or_else(|| error("Can't create socket".to_string()))
|
2019-12-21 16:50:23 +00:00
|
|
|
})
|
|
|
|
.and_then(|sock| TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION))
|
|
|
|
.and_then(|mut stream| {
|
|
|
|
stream.write(format!("{}\r\n", sel).as_ref());
|
|
|
|
Ok(stream)
|
|
|
|
})
|
|
|
|
.and_then(|mut stream| {
|
|
|
|
stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION));
|
|
|
|
std::fs::OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create(true)
|
|
|
|
.truncate(true)
|
|
|
|
.mode(0o770)
|
|
|
|
.open(path)
|
|
|
|
.and_then(|mut file| {
|
2019-12-21 17:27:01 +00:00
|
|
|
let mut buf = [0 as u8; 8]; // read 8 bytes at a time
|
2019-12-21 16:50:23 +00:00
|
|
|
while let Ok(count) = stream.read(&mut buf) {
|
|
|
|
if count == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
file.write_all(&buf);
|
|
|
|
}
|
|
|
|
Ok(filename.to_string())
|
2019-12-21 02:11:20 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-12-19 00:55:02 +00:00
|
|
|
// url parsing states
|
2019-12-17 01:01:23 +00:00
|
|
|
enum Parsing {
|
|
|
|
Host,
|
|
|
|
Port,
|
|
|
|
Selector,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parses gopher URL into parts.
|
2019-12-18 22:47:36 +00:00
|
|
|
pub fn parse_url(url: &str) -> (Type, &str, &str, &str) {
|
2019-12-17 01:01:23 +00:00
|
|
|
let url = url.trim_start_matches("gopher://");
|
|
|
|
|
|
|
|
let mut host = "";
|
|
|
|
let mut port = "70";
|
|
|
|
let mut sel = "/";
|
2019-12-17 01:53:34 +00:00
|
|
|
let mut typ = Type::Menu;
|
2019-12-17 01:01:23 +00:00
|
|
|
let mut state = Parsing::Host;
|
|
|
|
let mut start = 0;
|
|
|
|
|
|
|
|
for (i, c) in url.char_indices() {
|
|
|
|
match state {
|
|
|
|
Parsing::Host => {
|
2019-12-21 02:27:38 +00:00
|
|
|
state = match c {
|
|
|
|
':' => Parsing::Port,
|
|
|
|
'/' => Parsing::Selector,
|
2019-12-17 01:01:23 +00:00
|
|
|
_ => continue,
|
2019-12-21 02:27:38 +00:00
|
|
|
};
|
2019-12-17 01:01:23 +00:00
|
|
|
host = &url[start..i];
|
2019-12-17 05:56:54 +00:00
|
|
|
start = if c == '/' { i } else { i + 1 };
|
2019-12-17 01:01:23 +00:00
|
|
|
}
|
|
|
|
Parsing::Port => {
|
|
|
|
if c == '/' {
|
|
|
|
state = Parsing::Selector;
|
|
|
|
port = &url[start..i];
|
2019-12-17 05:56:54 +00:00
|
|
|
start = i;
|
2019-12-17 01:01:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Parsing::Selector => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
match state {
|
|
|
|
Parsing::Selector => sel = &url[start..],
|
|
|
|
Parsing::Port => port = &url[start..],
|
|
|
|
Parsing::Host => host = &url[start..],
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut chars = sel.chars();
|
2019-12-17 23:34:42 +00:00
|
|
|
if let (Some('/'), Some(c), Some('/')) = (chars.nth(0), chars.nth(0), chars.nth(0)) {
|
|
|
|
if let Some(t) = gopher::type_for_char(c) {
|
|
|
|
typ = t;
|
|
|
|
sel = &sel[2..];
|
2019-12-17 01:01:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
(typ, host, port, sel)
|
|
|
|
}
|
2019-12-20 22:50:58 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_simple_parse() {
|
|
|
|
let urls = vec![
|
|
|
|
"gopher://gopher.club/1/phlogs/",
|
|
|
|
"gopher://sdf.org:7777/1/maps",
|
|
|
|
"gopher.floodgap.org",
|
|
|
|
"gopher.floodgap.com/0/gopher/relevance.txt",
|
|
|
|
"gopher://gopherpedia.com/7/lookup?Gopher",
|
|
|
|
];
|
|
|
|
|
|
|
|
let (typ, host, port, sel) = parse_url(urls[0]);
|
|
|
|
assert_eq!(typ, Type::Menu);
|
|
|
|
assert_eq!(host, "gopher.club");
|
|
|
|
assert_eq!(port, "70");
|
|
|
|
assert_eq!(sel, "/phlogs/");
|
|
|
|
|
|
|
|
let (typ, host, port, sel) = parse_url(urls[1]);
|
|
|
|
assert_eq!(typ, Type::Menu);
|
|
|
|
assert_eq!(host, "sdf.org");
|
|
|
|
assert_eq!(port, "7777");
|
|
|
|
assert_eq!(sel, "/maps");
|
|
|
|
|
|
|
|
let (typ, host, port, sel) = parse_url(urls[2]);
|
|
|
|
assert_eq!(typ, Type::Menu);
|
|
|
|
assert_eq!(host, "gopher.floodgap.org");
|
|
|
|
assert_eq!(port, "70");
|
|
|
|
assert_eq!(sel, "/");
|
|
|
|
|
|
|
|
let (typ, host, port, sel) = parse_url(urls[3]);
|
|
|
|
assert_eq!(typ, Type::Text);
|
|
|
|
assert_eq!(host, "gopher.floodgap.com");
|
|
|
|
assert_eq!(port, "70");
|
|
|
|
assert_eq!(sel, "/gopher/relevance.txt");
|
|
|
|
|
|
|
|
let (typ, host, port, sel) = parse_url(urls[4]);
|
|
|
|
assert_eq!(typ, Type::Search);
|
|
|
|
assert_eq!(host, "gopherpedia.com");
|
|
|
|
assert_eq!(port, "70");
|
|
|
|
assert_eq!(sel, "/lookup?Gopher");
|
|
|
|
}
|
|
|
|
}
|