use std::io::{stdin, Read, Write};
use anyhow::{anyhow, Result};
use libtls::{config::Builder, tls::Tls};
use pico_args::Arguments;
use url::{Position, Url};
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
enum Status {
Input,
Success,
Redirect,
TemporaryFailure,
PermanentFailure,
ClientCertificateRequired,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
struct Header {
status: u8,
meta: String,
}
impl Header {
pub fn status(&self) -> Status {
if self.status >= 60 {
Status::ClientCertificateRequired
} else if self.status >= 50 {
Status::PermanentFailure
} else if self.status >= 40 {
Status::TemporaryFailure
} else if self.status >= 30 {
Status::Redirect
} else if self.status >= 20 {
Status::Success
} else if self.status >= 10 {
Status::Input
} else {
panic!("invalid status: {}", self.status)
}
}
}
fn parse_address(mut address: String) -> Result<Url> {
if !address.trim_start().starts_with("gemini://") {
if address.contains("://") {
return Err(anyhow!("only gemini is supported"));
}
address = format!("gemini://{}", address);
}
let mut url = Url::parse(address.as_str())?;
if !url.username().is_empty() || url.password().is_some() {
return Err(anyhow!("no user info is allowed"));
}
if !url.has_host() {
return Err(anyhow!("must supply host"));
}
if url.port().is_none() {
url.set_port(Some(1965)).expect("port error")
}
Ok(url)
}
fn make_request(url: &Url) -> Result<(Header, String)> {
let mut client = Tls::client()?;
let config = Builder::new().noverifycert().build()?;
client.configure(&config)?;
client.connect(&url[Position::BeforeHost..Position::AfterPort], None)?;
let shaken = client.tls_handshake()?;
if shaken != 0 {
return Err(anyhow!("tls handshake failed"));
}
let req = format!("{}\r\n", url);
let written = client.write(req.as_bytes())?;
if written == 0 {
return Err(anyhow!("failed to write request"));
} else if written != req.len() {
eprintln!(
"warning: request was {} bytes, only wrote {}",
req.len(),
written
)
}
let mut res = String::new();
let read = client.read_to_string(&mut res)?;
if read == 0 {
return Err(anyhow!("failed to read response"));
}
let line_break = res
.match_indices("\r\n")
.next()
.expect("response missing CRLF")
.0;
if line_break >= 1028 {
return Err(anyhow!("meta was too long"));
}
let body = res.split_off(line_break);
let header = Header {
status: (&res[..2]).parse().unwrap(),
meta: (&res[2..]).to_string(),
};
Ok((header, body))
}
fn handle_response(url: Url, header: Header, body: String) -> Result<()> {
println!("status: {}", header.status);
match header.status() {
Status::Input => {
println!("server is requesting input");
println!("{}", header.meta);
print!("> ");
let mut line = String::from("?");
stdin().read_line(&mut line)?;
let with_input = url.join(line.as_str())?;
println!("connecting to {} again with new input", with_input);
let (header, body) = make_request(&with_input)?;
handle_response(with_input, header, body)?
}
Status::Success => {
println!("{}", body)
}
Status::Redirect => {
let address = header.meta;
let redirect = parse_address(address)?;
println!("redirecting {} to {}", url, redirect);
let (header, body) = make_request(&redirect)?;
handle_response(redirect, header, body)?
}
Status::TemporaryFailure | Status::PermanentFailure => {
eprintln!("error from server");
eprintln!("{}", header.meta)
}
Status::ClientCertificateRequired => {
eprintln!("server requires a client certificate");
eprintln!("{}", header.meta)
}
};
Ok(())
}
fn main() -> Result<()> {
let mut args = Arguments::from_env();
let address: String = args.free_from_str()?.expect("must supply address");
let url = parse_address(address)?;
println!("connecting to {}", url);
let (header, body) = make_request(&url)?;
handle_response(url, header, body)
}