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) = host
else {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 body
Success(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 anything
println!("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 use
pub 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",
]