FZIABKEV74PFFRFETMT5WVL625I76UYN2LXKIKRZNQE5GDGX3GUQC
ARXVO5XAQFZR3X6MXW525Z6WXUYUHRJUGTTNCEKKXRAKQQBG5ZXAC
JWLMAS7UWFXHWBYEBW4YSLPZUF7UFQHAKN3AUF2IAJWVXW46T5PQC
3SPNKI46URBW4PD3JRMBV5YV5W56N3SFUTK25VQEF2FUZ64ZDQPAC
WGRFJRTEXOKY7PV536O62WUE32T24VQEHFQP67GEZZ67AE37ZWUAC
AOJXTWBZA2DQBFTJZX2YTGAKIB62SKTEVJ7RRIBJNDSMC75DDQVQC
DBKKKHC2ER7XAYR6HAFQ6I75M3OLDOSGCB5BTEP7X6XAZRSWKIRAC
HVFBGX2A2LBL6HSFKPNIG5KGQPVMZ5GFYJ4ZPDAM4POSRTQSKN7QC
AMTMTTJTF2XHEUC6ACP2PHP7GAFMFZRSJ47VJBKEO22X4ZGFIX7QC
OSV63NXNIRGOEJZUECHXYYYEFUAFXLN4BNIH3MKLE7SDZLHGFEWAC
ONQEIR5BV26QJSI3HY7B3MN6VWEGSZIXAIUIANJ5ZLR3QN3DFGEAC
use response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success};
use server::{Network, TcpNetwork};
use std::io::{BufReader, Read, Result as ioResult, Write};
use server::Network;
use std::{
fmt::Display,
io::{BufReader, Read, Result as ioResult, Write},
};
pub enum Error {}
impl App {
pub fn run(self) -> Result<(), Error> {
let mut cli = Cli {};
if self.test_prompt {
let overt = cli.read("tell me something");
cli.warn(format_args!("your response was: {overt}"));
let secret = cli.read_secret("tell me your secrets");
cli.warn(format_args!("your secret was: {secret}"));
Ok(())
} else {
let url = self.url.unwrap_or_else(|| {
"gemini://gemini.circumlunar.space/docs/specification.gmi".into()
});
let url: Url = url.parse().expect("Not a valid URL");
run::<TcpNetwork<danger::Naive>>(&mut cli, url);
Ok(())
}
}
}
pub struct Cli {}
impl UI for Cli {
fn show(&self, content: Media) {
match content {
Media::Gemini(g) => println!("{g:#?}"), // TODO
Media::Text(s) => println!("{s}"),
}
}
fn read(&self, prompt: impl Display) -> String {
print!("{prompt}: ");
stdout().flush().unwrap();
std::io::stdin()
.lines()
.next()
.expect("End of input")
.expect("Failed to read input")
}
fn read_secret(&self, prompt: impl Display) -> String {
let dim = ansi_term::Style::new().dimmed();
print!("{prompt}: ");
let hidden = "(input hidden)";
let len = hidden.len() as u32;
let back = ansi_control_codes::control_sequences::CUB(Some(len));
print!("{}{back}", dim.paint(hidden));
stdout().flush().unwrap();
rpassword::read_password().expect("Failed to read input")
}
fn warn<D: std::fmt::Display>(&self, warning: D) {
println!("Warning: {warning}")
}
}
}
pub trait UI {
fn show(&self, content: media::Media);
fn warn<D: Display>(&self, warning: D);
fn read(&self, prompt: impl Display) -> String;
fn read_secret(&self, prompt: impl Display) -> String;
let url = cli
.url
.unwrap_or_else(|| "gemini://gemini.circumlunar.space/docs/specification.gmi".into());
let mut url: Url = url.parse().expect("Not a valid URL");
while let Some(new_url) = run_url::<TcpNetwork<danger::Naive>>(url) {
match cli.run() {
Ok(()) => println!("done"),
Err(e) => match e {},
}
}
fn run<N: Network>(ui: &mut impl UI, mut url: Url)
where
<N as Network>::Error: std::fmt::Debug,
{
while let Some(new_url) = run_url::<N>(ui, url) {
response::Header::Input(s) => {
let Input { prompt, sensitive } = s;
println!("{prompt}");
let input = if let Some(true) = sensitive {
// Read sensitive input
rpassword::read_password()
} else {
// Read plain input
std::io::stdin().lines().next().expect("End of input")
}
.expect("Failed to read input");
let input = urlencoding::encode(&input);
url.set_query(Some(&input));
Some(url)
}
response::Header::Input(s) => handle::input(s, ui, url),
let Success { mime } = s;
let body = stream.read_string().expect("failed to read body");
//TODO remove this debug output
println!("page is encoded as {mime:?}");
println!("response body: {body}");
let media = decode_media(mime, &body);
media.display();
handle::success(s, stream, ui);
}
response::Header::Redirect(s) => {
let Redirect { url, temporary } = s;
if let Some(false) = temporary {
println!("Server has permanently moved. Redirecting to: {url}");
println!("Please update your records!");
} else {
println!("Server has temporarily moved. Redirecting to: {url}");
}
Some(url)
}
}
}
mod handle {
use crate::{
decode_media,
io::ReadExt,
response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success},
UI,
};
use std::io::Read;
use url::Url;
pub fn cert_required(s: CertRequired, ui: &mut impl UI) {
let CertRequired { message, typ } = s;
ui.warn(format_args!(
"Certificate required: {typ:?}! Details:\n{message}"
));
}
pub fn fail_perm(s: FailPerm, ui: &mut impl UI) {
let FailPerm { message, typ } = s;
ui.warn(format_args!(
"Permanent failure: {typ:?}! Details:\n{message}"
));
}
pub fn fail_temp(s: FailTemp, ui: &mut impl UI) {
let FailTemp { message, typ } = s;
ui.warn(format_args!(
"Temporary failure: {typ:?}! Please try again. Details:\n{message}"
));
}
pub fn redirect(s: Redirect, ui: &mut impl UI) -> Url {
let Redirect { url, temporary } = s;
if let Some(false) = temporary {
ui.warn(format_args!(
"Server has permanently moved. Redirecting to: {url}\nPlease update your records!"
));
} else {
ui.warn(format_args!(
"Server has temporarily moved. Redirecting to: {url}"
));
url
}
pub fn success(s: Success, mut stream: impl Read, ui: &mut impl UI) {
let Success { mime } = s;
let body = stream.read_string().expect("failed to read body");
//TODO remove this debug output
println!("page is encoded as {mime:?}");
println!("response body: {body}");
let media = decode_media(mime, &body);
ui.show(media);
}
pub fn input(s: Input, ui: &mut impl UI, mut url: Url) -> Option<Url> {
let Input { prompt, sensitive } = s;
let input = if let Some(true) = sensitive {
ui.read_secret(&prompt)
} else {
ui.read(&prompt)
};
let input = urlencoding::encode(&input);
url.set_query(Some(&input));
Some(url)
let media = match mime.type_() {
mime::TEXT => match mime.subtype().as_ref() {
"gemini" => Media::Gemini(body.into()),
_ => Media::Text(body),
},
let media = match (mime.type_(), mime.subtype().as_ref()) {
(mime::TEXT, "gemini") => Media::Gemini(body.into()),
let url = "gopher://abc.de".parse().unwrap();
let mut stream: &[u8] = b"";
let Some(url) = handle_response_header(head, url, &mut stream)
else {panic!("expected redirect handling to return a url")};
let mut app = Cli {};
let url = {
let crate::response::Header::Redirect(s) = head
else {
panic!("expected redirect")
};
crate::handle::redirect(s, &mut app)
};
}
#[test]
fn test_request() {
let url: Url = "gemini://some/url".try_into().unwrap();
let req = b"gemini://some/url\r\n";
let mut net_stream = [0; 1024];
send_request(&mut net_stream.as_mut_slice(), &url).expect("error sending request");
assert_eq!(req, &net_stream[..req.len()]);
}
#[rstest]
#[case('0', Some(false))]
#[case('1', Some(true))]
#[case('3', None)]
fn test_response_input(#[case] sensitive_code: char, #[case] sensitive: Option<bool>) {
let exp_head = Header::Input(header::Input {
prompt: "prompt text".to_string(),
sensitive,
});
let resp_text = format!("1{sensitive_code} prompt text\r\n");
let resp_head: &[u8] = resp_text.as_bytes();
let head = read_response_header(&mut &*resp_head).expect("failed to read response");
assert_eq!(exp_head, head);
name = "ansi-control-codes"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ca68a5142d31bc74840dc5520efedf65a3feb348fcca74d38ae1dadc0822f8"
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]