-- 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