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),
}