173 lines
5.2 KiB
Rust
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, 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);
|
|
}
|
|
}
|
|
}
|