libra/src/main.rs

173 lines
5.2 KiB
Rust

// 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<Self> {
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<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
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<u8>,
}
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);
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);
}
}
}
mime => {
eprintln!("Unsupported mime type {}", mime);
std::process::exit(1);
}
}
}