From 37a9a1347c74b67f5885fa30f42df7840a9984a1 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Mon, 27 Jul 2020 16:05:22 -0400 Subject: [PATCH 1/4] use a better theme by default --- majc/src/main.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/majc/src/main.rs b/majc/src/main.rs index 48167a7..7d4a42a 100644 --- a/majc/src/main.rs +++ b/majc/src/main.rs @@ -1,7 +1,7 @@ use cursive::{ event::Key, menu::MenuTree, - theme::{Effect, Style, Color, PaletteColor, Theme}, + theme::{BaseColor, BorderStyle, Color, Effect, PaletteColor, Style, Theme}, traits::*, utils::markup::StyledString, views::{Dialog, EditView, Panel, ResizedView, TextView}, @@ -38,9 +38,8 @@ fn main() { env!("CARGO_PKG_AUTHORS"), ))); }) - .leaf("Help", move |s| { - help(s); - }), + .leaf("Help", help) + .leaf("Quit", cursive::Cursive::quit), ) .add_leaf("Open", |s| open_prompt(s)); @@ -170,10 +169,9 @@ fn render_gemini(body: &str) -> StyledString { Text(line) => styled.append(StyledString::plain(line)), Link { to, name } => match name { None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), - Some(name) => styled.append(StyledString::styled( - format!("{}: {}", to, name), - Style::from(Effect::Underline), - )), + Some(name) => { + styled.append(StyledString::styled(name, Style::from(Effect::Underline))) + } }, Preformatted(data) => styled.append(StyledString::plain(data)), Heading { level: _, body } => { @@ -193,6 +191,12 @@ fn custom_theme_from_cursive(siv: &Cursive) -> Theme { let mut theme = siv.current_theme().clone(); theme.palette[PaletteColor::Background] = Color::TerminalDefault; + theme.palette[PaletteColor::View] = Color::TerminalDefault; + theme.palette[PaletteColor::Primary] = Color::Dark(BaseColor::White); + theme.palette[PaletteColor::TitlePrimary] = Color::Light(BaseColor::White); + + theme.shadow = false; + theme.borders = BorderStyle::Simple; theme } From c3e20b089c19b4a8ab72ad26c4ce226e2096177c Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Mon, 27 Jul 2020 17:25:59 -0400 Subject: [PATCH 2/4] majc: history support, link clicking support and more --- majc/Cargo.toml | 2 +- majc/src/commands.rs | 24 +++++ majc/src/gemini.rs | 223 +++++++++++++++++++++++++++++++++++++++++++ majc/src/help.gmi | 18 ++-- majc/src/main.rs | 194 ++++--------------------------------- majc/src/theme.rs | 19 ++++ 6 files changed, 293 insertions(+), 187 deletions(-) create mode 100644 majc/src/commands.rs create mode 100644 majc/src/gemini.rs create mode 100644 majc/src/theme.rs diff --git a/majc/Cargo.toml b/majc/Cargo.toml index e1546b6..5d19534 100644 --- a/majc/Cargo.toml +++ b/majc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "majc" -version = "0.1.0" +version = "0.2.0" authors = ["Christine Dodrill "] edition = "2018" diff --git a/majc/src/commands.rs b/majc/src/commands.rs new file mode 100644 index 0000000..a3752f2 --- /dev/null +++ b/majc/src/commands.rs @@ -0,0 +1,24 @@ +use cursive::{ + traits::*, + views::{Dialog, TextView}, + Cursive, +}; + +pub fn help(siv: &mut Cursive) { + let content = include_str!("./help.gmi"); + + siv.add_layer( + Dialog::around(TextView::new(crate::gemini::render(content)).scrollable()) + .title("Help") + .dismiss_button("Ok"), + ); +} + +pub fn about(siv: &mut Cursive) { + siv.add_layer(Dialog::info(format!( + "{} {}\n\nby {}\n\nSee https://tulpa.dev/cadey/maj for more information", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ))); +} diff --git a/majc/src/gemini.rs b/majc/src/gemini.rs new file mode 100644 index 0000000..2c527dd --- /dev/null +++ b/majc/src/gemini.rs @@ -0,0 +1,223 @@ +use cursive::{ + theme::{Effect, Style}, + traits::*, + utils::markup::StyledString, + views::{Dialog, EditView, ResizedView, SelectView, TextView}, + Cursive, +}; +use maj::{self, Response}; +use std::str; + +/// The state of the browser. +#[derive(Default, Clone, Debug)] +pub struct State { + url: Option, + history: Vec, + links: Vec<(url::Url, Option)>, +} + +pub fn history(siv: &mut Cursive) { + let st = siv.user_data::().unwrap(); + + let mut select = SelectView::new().autojump(); + + for entry in st.history.iter() { + select.add_item( + format!("{}{}", entry.host_str().unwrap(), entry.path()), + entry.to_string(), + ); + } + + select.set_on_submit(open); + + siv.add_layer( + Dialog::around(select.scrollable()) + .title("History") + .button("Cancel", |s| { + s.pop_layer(); + }), + ); +} + +pub fn links(siv: &mut Cursive) { + let st = siv.user_data::().unwrap(); + + let mut select = SelectView::new().autojump(); + + for link in st.links.iter() { + select.add_item( + match &link.1 { + None => link.0.to_string(), + Some(name) => format!("{}: {}", name, link.0.to_string()), + }, + link.0.to_string(), + ); + } + + select.set_on_submit(open); + + siv.add_layer( + Dialog::around(select.scrollable()) + .title("Links") + .button("Cancel", |s| { + s.pop_layer(); + }), + ); +} + +pub fn open_prompt(siv: &mut Cursive) { + siv.add_layer( + Dialog::around( + EditView::new() + .on_submit(open) + .with_name("url") + .fixed_width(50), + ) + .title("Enter a Gemini URL") + .button("Ok", |s| { + let url = s + .call_on_name("url", |view: &mut EditView| view.get_content()) + .unwrap(); + open(s, &url); + }) + .button("Cancel", |s| { + s.pop_layer(); + }), + ); +} + +pub fn open(siv: &mut Cursive, url: &str) { + siv.pop_layer(); + log::debug!("got URL: {}", url); + let mut st = siv.user_data::().unwrap(); + + let mut url: String = url.to_string(); + if !url.starts_with("gemini://") { + url = format!("gemini://{}", url); + } + + let url = url::Url::parse(url.as_str()).unwrap(); + + st.url = Some(url.clone()); + + match maj::get(url.to_string()) { + Ok(resp) => { + st.history.push(url.clone()); + show(siv, url.to_string().as_str(), resp); + } + Err(why) => { + log::error!("got response error: {:?}", why); + siv.add_layer(Dialog::info(format!("Error fetching response: {:?}", why))); + } + } +} + +pub fn show(siv: &mut Cursive, url: &str, resp: Response) { + use maj::StatusCode::*; + + match resp.status { + Success => match str::from_utf8(&resp.body) { + Ok(content) => { + let content: StyledString = if resp.meta.starts_with("text/gemini") { + populate_links(siv, content); + render(content) + } else { + StyledString::plain(content) + }; + siv.add_fullscreen_layer(ResizedView::with_full_screen( + Dialog::around(TextView::new(content).scrollable()) + .title(format!("{}: {}", url, resp.meta)), + )); + } + Err(why) => { + siv.add_layer(Dialog::info(format!( + "UTF/8 decoding error for {}: {:?}", + url, why + ))); + } + }, + + TemporaryRedirect => { + let st = siv.user_data::().unwrap(); + st.history.pop(); + open(siv, resp.meta.as_str()); + } + + PermanentRedirect => { + let st = siv.user_data::().unwrap(); + st.history.pop(); + open(siv, resp.meta.as_str()); + } + + Input => { + siv.add_layer( + Dialog::around(EditView::new().with_name("input").fixed_width(50)) + .title(resp.meta) + .button("Ok", |s| { + let inp = s + .call_on_name("input", |view: &mut EditView| view.get_content()) + .unwrap(); + let url = { + let st = s.user_data::().unwrap(); + let url = st.url.as_ref().unwrap(); + let mut u = url.clone(); + u.set_query(Some(&inp)); + &u.to_string() + }; + open(s, url); + }), + ); + } + + _ => { + siv.add_layer(Dialog::info(format!("{:?}: {}", resp.status, resp.meta))); + return; + } + } +} + +pub fn populate_links(siv: &mut Cursive, body: &str) { + use maj::gemini::Node::*; + let doc = maj::gemini::parse(body); + let st = siv.user_data::().unwrap(); + st.links.clear(); + let u = st.url.as_ref().unwrap(); + + for node in doc { + match node { + Link { to, name } => st.links.push((u.join(&to).unwrap(), name)), + _ => {} + } + } +} + +pub fn render(body: &str) -> StyledString { + let doc = maj::gemini::parse(body); + let mut styled = StyledString::new(); + + use maj::gemini::Node::*; + + for node in doc { + match node { + Text(line) => styled.append(StyledString::plain(line)), + Link { to, name } => { + match name { + None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), + Some(name) => { + styled.append(StyledString::styled(name, Style::from(Effect::Underline))) + } + } + } + Preformatted(data) => styled.append(StyledString::plain(data)), + Heading { level, body } => styled.append(StyledString::styled( + format!("{} {}", "#".repeat(level as usize), body), + Style::from(Effect::Bold), + )), + ListItem(item) => styled.append(StyledString::plain(format!("* {}", item))), + Quote(quote) => styled.append(StyledString::plain(format!("> {}", quote))), + } + styled.append(StyledString::plain("\n")); + } + + styled +} diff --git a/majc/src/help.gmi b/majc/src/help.gmi index 344cae7..80c3f74 100644 --- a/majc/src/help.gmi +++ b/majc/src/help.gmi @@ -1,26 +1,24 @@ -# majc ``` - __ - _____ _____ |__| ____ - / \ \__ \ | |_/ ___\ -| Y Y \ / __ \_ | |\ \___ -|__|_| /(____ //\__| | \___ > - \/ \/ \______| \/ + __ + _____ _____ |__| ____ + / \ \__ \ | |_/ ___\ + | Y Y \ / __ \_ | |\ \___ + |__|_| /(____ //\__| | \___ > + \/ \/ \______| \/ ``` A curses client for Gemini! -=> gemini://gemini.circumlunar.space/ Gemini homepage - ## Homepage The main homepage for majc is on tulpa.dev: - => https://tulpa.dev/cadey/maj ## Important Keys : opens the menubar c: closes the active window o: prompts to open a URL +h: shows history +l: shows active links in the page q: quits majc ?: shows this screen ~: toggles the debug logging pane diff --git a/majc/src/main.rs b/majc/src/main.rs index 7d4a42a..ded7bce 100644 --- a/majc/src/main.rs +++ b/majc/src/main.rs @@ -1,202 +1,44 @@ -use cursive::{ - event::Key, - menu::MenuTree, - theme::{BaseColor, BorderStyle, Color, Effect, PaletteColor, Style, Theme}, - traits::*, - utils::markup::StyledString, - views::{Dialog, EditView, Panel, ResizedView, TextView}, - Cursive, -}; -use maj::{self, Response, StatusCode}; -use std::str; +use cursive::{event::Key, menu::MenuTree}; + +pub(crate) mod commands; +pub(crate) mod gemini; +pub(crate) mod theme; fn main() { cursive::logger::init(); let mut siv = cursive::default(); - let theme = custom_theme_from_cursive(&siv); - siv.set_theme(theme); + theme::apply(&mut siv); + + siv.set_user_data(gemini::State::default()); siv.add_global_callback('c', |s| { s.pop_layer(); }); siv.add_global_callback('q', cursive::Cursive::quit); siv.add_global_callback('~', cursive::Cursive::toggle_debug_console); - siv.add_global_callback('o', open_prompt); - siv.add_global_callback('?', help); + siv.add_global_callback('h', gemini::history); + siv.add_global_callback('l', gemini::links); + siv.add_global_callback('o', gemini::open_prompt); + siv.add_global_callback('?', commands::help); siv.menubar() .add_subtree( "majc", MenuTree::new() - .leaf("About", move |s| { - s.add_layer(Dialog::info(format!( - "{} {}\n\nby {}\n\nSee https://tulpa.dev/cadey/maj for more information", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION"), - env!("CARGO_PKG_AUTHORS"), - ))); - }) - .leaf("Help", help) + .leaf("About", commands::about) + .leaf("Help", commands::help) .leaf("Quit", cursive::Cursive::quit), ) - .add_leaf("Open", |s| open_prompt(s)); + .add_leaf("Open", gemini::open_prompt) + .add_leaf("Links", gemini::links) + .add_leaf("History", gemini::history); siv.set_autohide_menu(false); siv.add_global_callback(Key::Esc, |s| s.select_menubar()); - help(&mut siv); + commands::help(&mut siv); siv.run(); } - -fn help(siv: &mut Cursive) { - let content = include_str!("./help.gmi"); - - siv.add_layer( - Dialog::around(Panel::new( - TextView::new(render_gemini(content)).scrollable(), - )) - .title("Help") - .dismiss_button("Ok"), - ); -} - -fn open_prompt(siv: &mut Cursive) { - siv.add_layer( - Dialog::around( - EditView::new() - .on_submit(open) - .with_name("url") - .fixed_width(50), - ) - .title("Enter a Gemini URL") - .button("Ok", |s| { - let url = s - .call_on_name("url", |view: &mut EditView| view.get_content()) - .unwrap(); - open(s, &url); - }) - .button("Cancel", |s| { - s.pop_layer(); - }), - ); -} - -fn open(siv: &mut Cursive, url: &str) { - siv.pop_layer(); - log::debug!("got URL: {}", url); - - match maj::get(url.to_string()) { - Ok(resp) => { - show(siv, url, resp); - } - Err(why) => { - log::error!("got response error: {:?}", why); - siv.add_layer(Dialog::info(format!("Error fetching response: {:?}", why))); - } - } -} - -fn show(siv: &mut Cursive, url: &str, resp: Response) { - use StatusCode::*; - - match resp.status { - Success => match str::from_utf8(&resp.body) { - Ok(content) => { - let content: StyledString = if resp.meta.starts_with("text/gemini") { - render_gemini(content) - } else { - StyledString::plain(content) - }; - siv.add_fullscreen_layer(ResizedView::with_full_screen( - Dialog::around(TextView::new(content).scrollable()) - .title(format!("{}: {}", url, resp.meta)), - )); - } - Err(why) => { - siv.add_layer(Dialog::info(format!( - "UTF/8 decoding error for {}: {:?}", - url, why - ))); - } - }, - - TemporaryRedirect => { - open(siv, resp.meta.as_str()); - } - - PermanentRedirect => { - open(siv, resp.meta.as_str()); - } - - Input => { - siv.add_layer(Dialog::info("needs input support")); - return; - // siv.add_layer( - // Dialog::around(EditView::new().with_name("input").fixed_width(50)) - // .title(resp.meta) - // .button("Ok", |s| { - // let inp = s - // .call_on_name("input", |view: &mut EditView| view.get_content()) - // .unwrap(); - // let url = { - // let mut u = url::Url::parse(url).unwrap(); - // u.set_query(Some(&inp)); - // u.as_str() - // }; - // open(s, url); - // }), - // ); - } - - _ => { - siv.add_layer(Dialog::info(format!("{:?}: {}", resp.status, resp.meta))); - return; - } - } -} - -fn render_gemini(body: &str) -> StyledString { - let doc = maj::gemini::parse(body); - let mut styled = StyledString::new(); - - use maj::gemini::Node::*; - - for node in doc { - match node { - Text(line) => styled.append(StyledString::plain(line)), - Link { to, name } => match name { - None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), - Some(name) => { - styled.append(StyledString::styled(name, Style::from(Effect::Underline))) - } - }, - Preformatted(data) => styled.append(StyledString::plain(data)), - Heading { level: _, body } => { - styled.append(StyledString::styled(body, Style::from(Effect::Bold))) - } - ListItem(item) => styled.append(StyledString::plain(format!("* {}", item))), - Quote(quote) => styled.append(StyledString::plain(format!("> {}", quote))), - } - styled.append(StyledString::plain("\n")); - } - - styled -} - -fn custom_theme_from_cursive(siv: &Cursive) -> Theme { - // We'll return the current theme with a small modification. - let mut theme = siv.current_theme().clone(); - - theme.palette[PaletteColor::Background] = Color::TerminalDefault; - theme.palette[PaletteColor::View] = Color::TerminalDefault; - theme.palette[PaletteColor::Primary] = Color::Dark(BaseColor::White); - theme.palette[PaletteColor::TitlePrimary] = Color::Light(BaseColor::White); - - theme.shadow = false; - theme.borders = BorderStyle::Simple; - - theme -} diff --git a/majc/src/theme.rs b/majc/src/theme.rs new file mode 100644 index 0000000..762bd26 --- /dev/null +++ b/majc/src/theme.rs @@ -0,0 +1,19 @@ +use cursive::{ + theme::{BaseColor, BorderStyle, Color, PaletteColor}, + Cursive, +}; + +pub fn apply(siv: &mut Cursive) { + // We'll return the current theme with a small modification. + let mut theme = siv.current_theme().clone(); + + theme.palette[PaletteColor::Background] = Color::TerminalDefault; + theme.palette[PaletteColor::View] = Color::TerminalDefault; + theme.palette[PaletteColor::Primary] = Color::Dark(BaseColor::White); + theme.palette[PaletteColor::TitlePrimary] = Color::Light(BaseColor::White); + + theme.shadow = false; + theme.borders = BorderStyle::Simple; + + siv.set_theme(theme); +} From 62350daba5641fb4a7b3675e5b0e0c6de912aad0 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Mon, 27 Jul 2020 17:31:38 -0400 Subject: [PATCH 3/4] majc: implement changelog screen --- majc/src/changelog.gmi | 8 ++++++++ majc/src/commands.rs | 10 ++++++++++ majc/src/main.rs | 1 + 3 files changed, 19 insertions(+) create mode 100644 majc/src/changelog.gmi diff --git a/majc/src/changelog.gmi b/majc/src/changelog.gmi new file mode 100644 index 0000000..0315c03 --- /dev/null +++ b/majc/src/changelog.gmi @@ -0,0 +1,8 @@ +## 0.2.0 + +* History works (h/l) +* Added changelog screen + +## 0.1.0 + +* Base functionality implemented diff --git a/majc/src/commands.rs b/majc/src/commands.rs index a3752f2..9366fe6 100644 --- a/majc/src/commands.rs +++ b/majc/src/commands.rs @@ -14,6 +14,16 @@ pub fn help(siv: &mut Cursive) { ); } +pub fn changelog(siv: &mut Cursive) { + let content = include_str!("./changelog.gmi"); + + siv.add_layer( + Dialog::around(TextView::new(crate::gemini::render(content)).scrollable()) + .title("Changelog") + .dismiss_button("Ok"), + ); +} + pub fn about(siv: &mut Cursive) { siv.add_layer(Dialog::info(format!( "{} {}\n\nby {}\n\nSee https://tulpa.dev/cadey/maj for more information", diff --git a/majc/src/main.rs b/majc/src/main.rs index ded7bce..b3bd9d2 100644 --- a/majc/src/main.rs +++ b/majc/src/main.rs @@ -28,6 +28,7 @@ fn main() { "majc", MenuTree::new() .leaf("About", commands::about) + .leaf("Changelog", commands::changelog) .leaf("Help", commands::help) .leaf("Quit", cursive::Cursive::quit), ) From 30afd1126cd6b88ab43728a21c07d2788ca8cf84 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Mon, 27 Jul 2020 18:14:45 -0400 Subject: [PATCH 4/4] use smol --- CHANGELOG.md | 12 ++++++ Cargo.toml | 5 ++- VERSION | 2 +- majc/Cargo.toml | 5 +++ majc/src/gemini.rs | 29 +++++++++----- majc/src/main.rs | 1 + majc/src/tls.rs | 25 ++++++++++++ src/client.rs | 97 ++++++++++++++++++++++++++++------------------ src/response.rs | 1 + 9 files changed, 128 insertions(+), 49 deletions(-) create mode 100644 majc/src/tls.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9b1b6..2cda643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.3.0 + +### maj + +- `maj::get` unfortunately had to become async in order for fetching `gus.guru` to work + +### majc + +- keep track of loaded pages as history +- `l` to view/navigate to links +- use [smol](https://github.com/stjepang/smol) for doing the async operations synchronously + ## 0.2.0 ### maj diff --git a/Cargo.toml b/Cargo.toml index 1444137..98025f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maj" -version = "0.2.0" +version = "0.3.0" authors = ["Christine Dodrill "] edition = "2018" license = "0BSD" @@ -18,6 +18,7 @@ rustls = { version = "0.18", optional = true, features = ["dangerous_configurati webpki = { version = "0.21.0", optional = true } webpki-roots = { version = "0.20", optional = true } tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true } +tokio-io-timeout = "0.4" log = "0.4" url = "2" thiserror = "1" @@ -42,7 +43,7 @@ optional = true [features] default = ["client", "server"] -client = ["rustls", "webpki", "webpki-roots"] +client = ["rustls", "webpki", "webpki-roots", "tokio", "tokio-rustls"] server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"] [workspace] diff --git a/VERSION b/VERSION index 0ea3a94..0d91a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.3.0 diff --git a/majc/Cargo.toml b/majc/Cargo.toml index 5d19534..ae85c61 100644 --- a/majc/Cargo.toml +++ b/majc/Cargo.toml @@ -10,5 +10,10 @@ edition = "2018" cursive = "0.15" log = "0.4" url = "2" +webpki = "0.21.0" +tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] } +rustls = { version = "0.18", features = ["dangerous_configuration"] } +smol = { version = "0.3", features = ["tokio02"] } maj = { path = ".." } + diff --git a/majc/src/gemini.rs b/majc/src/gemini.rs index 2c527dd..7ccb3ac 100644 --- a/majc/src/gemini.rs +++ b/majc/src/gemini.rs @@ -7,13 +7,26 @@ use cursive::{ }; use maj::{self, Response}; use std::str; +use tokio_rustls::rustls::ClientConfig; /// The state of the browser. -#[derive(Default, Clone, Debug)] +#[derive(Clone)] pub struct State { url: Option, history: Vec, links: Vec<(url::Url, Option)>, + cfg: ClientConfig, +} + +impl Default for State { + fn default() -> Self { + State { + url: None, + history: vec![], + links: vec![], + cfg: crate::tls::config(), + } + } } pub fn history(siv: &mut Cursive) { @@ -100,7 +113,7 @@ pub fn open(siv: &mut Cursive, url: &str) { st.url = Some(url.clone()); - match maj::get(url.to_string()) { + match smol::run(maj::get(url.to_string(), st.cfg.clone())) { Ok(resp) => { st.history.push(url.clone()); show(siv, url.to_string().as_str(), resp); @@ -200,14 +213,12 @@ pub fn render(body: &str) -> StyledString { for node in doc { match node { Text(line) => styled.append(StyledString::plain(line)), - Link { to, name } => { - match name { - None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), - Some(name) => { - styled.append(StyledString::styled(name, Style::from(Effect::Underline))) - } + Link { to, name } => match name { + None => styled.append(StyledString::styled(to, Style::from(Effect::Underline))), + Some(name) => { + styled.append(StyledString::styled(name, Style::from(Effect::Underline))) } - } + }, Preformatted(data) => styled.append(StyledString::plain(data)), Heading { level, body } => styled.append(StyledString::styled( format!("{} {}", "#".repeat(level as usize), body), diff --git a/majc/src/main.rs b/majc/src/main.rs index b3bd9d2..53ae9a3 100644 --- a/majc/src/main.rs +++ b/majc/src/main.rs @@ -3,6 +3,7 @@ use cursive::{event::Key, menu::MenuTree}; pub(crate) mod commands; pub(crate) mod gemini; pub(crate) mod theme; +pub(crate) mod tls; fn main() { cursive::logger::init(); diff --git a/majc/src/tls.rs b/majc/src/tls.rs new file mode 100644 index 0000000..7a6e853 --- /dev/null +++ b/majc/src/tls.rs @@ -0,0 +1,25 @@ +use tokio_rustls::rustls; +use std::sync::Arc; + +pub fn config() -> rustls::ClientConfig { + let mut config = rustls::ClientConfig::new(); + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + + config +} + +struct NoCertificateVerification {} + +impl rustls::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _roots: &rustls::RootCertStore, + _presented_certs: &[rustls::Certificate], + _dns_name: webpki::DNSNameRef<'_>, + _ocsp: &[u8], + ) -> Result { + Ok(rustls::ServerCertVerified::assertion()) + } +} diff --git a/src/client.rs b/src/client.rs index 107a764..e1db0a6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,27 +1,15 @@ use crate::Response; -use rustls::{ClientConfig, ClientSession, Stream, TLSError}; -use std::{io::prelude::*, net::TcpStream, sync::Arc}; +use std::{io::Cursor, sync::Arc}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, +}; +use tokio_rustls::{ + rustls::{TLSError}, + TlsConnector, +}; use url::Url; -fn config() -> ClientConfig { - let mut config = ClientConfig::new(); - config.dangerous().set_certificate_verifier(Arc::new(NoCertificateVerification{})); - - config -} - -struct NoCertificateVerification {} - -impl rustls::ServerCertVerifier for NoCertificateVerification { - fn verify_server_cert(&self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: webpki::DNSNameRef<'_>, - _ocsp: &[u8]) -> Result { - Ok(rustls::ServerCertVerified::assertion()) - } -} - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("TLS error: {0:?}")] @@ -43,7 +31,7 @@ pub enum Error { InvalidScheme(String), } -pub fn get(u: T) -> Result +pub async fn get(u: T, cfg: tokio_rustls::rustls::ClientConfig) -> Result where T: Into, { @@ -53,38 +41,73 @@ where ur.set_port(Some(1965)).unwrap(); } - if ur.scheme() == "" { - let _ = ur.set_scheme("gemini"); - } - if ur.scheme() != "gemini" { return Err(Error::InvalidScheme(ur.scheme().to_string())); } - let cfg = Arc::new(config()); + let cfg = Arc::new(cfg); let host = ur.host_str().unwrap(); - let mut sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap()))?; let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?; - let mut client = ClientSession::new(&cfg, name_ref); - let mut tls = Stream::new(&mut client, &mut sock); + let config = TlsConnector::from(cfg); + + let sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap())).await?; + let mut tls = config.connect(name_ref, sock).await?; let req = format!("{}\r\n", u); log::trace!("writing request {:?}", req); - tls.write(req.as_bytes())?; - Ok(Response::parse(&mut tls)?) + tls.write(req.as_bytes()).await?; + let mut buf: Vec = vec![]; + tls.read_to_end(&mut buf).await?; + Ok(Response::parse(&mut Cursor::new(buf))?) } #[cfg(test)] mod tests { + use tokio_rustls::rustls; + + fn config() -> rustls::ClientConfig { + let mut config = rustls::ClientConfig::new(); + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + + config + } + + struct NoCertificateVerification {} + + impl rustls::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _roots: &rustls::RootCertStore, + _presented_certs: &[rustls::Certificate], + _dns_name: webpki::DNSNameRef<'_>, + _ocsp: &[u8], + ) -> Result { + Ok(rustls::ServerCertVerified::assertion()) + } + } + use super::*; - use crate::*; - #[test] - fn gemini_homepage() -> Result<(), Error> { + #[tokio::test] + async fn gemini_homepage() -> Result<(), Error> { let _ = pretty_env_logger::try_init(); - let resp = get("gemini://gemini.circumlunar.space/".to_string())?; + let resp = get("gemini://gemini.circumlunar.space/".to_string(), config()).await?; - assert_eq!(resp.status, StatusCode::Success); + assert_eq!(resp.status, crate::StatusCode::Success); + assert_eq!(resp.meta, "text/gemini"); + assert_ne!(resp.body.len(), 0); + + Ok(()) + } + + #[tokio::test] + async fn gus() -> Result<(), Error> { + let _ = pretty_env_logger::try_init(); + let resp = get("gemini://gus.guru/".to_string(), config()).await?; + + assert_eq!(resp.status, crate::StatusCode::Success); assert_eq!(resp.meta, "text/gemini"); assert_ne!(resp.body.len(), 0); diff --git a/src/response.rs b/src/response.rs index 1858060..46b0cf2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -58,6 +58,7 @@ impl Response { result.body = data; return Ok(result); } + panic!("got here: {}, {:?}", n, state); } }