~klve/poptea

dc66f00c544a0a345e87a78dca1d66a4f06f96e9 — klve bunc ntnn 6 months ago 3cf38e5
Extracted client code into a lib
6 files changed, 156 insertions(+), 77 deletions(-)

M Cargo.lock
M Cargo.toml
M src/bin/poptea-cli.rs
M src/infra/client.rs
M src/infra/mod.rs
M src/lib.rs
M Cargo.lock => Cargo.lock +76 -0
@@ 21,6 21,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
 "matches",
 "percent-encoding",
]

[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
 "matches",
 "unicode-bidi",
 "unicode-normalization",
]

[[package]]
name = "js-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 51,16 72,29 @@ dependencies = [
]

[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"

[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"

[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"

[[package]]
name = "poptea"
version = "0.1.0"
dependencies = [
 "rustls",
 "url",
]

[[package]]


@@ 136,6 170,36 @@ dependencies = [
]

[[package]]
name = "tinyvec"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
 "tinyvec_macros",
]

[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "unicode-bidi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"

[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
 "tinyvec",
]

[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 148,6 212,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"

[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
 "form_urlencoded",
 "idna",
 "matches",
 "percent-encoding",
]

[[package]]
name = "wasm-bindgen"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +1 -0
@@ 7,3 7,4 @@ edition = "2021"

[dependencies]
rustls = {version = "0.20.1", features = ["dangerous_configuration"]}
url = "2.2.2"

M src/bin/poptea-cli.rs => src/bin/poptea-cli.rs +8 -38
@@ 1,43 1,13 @@
use io::{Read, Write};
use io::Write;
use poptea::GeminiClient;
use std::io;
use std::sync::Arc;

fn main() {
    let root_store = rustls::RootCertStore::empty();
    let mut config = rustls::ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(root_store)
        .with_no_client_auth();
    config
        .dangerous()
        .set_certificate_verifier(Arc::new(NoCertificateVerification {}));
    let arc = std::sync::Arc::new(config);
    let url = std::env::args().nth(1).expect("please provide gemini url");
    let client = poptea::TlsClient::new();
    let res = client.get(&url).expect("failed to make a request");

    let mut sess =
        rustls::ClientConnection::new(arc, "transjovian.org".try_into().unwrap()).unwrap();
    let mut sock = std::net::TcpStream::connect("transjovian.org:1965").unwrap();
    let mut stream = rustls::Stream::new(&mut sess, &mut sock);

    stream
        .write_all(b"gemini://transjovian.org/oracle/\r\n")
        .unwrap();
    let mut plaintext = Vec::new();
    stream.read_to_end(&mut plaintext).unwrap();
    io::stdout().write_all(&plaintext).unwrap();
}

pub struct NoCertificateVerification {}

impl rustls::client::ServerCertVerifier for NoCertificateVerification {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::Certificate,
        _intermediates: &[rustls::Certificate],
        _server_name: &rustls::ServerName,
        _scts: &mut dyn Iterator<Item = &[u8]>,
        _ocsp: &[u8],
        _now: std::time::SystemTime,
    ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::ServerCertVerified::assertion())
    }
    io::stdout()
        .write_all(&res.body.unwrap_or_else(|| b"response has no body".to_vec()))
        .expect("failed to write to stdout");
}

M src/infra/client.rs => src/infra/client.rs +45 -30
@@ 1,15 1,15 @@
use std::net::TcpStream;
use url::Url;

use io::{Read, Write};
use std::io;
use std::str::FromStr;
use std::sync::Arc;
use rustls::ClientConnection;
use std::{io, io::BufRead};

use crate::{PopResult, GemResponse, GeminiClient};
use crate::{GemResponse, GemStatus, GeminiClient, PopResult};

struct NoCertificateVerification {}
struct TofuVerification {}

impl rustls::client::ServerCertVerifier for NoCertificateVerification {
impl rustls::client::ServerCertVerifier for TofuVerification {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::Certificate,


@@ 23,48 23,63 @@ impl rustls::client::ServerCertVerifier for NoCertificateVerification {
    }
}

struct TlsClient {
    socket: TcpStream,
    closing: bool,
    clean_closure: bool,
    tls_conn: rustls::ClientConnection,
}
pub struct TlsClient {}

impl TlsClient {
    fn new(
        sock: TcpStream,
        server_name: rustls::ServerName,
        cfg: Arc<rustls::ClientConfig>,
    ) -> Self {
        Self {
            socket: sock,
            closing: false,
            clean_closure: false,
            tls_conn: ClientConnection::new(cfg, server_name).unwrap(),
        }
    pub fn new() -> Self {
        Self {}
    }

    fn get_stream(&self, url: &str) -> PopResult<rustls::Stream<ClientConnection, TcpStream>> {
    pub fn get_plain(&self, url: &str) -> PopResult<Vec<u8>> {
        let url = Url::parse(url).map_err(|_| crate::PopError::Local("failed to parse".into()))?;
        let root_store = rustls::RootCertStore::empty();

        let mut plaintext = vec![];
        let host = url
            .host_str()
            .ok_or_else(|| crate::PopError::Local("host is missing".into()))?;
        let addr = format!("{}:{:?}", host, url.port().unwrap_or(1965));
        let req = format!("{}\r\n", url);

        let mut config = rustls::ClientConfig::builder()
            .with_safe_defaults()
            .with_root_certificates(root_store)
            .with_no_client_auth();
        config
            .dangerous()
            .set_certificate_verifier(Arc::new(NoCertificateVerification {}));
            .set_certificate_verifier(Arc::new(TofuVerification {}));
        let arc = std::sync::Arc::new(config);

        let mut sess =
            rustls::ClientConnection::new(arc, "transjovian.org".try_into().unwrap()).unwrap();
        let mut sock = std::net::TcpStream::connect("transjovian.org:1965").unwrap();
        let mut sess = rustls::ClientConnection::new(arc, host.try_into().unwrap()).unwrap();
        let mut sock = std::net::TcpStream::connect(addr).unwrap();
        let mut stream = rustls::Stream::new(&mut sess, &mut sock);
        Ok(stream)
        stream.write_all(req.as_bytes()).unwrap();
        stream.read_to_end(&mut plaintext).unwrap();

        Ok(plaintext)
    }
}

impl GeminiClient for TlsClient {
    fn get(&self, url: &str) -> PopResult<GemResponse> {
        unimplemented!()
        let plaintext = self.get_plain(url)?;
        let header = plaintext.lines().next().unwrap().unwrap();
        let body = plaintext[header.len()..].to_vec();

        let (status, meta) = header
            .split_once(" ")
            .map(|(s, m)| (GemStatus::from_str(s), m.into()))
            .ok_or_else(|| crate::PopError::Remote("invalid header".into()))?;

        let body = match &body.len() {
            0 => None,
            _ => Some(body),
        };

        Ok(GemResponse {
            status: status?,
            meta,
            body,
        })
    }
}

M src/infra/mod.rs => src/infra/mod.rs +1 -1
@@ 1,3 1,3 @@
mod client;

pub use client::RustlsClient;
pub use client::TlsClient;

M src/lib.rs => src/lib.rs +25 -8
@@ 1,6 1,9 @@
mod infra;
pub use infra::TlsClient;
use std::str::FromStr;

enum GemStatus {
#[derive(Debug)]
pub enum GemStatus {
    Input,
    SensitiveInput,
    Success,


@@ 21,21 24,35 @@ enum GemStatus {
    CertificateNotValid,
}

enum GemMimeType {
#[derive(Debug)]
pub enum GemMimeType {
    GeminiText,
}

struct GemResponse {
    status: GemStatus,
    meta: Option<String>,
    body: Option<String>,
#[derive(Debug)]
pub struct GemResponse {
    pub status: GemStatus,
    pub meta: String,
    pub body: Option<Vec<u8>>,
}

trait GeminiClient {
impl FromStr for GemStatus {
    type Err = PopError;

    fn from_str(input: &str) -> Result<GemStatus, Self::Err> {
        match input {
            "20" => Ok(GemStatus::Success),
            _ => Err(PopError::Local("unimplemented status code".into())),
        }
    }
}

pub trait GeminiClient {
    fn get(&self, url: &str) -> PopResult<GemResponse>;
}

enum PopError {
#[derive(Debug)]
pub enum PopError {
    Local(String),
    Remote(String),
}