Improve parsing. Add modules that were missed
Dependencies
Change contents
- file addition: server.rs[3.15]
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} - file addition: response.rs[3.15]
#[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(),})}} - edit in src/main.rs at line 4
use response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success}; - replacement in src/main.rs at line 36
fn run_url(url: String) {let url: Url = url.parse().expect("Not a valid URL");fn run_url(url: String) -> Option<Url> {let mut url: Url = url.parse().expect("Not a valid URL"); - replacement in src/main.rs at line 40
send_request(&mut stream, url);send_request(&mut stream, &url).expect("request failed to send"); - replacement in src/main.rs at line 45
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}} - edit in src/main.rs at line 97[3.4504]→[2.384:427](∅→∅),[2.427]→[3.4547:4568](∅→∅),[3.4547]→[3.4547:4568](∅→∅),[3.4568]→[2.428:446](∅→∅),[2.446]→[3.4586:4607](∅→∅),[3.4586]→[3.4586:4607](∅→∅),[3.4607]→[2.447:448](∅→∅)
// TODO parse from 2-digit response headertype Code = [u8; 2];// TODO parse etctype Meta = Vec<u8>; - replacement in src/main.rs at line 103
panic!("separator b'{c}' was not a space")println!("Warning: illegal separator byte '0x{c:02X}' was not a space") - edit in src/main.rs at line 114
let meta = meta.into_string().expect("Meta was not valid UTF8");let header = response::raw::Header { meta, status }; - replacement in src/main.rs at line 115
Ok(header.try_into().expect("failed to parse header"))let meta = response::Meta(meta.into_string().expect("Meta was not valid UTF8"));let status = response::raw::Header { meta, status }.try_into().expect("failed to parse header");Ok(status) - replacement in src/main.rs at line 128
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) - file addition: io.rs[3.15]
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)} - file addition: danger.rs[3.15]
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())}} - edit in Cargo.toml at line 12
num_enum = "0.6.0"rpassword = "7.2.0" - edit in Cargo.toml at line 16[3.42276]
urlencoding = "2.1.2" - edit in Cargo.lock at line 52
name = "autocfg"version = "1.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"[[package]] - edit in Cargo.lock at line 187
"num_enum","rpassword", - edit in Cargo.lock at line 191
"urlencoding", - edit in Cargo.lock at line 193
[[package]]name = "hashbrown"version = "0.12.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - edit in Cargo.lock at line 220
][[package]]name = "indexmap"version = "1.9.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"dependencies = ["autocfg","hashbrown", - edit in Cargo.lock at line 292
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]] - edit in Cargo.lock at line 325
name = "proc-macro-crate"version = "1.3.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"dependencies = ["once_cell","toml_edit",][[package]] - edit in Cargo.lock at line 370
"winapi",][[package]]name = "rpassword"version = "7.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322"dependencies = ["libc","rtoolbox", - edit in Cargo.lock at line 385
name = "rtoolbox"version = "0.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a"dependencies = ["libc","winapi",][[package]] - edit in Cargo.lock at line 494
[[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",] - edit in Cargo.lock at line 551
name = "urlencoding"version = "2.1.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"[[package]] - edit in Cargo.lock at line 713[3.58544]
[[package]]name = "winnow"version = "0.4.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"dependencies = ["memchr",]