// Copyright (c) 2022 Tebibyte Media // SPDX-License-Identifier: GPL-3.0-or-later use clap::Parser; use std::sync::Arc; use url::Url; #[derive(Parser, Debug)] #[clap(author = "Tebibyte Media")] struct Args { #[clap()] url: Url, } struct SkipServerVerification; impl SkipServerVerification { fn new() -> Arc { Arc::new(Self) } } impl rustls::client::ServerCertVerifier for SkipServerVerification { fn verify_server_cert( &self, _end_entity: &rustls::Certificate, _intermediates: &[rustls::Certificate], _server_name: &rustls::ServerName, _scts: &mut dyn Iterator, _ocsp_response: &[u8], _now: std::time::SystemTime, ) -> Result { Ok(rustls::client::ServerCertVerified::assertion()) } } #[rustfmt::skip] #[derive(Debug, strum::Display, strum::EnumString)] pub enum ResponseStatus { #[strum(serialize="10")] Input, #[strum(serialize="11")] SensitiveInput, #[strum(serialize="20")] Success, #[strum(serialize="30")] TemporaryRedirect, #[strum(serialize="31")] PermanentRedirect, #[strum(serialize="40")] TemporaryFailure, #[strum(serialize="41")] ServerUnavailable, #[strum(serialize="42")] CgiError, #[strum(serialize="43")] ProxyError, #[strum(serialize="44")] SlowDown, #[strum(serialize="50")] PermanentFailure, #[strum(serialize="51")] NotFound, #[strum(serialize="52")] Gone, #[strum(serialize="53")] ProxyRequestRefused, #[strum(serialize="59")] BadRequest, #[strum(serialize="60")] ClientCertRequired, #[strum(serialize="61")] ClientCertNotAuthorized, #[strum(serialize="62")] ClientCertNotValid, } #[derive(Debug)] pub struct Response { pub status: ResponseStatus, pub meta: String, pub body: Vec, } impl Response { pub fn from_reader(reader: &mut impl std::io::Read) -> Self { let mut response = Vec::new(); reader.read_to_end(&mut response).unwrap(); let crlf = b"\r\n"; let header_end = response .windows(crlf.len()) .position(|window| window == crlf) .unwrap(); let status_code = std::str::from_utf8(&response[0..2]).unwrap(); let status = ResponseStatus::try_from(status_code).unwrap(); let meta = String::from_utf8(response[3..header_end].to_vec()).unwrap(); let body_begin = header_end + crlf.len(); let body = response[body_begin..].to_vec(); Self { status, meta, body } } } fn get_gemini(url: &Url) -> Response { use rustls::{ClientConfig, ClientConnection}; use std::io::Write; use std::net::TcpStream; let config = ClientConfig::builder() .with_safe_defaults() .with_custom_certificate_verifier(SkipServerVerification::new()) .with_no_client_auth(); let rc_config = Arc::new(config); let host_str = url.host_str().unwrap(); let server_name = host_str.try_into().unwrap(); let mut conn = ClientConnection::new(rc_config, server_name).unwrap(); let port = url.port().unwrap_or(1965); let mut sock = TcpStream::connect((host_str, port)).unwrap(); let mut tls = rustls::Stream::new(&mut conn, &mut sock); let request = format!("{}\r\n", url); tls.write_all(request.as_bytes()).unwrap(); Response::from_reader(&mut tls) } fn main() { let args = Args::parse(); let mut url = args.url; let (mime, body) = loop { println!("Requesting {}", url); let response = get_gemini(&url); use ResponseStatus::*; match response.status { Success => { let mime: mime::Mime = response.meta.parse().unwrap(); break (mime, response.body); } TemporaryRedirect | PermanentRedirect => { url = response.meta.as_str().try_into().unwrap(); println!("Redirected to {}", url); } Input | SensitiveInput => { use std::io::Write; print!("[Input]\n {}\n=> ", response.meta); std::io::stdout().flush().unwrap(); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); let input = input.trim(); use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; let ascii_set = NON_ALPHANUMERIC; let encoded = utf8_percent_encode(&input, &ascii_set); url.set_query(Some(&encoded.to_string())); }, e => { eprintln!("Received error status {} ({:?})", e, e); std::process::exit(1); } } }; match mime.type_().as_str() { "text" => { let body = String::from_utf8(body).unwrap(); match mime.subtype().as_str() { "plain" => println!("Plain text:\n{}", body), "gemini" => println!("Gemtext:\n{}", body), _ => { eprintln!("Unsupported mime type {}", mime); std::process::exit(1); } } } _ => { eprintln!("Unsupported mime type {}", mime); std::process::exit(1); } } }