3SPNKI46URBW4PD3JRMBV5YV5W56N3SFUTK25VQEF2FUZ64ZDQPAC use std::{net::TcpStream, sync::Arc};use rustls::{client::ServerCertVerifier, ClientConnection, ServerName};use url::Url;pub(crate) fn connect<V: Default + ServerCertVerifier + 'static>(url: &Url,) -> rustls::StreamOwned<ClientConnection, TcpStream> {let host = url.host().expect("");let sn = server(host);let stream = {let cfg = tls_cfg::<V>();let conn = {let client = ClientConnection::new(cfg, sn).expect("could not connect");client};let addrs = url.socket_addrs(|| Some(1965)).expect("could not get addresses for URL");let addr = addrs.get(0).expect("no addresses");let sock = TcpStream::connect(addr).expect("failed to connect");let stream = rustls::StreamOwned { conn, sock };stream};stream}fn server(host: url::Host<&str>) -> ServerName {let url::Host::Domain(s) = hostelse {unreachable!("the url is always a string")};let sn = s.try_into().expect("this should be a valid DNS name");sn}fn tls_cfg<V: Default + ServerCertVerifier + 'static>() -> Arc<rustls::ClientConfig> {let root_store = rustls::RootCertStore::empty();let cfg = {let mut cfg = rustls::ClientConfig::builder().with_safe_defaults().with_root_certificates(root_store).with_no_client_auth();cfg.dangerous().set_certificate_verifier(Arc::new(V::default()));Arc::new(cfg)};cfg}
#[repr(u8)]#[derive(Debug, PartialEq, Eq)]pub enum Status {Input(header::Input) = 1,/// has a response bodySuccess(header::Success) = 2,Redirect(header::Redirect) = 3,FailTemp(header::FailTemp) = 4,FailPerm(header::FailPerm) = 5,CertRequired(header::CertRequired) = 6,}pub mod raw {pub use super::Meta;pub type Status = [u8; 2];pub struct Header {pub meta: Meta,pub status: Status,}}pub mod header {use url::Url;use crate::response::Mime;use super::Meta;#[derive(Debug, PartialEq, Eq)]pub struct Input {pub prompt: String,pub sensitive: Option<bool>,}impl From<(u8, Meta)> for Input {fn from((code, meta): (u8, Meta)) -> Self {let prompt = super::Meta::try_from(meta).unwrap().0;let sensitive = match code {0 => Some(false),1 => Some(true),_ => None,};Self { prompt, sensitive }}}#[derive(Debug, PartialEq, Eq)]pub struct Success {pub mime: Mime,}impl From<(u8, Meta)> for Success {fn from((_, meta): (u8, Meta)) -> Self {let mime = Mime::try_from(Meta::try_from(meta).unwrap()).expect("failed to read mime");Self { mime }}}#[derive(Debug, PartialEq, Eq)]pub struct Redirect {pub url: Url,pub temporary: Option<bool>,}impl From<(u8, Meta)> for Redirect {fn from((code, meta): (u8, Meta)) -> Self {let temporary = match code {0 => Some(true),1 => Some(false),_ => None,};Self {url: meta.0.parse().expect("not a valid url"),temporary,}}}#[derive(Debug, PartialEq, Eq)]pub struct FailTemp {pub message: String,pub typ: Option<sub::FailTemp>,}impl From<(u8, Meta)> for FailTemp {fn from((code, meta): (u8, Meta)) -> Self {let typ = sub::FailTemp::try_from(code).ok();Self {message: meta.0,typ,}}}#[derive(Debug, PartialEq, Eq)]pub struct FailPerm {pub message: String,pub typ: Option<sub::FailPerm>,}impl From<(u8, Meta)> for FailPerm {fn from((code, meta): (u8, Meta)) -> Self {let typ = sub::FailPerm::try_from(code).ok();Self {message: meta.0,typ,}}}#[derive(Debug, PartialEq, Eq)]pub struct CertRequired {pub message: String,pub typ: Option<sub::CertRequired>,}impl From<(u8, Meta)> for CertRequired {fn from(value: (u8, Meta)) -> Self {let (code, meta) = value;let typ = sub::CertRequired::try_from(code).ok();Self {message: meta.0,typ,}}}mod sub {use num_enum::TryFromPrimitive;#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]#[repr(u8)]pub enum FailTemp {TemporaryFailure = 0,ServerUnavailable = 1,CgiError = 2,ProxyError = 3,SlowDown = 4,}#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]#[repr(u8)]pub enum FailPerm {PermanentFailure = 0,NotFound = 1,Gone = 2,ProxyRequestRefused = 3,BadRequest = 9,}#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]#[repr(u8)]pub enum CertRequired {ClientCertificateRequired = 0,CertificateNotAuthorised = 1,CertificateNotValid = 2,}}}impl TryFrom<raw::Header> for Status {type Error = String;fn try_from(value: raw::Header) -> Result<Self, Self::Error> {let raw::Header { status, meta } = value;let [status, sub] = status;match match status {1 => Some(Status::Input((sub, meta).into())),2 => Some(Status::Success((sub, meta).into())),3 => Some(Status::Redirect((sub, meta).into())),4 => Some(Status::FailTemp((sub, meta).into())),5 => Some(Status::FailPerm((sub, meta).into())),6 => Some(Status::CertRequired((sub, meta).into())),_ => None,} {Some(s) => Ok(s),None => Err(format!("bad status code: {:?}", status)),}}}// TODO default is text/gemini#[derive(Debug, PartialEq, Eq)]pub struct Mime {typ: String,sub: String,}#[derive(Debug, PartialEq, Eq)]pub struct Meta(pub String);impl TryFrom<Meta> for Mime {type Error = String;fn try_from(meta: Meta) -> Result<Self, Self::Error> {let (typ, sub) = meta.0.split_once('/').ok_or(format!("invalid MIME type: {}", meta.0))?;Ok(Self {typ: typ.to_owned(),sub: sub.to_owned(),})}}
let body = read_response_string(&mut stream).expect("failed to read body");println!("{body}");
match head {response::Status::Input(s) => {let Input { prompt, sensitive } = s;println!("{prompt}");let input = match sensitive {Some(true) => rpassword::read_password(),_ => 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::Status::Success(s) => {let Success { mime } = s;let body = read_response_string(&mut stream).expect("failed to read body");// TODO use mime for anythingprintln!("page is encoded as {mime:?}");println!("{body}");None}response::Status::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)}response::Status::FailTemp(s) => {let FailTemp { message, typ } = s;println!("Temporary failure: {typ:?}! Please try again. Details:");println!("{message}");None}response::Status::FailPerm(s) => {let FailPerm { message, typ } = s;println!("Permanent failure: {typ:?}! Details:");println!("{message}");None}response::Status::CertRequired(s) => {let CertRequired { message, typ } = s;println!("Certificate required: {typ:?}! Details:");println!("{message}");None}}
fn send_request(stream: &mut impl Write, url: Url) {stream.write(url.as_str().as_bytes()).expect("send failed");stream.write("\r\n".as_bytes()).expect("send failed");
fn send_request(stream: &mut impl Write, url: &Url) -> ioResult<usize> {let mut sent = 0;sent += stream.write(url.as_str().as_bytes())?;sent += stream.write("\r\n".as_bytes())?;Ok(sent)
use std::io::{Read, Result};pub(crate) fn read_byte(stream: &mut impl Read) -> Result<u8> {let [c] = read_n::<1>(stream)?;Ok(c)}pub(crate) fn read_n<const N: usize>(stream: &mut impl Read) -> Result<[u8; N]> {let mut buf = [0; N];stream.read_exact(&mut buf)?;Ok(buf)}
use std::time::SystemTime;use rustls::{client::ServerCertVerified, Certificate, Error, ServerName};// unconditionally trusts#[derive(Default)]pub struct Naive;// trust on first usepub struct Tofu {}impl rustls::client::ServerCertVerifier for Naive {fn verify_server_cert(&self,_: &Certificate,_: &[Certificate],_: &ServerName,_: &mut dyn Iterator<Item = &[u8]>,_: &[u8],_: SystemTime,) -> Result<ServerCertVerified, Error> {Ok(ServerCertVerified::assertion())}}
urlencoding = "2.1.2"
name = "num_enum"version = "0.6.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a0fa9d8a04aa0af7b5845b514a828f829ae3f0ec3f60d9842e1dfaeb49a0e68b"dependencies = ["num_enum_derive",][[package]]name = "num_enum_derive"version = "0.6.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3e51dcc6bafb7f3ac88b65d2ad21f4b53d878e496712060e23011862ebd2d2d1"dependencies = ["proc-macro-crate","proc-macro2","quote","syn 2.0.13",][[package]]
[[package]]name = "toml_datetime"version = "0.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"[[package]]name = "toml_edit"version = "0.19.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"dependencies = ["indexmap","toml_datetime","winnow",]
[[package]]name = "winnow"version = "0.4.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"dependencies = ["memchr",]