-- Leo's gemini proxy
-- Connecting to typed-hole.org:1965...
-- Connected
-- Sending request
-- Meta line: 20 text/gemini
commit 9eead3ea8695f4ae307d203de2110816a95a66ca
Author: Julien Blanchard <julien@sideburns.eu>
Date: Sat Dec 28 17:39:57 2019 +0100
Add traits to remove some duplication
diff --git a/src/absolute_url.rs b/src/absolute_url.rs
new file mode 100644
index 0000000..36cc921
--- /dev/null
+++ b/src/absolute_url.rs
@@ -0,0 +1,129 @@
+use crate::Gemini;
+use crate::Gopher;
+use crate::Protocol;
+use url::Url;
+
+pub trait AbsoluteUrl {
+ fn to_absolute_url(&self) -> Result<url::Url, url::ParseError>;
+}
+
+impl AbsoluteUrl for Gemini {
+ fn to_absolute_url(&self) -> Result<url::Url, url::ParseError> {
+ let url = self.get_source_str();
+ // Creates an absolute link if needed
+ match crate::history::get_current_host() {
+ Some(host) => {
+ if url.starts_with("gemini://") {
+ Url::parse(&url)
+ } else if url.starts_with("//") {
+ Url::parse(&format!("gemini:{}", url))
+ } else if url.starts_with('/') {
+ Url::parse(&format!("gemini://{}{}", host, url))
+ } else {
+ let current_host_path = crate::history::get_current_url().unwrap();
+ Url::parse(&format!("{}{}", current_host_path, url))
+ }
+ }
+ None => {
+ if url.starts_with("gemini://") {
+ Url::parse(&url)
+ } else if url.starts_with("//") {
+ Url::parse(&format!("gemini:{}", url))
+ } else {
+ Url::parse(&format!("gemini://{}", url))
+ }
+ }
+ }
+ }
+}
+
+impl AbsoluteUrl for Gopher {
+ fn to_absolute_url(&self) -> Result<url::Url, url::ParseError> {
+ let url = self.get_source_str();
+ // Creates an absolute link if needed
+ match crate::history::get_current_host() {
+ Some(host) => {
+ if url.starts_with("gopher://") {
+ Url::parse(&url)
+ } else if url.starts_with("//") {
+ Url::parse(&format!("gopher:{}", url))
+ } else if url.starts_with('/') {
+ Url::parse(&format!("gopher://{}{}", host, url))
+ } else {
+ let current_host_path = crate::history::get_current_url().unwrap();
+ Url::parse(&format!("{}{}", current_host_path, url))
+ }
+ }
+ None => {
+ if url.starts_with("gopher://") {
+ Url::parse(&url)
+ } else if url.starts_with("//") {
+ Url::parse(&format!("gopher:{}", url))
+ } else {
+ Url::parse(&format!("gopher://{}", url))
+ }
+ }
+ }
+ }
+}
+
+#[test]
+fn test_make_absolute_full_url() {
+ crate::history::append("gemini://typed-hole.org");
+ let url = "gemini://typed-hole.org/foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_full_url_no_protocol() {
+ crate::history::append("gemini://typed-hole.org");
+ let url = "//typed-hole.org/foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_slash_path() {
+ crate::history::append("gemini://typed-hole.org");
+ let url = "/foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_just_path() {
+ crate::history::append("gemini://typed-hole.org");
+ let url = "foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_full_url_no_current_host() {
+ let url = "gemini://typed-hole.org/foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_full_url_no_protocol_no_current_host() {
+ let url = "//typed-hole.org/foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_slash_path_no_current_host() {
+ let url = "/foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
+#[test]
+fn test_make_absolute_just_path_no_current_host() {
+ let url = "foo";
+ let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
+ let absolute_url = Gemini { source: String::from(url) }.to_absolute_url().unwrap();
+ assert_eq!(expected_url, absolute_url);
+}
diff --git a/src/client.rs b/src/client.rs
new file mode 100644
index 0000000..8d5ed0d
--- /dev/null
+++ b/src/client.rs
@@ -0,0 +1,103 @@
+use std::io::{Read, Write};
+use tempfile::NamedTempFile;
+
+use native_tls::TlsConnector;
+use std::net::{TcpStream, ToSocketAddrs};
+use std::time::Duration;
+
+use crate::Gemini;
+use crate::Gopher;
+use crate::Protocol;
+
+
+pub trait Client {
+ fn get_data(&self) -> Result<(Option<Vec<u8>>, Vec<u8>), String>;
+}
+
+impl Client for Gemini {
+ fn get_data(&self) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
+ let url = self.get_source_url();
+ let host = url.host_str().unwrap();
+ let urlf = format!("{}:1965", host);
+
+ let mut builder = TlsConnector::builder();
+ builder.danger_accept_invalid_hostnames(true);
+ builder.danger_accept_invalid_certs(true);
+ let connector = builder.build().unwrap();
+
+ match urlf.to_socket_addrs() {
+ Ok(mut addrs_iter) => match addrs_iter.next() {
+ Some(socket_addr) => {
+ let stream = TcpStream::connect_timeout(&socket_addr, Duration::new(5, 0));
+
+ match stream {
+ Ok(stream) => {
+ let mstream = connector.connect(&host, stream);
+
+ match mstream {
+ Ok(mut stream) => {
+ let url = format!("{}\r\n", url);
+ stream.write_all(url.as_bytes()).unwrap();
+ let mut res = vec![];
+ stream.read_to_end(&mut res).unwrap();
+
+ let clrf_idx = find_clrf(&res);
+ let content = res.split_off(clrf_idx.unwrap() + 2);
+
+ Ok((Some(res), content))
+ }
+ Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)),
+ }
+ }
+ Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)),
+ }
+ }
+ None => Err(format!("Could not connect to {}", urlf)),
+ },
+ Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)),
+ }
+ }
+}
+
+impl Client for Gopher {
+ fn get_data(&self) -> Result<(Option<Vec<u8>>, Vec<u8>), String> {
+ let url = self.get_source_url();
+ let host = url.host_str().unwrap();
+ let urlf = format!("{}:70", host);
+ println!("{:?}", url.path());
+
+ match TcpStream::connect(&urlf) {
+ Ok(mut stream) => {
+ let mut url = format!("{}\r\n", url.path());
+ let url = if url.starts_with("/0/") || url.starts_with("/1/") {
+ url.split_off(2)
+ } else {
+ url
+ };
+ stream.write_all(url.as_bytes()).unwrap();
+ let mut res = vec![];
+ stream.read_to_end(&mut res).unwrap();
+
+ Ok((None, res))
+ }
+ Err(e) => Err(format!("Could not connect to {}\n{}", urlf, e)),
+ }
+ }
+}
+
+pub fn download(content: Vec<u8>) {
+ let path = write_tmp_file(content);
+ open::that(path).unwrap();
+}
+
+fn write_tmp_file(content: Vec<u8>) -> std::path::PathBuf {
+ let mut tmp_file = NamedTempFile::new().unwrap();
+ tmp_file.write_all(&content).unwrap();
+ let (_file, path) = tmp_file.keep().unwrap();
+ path
+}
+
+fn find_clrf(data: &[u8]) -> Option<usize> {
+ let clrf = b"\r\n";
+ data.windows(clrf.len()).position(|window| window == clrf)
+}
diff --git a/src/gemini/absolute.rs b/src/gemini/absolute.rs
deleted file mode 100644
index 5f3cdac..0000000
--- a/src/gemini/absolute.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-use url::Url;
-
-pub fn make(url: &str) -> Result<url::Url, url::ParseError> {
- // Creates an absolute link if needed
- match crate::history::get_current_host() {
- Some(host) => {
- if url.starts_with("gemini://") {
- Url::parse(url)
- } else if url.starts_with("//") {
- Url::parse(&format!("gemini:{}", url))
- } else if url.starts_with('/') {
- Url::parse(&format!("gemini://{}{}", host, url))
- } else {
- let current_host_path = crate::history::get_current_url().unwrap();
- Url::parse(&format!("{}{}", current_host_path, url))
- }
- }
- None => {
- if url.starts_with("gemini://") {
- Url::parse(url)
- } else if url.starts_with("//") {
- Url::parse(&format!("gemini:{}", url))
- } else {
- Url::parse(&format!("gemini://{}", url))
- }
- }
- }
-}
-
-#[test]
-fn test_make_absolute_full_url() {
- crate::history::append("gemini://typed-hole.org");
- let url = "gemini://typed-hole.org/foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_full_url_no_protocol() {
- crate::history::append("gemini://typed-hole.org");
- let url = "//typed-hole.org/foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_slash_path() {
- crate::history::append("gemini://typed-hole.org");
- let url = "/foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_just_path() {
- crate::history::append("gemini://typed-hole.org");
- let url = "foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_full_url_no_current_host() {
- let url = "gemini://typed-hole.org/foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_full_url_no_protocol_no_current_host() {
- let url = "//typed-hole.org/foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_slash_path_no_current_host() {
- let url = "/foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_just_path_no_current_host() {
- let url = "foo";
- let expected_url = Url::parse("gemini://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
diff --git a/src/gemini/mod.rs b/src/gemini/mod.rs
index 9516af0..9d35acf 100644
--- a/src/gemini/mod.rs
+++ b/src/gemini/mod.rs
@@ -1,4 +1,5 @@
-pub mod absolute;
pub mod client;
pub mod link;
pub mod parser;
+
+struct GeminiUrl(&'static str);
diff --git a/src/gopher/absolute.rs b/src/gopher/absolute.rs
deleted file mode 100644
index 14003fc..0000000
--- a/src/gopher/absolute.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-use url::Url;
-
-pub fn make(url: &str) -> Result<url::Url, url::ParseError> {
- // Creates an absolute link if needed
- match crate::history::get_current_host() {
- Some(host) => {
- if url.starts_with("gopher://") {
- Url::parse(url)
- } else if url.starts_with("//") {
- Url::parse(&format!("gopher:{}", url))
- } else if url.starts_with('/') {
- Url::parse(&format!("gopher://{}{}", host, url))
- } else {
- let current_host_path = crate::history::get_current_url().unwrap();
- Url::parse(&format!("{}{}", current_host_path, url))
- }
- }
- None => {
- if url.starts_with("gopher://") {
- Url::parse(url)
- } else if url.starts_with("//") {
- Url::parse(&format!("gopher:{}", url))
- } else {
- Url::parse(&format!("gopher://{}", url))
- }
- }
- }
-}
-
-#[test]
-fn test_make_absolute_full_url() {
- crate::history::append("gopher://typed-hole.org");
- let url = "gopher://typed-hole.org/foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_full_url_no_protocol() {
- crate::history::append("gopher://typed-hole.org");
- let url = "//typed-hole.org/foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_slash_path() {
- crate::history::append("gopher://typed-hole.org");
- let url = "/foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_just_path() {
- crate::history::append("gopher://typed-hole.org");
- let url = "foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_full_url_no_current_host() {
- let url = "gopher://typed-hole.org/foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_full_url_no_protocol_no_current_host() {
- let url = "//typed-hole.org/foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_slash_path_no_current_host() {
- let url = "/foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
-#[test]
-fn test_make_absolute_just_path_no_current_host() {
- let url = "foo";
- let expected_url = Url::parse("gopher://typed-hole.org/foo").unwrap();
- let absolute_url = make(&url).unwrap();
- assert_eq!(expected_url, absolute_url);
-}
diff --git a/src/gopher/mod.rs b/src/gopher/mod.rs
index 9516af0..b6c0233 100644
--- a/src/gopher/mod.rs
+++ b/src/gopher/mod.rs
@@ -1,4 +1,3 @@
-pub mod absolute;
pub mod client;
pub mod link;
pub mod parser;
diff --git a/src/main.rs b/src/main.rs
index 196f821..fb217d1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,16 +15,23 @@ use url::{Position, Url};
mod gui;
use gui::Gui;
+mod absolute_url;
+use absolute_url::AbsoluteUrl;
mod bookmarks;
+mod client;
+use client::Client;
mod gemini;
mod gopher;
mod history;
use gemini::link::Link as GeminiLink;
use gopher::link::Link as GopherLink;
+mod protocols;
+use protocols::{Protocol, Scheme, Gopher, Gemini};
mod status;
use status::Status;
mod tags;
+
fn main() {
// Start up the GTK3 subsystem.
gtk::init().expect("Unable to start GTK3. Error");
@@ -66,13 +73,14 @@ fn main() {
let url_bar = gui.url_bar();
url_bar.connect_activate(move |b| {
let url = b.get_text().expect("get_text failed").to_string();
- // let full_url = if url.starts_with("gemini://") {
- // url
- // } else {
- // format!("gemini://{}", url)
- // };
- visit_url(&gui_clone, url);
+ if url.starts_with("gemini://") {
+ visit_url(&gui_clone, Gemini { source: url })
+ } else if url.starts_with("gopher://") {
+ visit_url(&gui_clone, Gopher { source: url })
+ } else {
+ visit_url(&gui_clone, Gemini { source: format!("gemini://{}", url) })
+ };
});
}
@@ -86,7 +94,11 @@ fn main() {
fn go_back(gui: &Arc<Gui>) {
let previous = history::get_previous_url();
if let Some(url) = previous {
- visit_url(gui, url.to_string())
+ match url.scheme() {
+ "gemini" => visit_url(gui, Gemini { source: url.to_string() }),
+ "gopher" => visit_url(gui, Gopher { source: url.to_string() }),
+ _ => ()
+ }
}
}
@@ -117,20 +129,20 @@ fn show_bookmarks(gui: &Arc<Gui>) {
content_view.show_all();
}
-fn visit_url(gui: &Arc<Gui>, url: String) {
+fn visit_url<T: AbsoluteUrl + Client + Protocol>(gui: &Arc<Gui>, url: T) {
{
- if url == "gemini://::bookmarks" {
+ if url.get_source_str() == "gemini://::bookmarks" {
show_bookmarks(&gui);
return;
}
let content_view = gui.content_view();
- if url.starts_with("gemini") {
- let absolute_url = gemini::absolute::make(url.as_str());
+ if url.get_scheme() == Scheme::Gemini {
+ let absolute_url = url.to_absolute_url();
match absolute_url {
- Ok(url) => match gemini::client::get(&url) {
+ Ok(url2) => match url.get_data() {
Ok((meta, new_content)) => {
let meta_str = String::from_utf8_lossy(&meta.unwrap()).to_string();
if let Ok(status) = Status::from_str(&meta_str) {
@@ -138,8 +150,8 @@ fn visit_url(gui: &Arc<Gui>, url: String) {
Status::Success(meta) => {
if meta.starts_with("text/") {
// display text files.
- history::append(url.as_str());
- update_url_field(&gui, url.as_str());
+ history::append(url2.as_str());
+ update_url_field(&gui, url2.as_str());
let content_str =
String::from_utf8_lossy(&new_content).to_string();
@@ -158,7 +170,7 @@ fn visit_url(gui: &Arc<Gui>, url: String) {
}
Status::RedirectTemporary(new_url)
| Status::RedirectPermanent(new_url) => {
- visit_url(&gui, new_url);
+ visit_url(&gui, Gemini { source: new_url });
}
Status::TransientCertificateRequired(_meta)
| Status::AuthorisedCertificatedRequired(_meta) => {
@@ -168,7 +180,7 @@ fn visit_url(gui: &Arc<Gui>, url: String) {
);
}
Status::Input(message) => {
- input_dialog(&gui, url, &message);
+ input_dialog(&gui, url2, &message);
}
_ => (),
}
@@ -183,7 +195,7 @@ fn visit_url(gui: &Arc<Gui>, url: String) {
}
}
} else {
- let absolute_url = gopher::absolute::make(url.as_str());
+ let absolute_url = url.to_absolute_url();
match absolute_url {
Ok(url) => match gopher::client::get(&url) {
Ok((_meta, new_content)) => {
@@ -326,7 +338,7 @@ fn draw_gemini_link(gui: &Arc<Gui>, link_item: String) {
insert_gemini_button(&gui, url, label);
}
Ok(GeminiLink::Relative(url, label)) => {
- let new_url = gemini::absolute::make(&url).unwrap();
+ let new_url = Gemini { source: url }.to_absolute_url().unwrap();
insert_gemini_button(&gui, new_url, label);
}
Ok(GeminiLink::Unknown(_, _)) => (),
@@ -359,7 +371,7 @@ fn draw_gopher_link(gui: &Arc<Gui>, link_item: String) {
insert_gemini_button(&gui, url, label);
}
Ok(GopherLink::Relative(url, label)) => {
- let new_url = gemini::absolute::make(&url).unwrap();
+ let new_url = Gopher { source: url }.to_absolute_url().unwrap();
insert_gemini_button(&gui, new_url, label);
}
Ok(GopherLink::Unknown(_, _)) => (),
@@ -381,7 +393,11 @@ fn insert_gemini_button(gui: &Arc<Gui>, url: Url, label: String) {
button.set_tooltip_text(Some(&url.to_string()));
button.connect_clicked(clone!(@weak gui => move |_| {
- visit_url(&gui, url.to_string());
+ match url.scheme() {
+ "gemini" => visit_url(&gui, Gemini { source: url.to_string() }),
+ "gopher" => visit_url(&gui, Gopher { source: url.to_string() }),
+ _ => ()
+ }
}));
let mut start_iter = buffer.get_end_iter();
@@ -464,7 +480,8 @@ fn input_dialog(gui: &Arc<Gui>, url: Url, message: &str) {
let response = entry.get_text().expect("get_text failed").to_string();
let cleaned: &str = &url[..Position::AfterPath];
let full_url = format!("{}?{}", cleaned.to_string(), response);
- visit_url(&gui, full_url);
+
+ visit_url(&gui, Gemini { source: full_url });
}
dialog.destroy();
diff --git a/src/protocols.rs b/src/protocols.rs
new file mode 100644
index 0000000..9b2b70c
--- /dev/null
+++ b/src/protocols.rs
@@ -0,0 +1,44 @@
+use url::Url;
+
+pub trait Protocol {
+ fn get_source_str(&self) -> &str;
+ fn get_source_url(&self) -> Url;
+ fn get_scheme(&self) -> Scheme;
+}
+
+pub struct Gemini { pub source: String }
+pub struct Gopher { pub source: String }
+
+impl Protocol for Gemini {
+ fn get_source_str(&self) -> &str {
+ &self.source
+ }
+
+ fn get_source_url(&self) -> Url {
+ Url::parse(&self.source).unwrap()
+ }
+
+ fn get_scheme(&self) -> Scheme {
+ Scheme::Gemini
+ }
+}
+
+impl Protocol for Gopher {
+ fn get_source_str(&self) -> &str {
+ &self.source
+ }
+
+ fn get_source_url(&self) -> Url {
+ Url::parse(&self.source).unwrap()
+ }
+
+ fn get_scheme(&self) -> Scheme {
+ Scheme::Gopher
+ }
+}
+
+#[derive(PartialEq)]
+pub enum Scheme {
+ Gemini,
+ Gopher
+}
---
Served by Pollux Gemini Server.
-- Response ended
-- Page fetched on Sun May 19 04:55:14 2024