From c81468ac2358f5dc7c7af095221d078c15da0ad9 Mon Sep 17 00:00:00 2001 From: Andrew Thorp Date: Sat, 24 Jul 2021 17:53:06 -0400 Subject: [PATCH] Parse URL and add scheme when missing --- Cargo.lock | 79 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 25 ++++++------ src/request.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cce04d..d225230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,7 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", + "url", "webpki-roots", ] @@ -128,6 +129,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[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 = "getrandom" version = "0.2.3" @@ -163,6 +174,17 @@ dependencies = [ "libc", ] +[[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 = "indexmap" version = "1.7.0" @@ -203,6 +225,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + [[package]] name = "memchr" version = "2.4.0" @@ -313,6 +341,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pin-project-lite" version = "0.2.7" @@ -534,6 +568,21 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +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 = "tokio" version = "1.9.0" @@ -572,6 +621,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[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-segmentation" version = "1.8.0" @@ -596,6 +663,18 @@ version = "0.7.1" 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 = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 199a94b..b7263d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ clap = "3.0.0-beta.2" native-tls = "0.2" tokio = { version = "1.9.0", features = ["rt-multi-thread", "macros", "net", "io-std", "io-util"] } tokio-native-tls = "0.3" +url = "2.2.2" webpki-roots = "0.21" diff --git a/src/main.rs b/src/main.rs index f735860..a47769a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context, Result}; use clap::{AppSettings, Clap}; use native_tls::TlsConnector; +use std::convert::TryFrom; use std::fs::File; use std::io; use std::io::BufReader; @@ -10,6 +11,7 @@ use std::sync::Arc; use tokio::io::{copy, split, stdout as tokio_stdout, AsyncWriteExt}; use tokio::net::TcpStream; +mod request; mod response; /// Coil is a Curl-like client for the Gemini web protocol. @@ -18,6 +20,7 @@ mod response; #[clap(setting = AppSettings::ColoredHelp)] struct Coil { /// The hostname of the server to send the request. + /// The Gemini protocol is inferred." hostname: String, /// Port number of the listening server. #[clap(short, long, default_value = "1965")] @@ -28,14 +31,12 @@ struct Coil { async fn main() -> Result<()> { let coil: Coil = Coil::parse(); - // Open TCP connection - // let hostname = "gemini.circumlunar.space"; - let protocol = "gemini://"; - // The year of the first manned Gemini mission - //let port: u16 = 1965; + let req = request::Request::try_from(coil.hostname).context("Error creating request")?; - let addr = (coil.hostname.as_str(), coil.port) - .to_socket_addrs()? + // Open TCP connection + let addr = (req.host.clone(), coil.port) + .to_socket_addrs() + .context("Error parsing hostname")? .next() .context("Could not get hostname address")?; @@ -45,16 +46,16 @@ async fn main() -> Result<()> { let connector = tokio_native_tls::TlsConnector::from(connector); // Open connection - let stream = TcpStream::connect(&addr).await?; + let stream = TcpStream::connect(&addr) + .await + .context("TCP Connection failure")?; let mut stream = connector - .connect(coil.hostname.as_str(), stream) + .connect(req.host.as_str(), stream) .await .context("Connection failed")?; - let req = format!("{}{}/\r\n", protocol, coil.hostname,); - - stream.write_all(req.as_bytes()).await?; + stream.write_all(&req.as_bytes()).await?; let (mut reader, mut writer) = split(stream); diff --git a/src/request.rs b/src/request.rs index 44afcd5..0416e0a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,3 +1,102 @@ -struct Request { - url: String, +use anyhow::{anyhow, Context, Result}; +use std::convert::TryFrom; +use url::{Host, Url}; + +#[derive(Debug, Eq, PartialEq)] +pub struct Request { + pub protocol: String, + pub host: String, + pub path: String, +} + +impl Request { + pub fn as_bytes<'a>(self) -> Vec { + format!("{}://{}{}\r\n", self.protocol, self.host, self.path).into_bytes() + } +} + +impl TryFrom for Request { + type Error = anyhow::Error; + + fn try_from(input: String) -> Result { + let mut url = String::new(); + + // Ensure the scheme is specified and is Gemini + if !input.starts_with("gemini://") && !input.contains("://") { + url.push_str(format!("gemini://{}", input).as_str()); + } else if input.starts_with("gemini://") { + url.push_str(input.as_str()); + } else { + let i = input.find("://").unwrap(); + let scheme = input.get(0..i).unwrap(); + return Err(anyhow!("Unsupported scheme: {}", scheme)); + } + + let mut url = Url::parse(url.as_str()).context("Error parsing URL")?; + + // Set path if path is empty + if url.path() == "" { + url.set_path("/"); + }; + + Ok(Request { + protocol: url.scheme().to_string(), + host: url.host_str().unwrap().to_string(), + path: url.path().to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_ok_hostname_ok_scheme() { + let url = String::from("gemini://hostname.com/path/to/file"); + let r = Request::try_from(url).unwrap(); + let expected = Request { + protocol: String::from("gemini"), + host: String::from("hostname.com"), + path: String::from("/path/to/file"), + }; + assert_eq!(r, expected); + } + + #[test] + fn parse_ok_hostname_no_scheme() { + let url = String::from("hostname.com/path/to/file"); + let r = Request::try_from(url).unwrap(); + let expected = Request { + protocol: String::from("gemini"), + host: String::from("hostname.com"), + path: String::from("/path/to/file"), + }; + assert_eq!(r, expected); + } + #[test] + fn parse_ok_hostname_no_path() { + let url = String::from("gemini://hostname.com"); + let r = Request::try_from(url).unwrap(); + let expected = Request { + protocol: String::from("gemini"), + host: String::from("hostname.com"), + path: String::from("/"), + }; + assert_eq!(r, expected); + } + + #[test] + fn parse_bad_hostname_ok_scheme() { + let url = String::from("gemini://host&^name.com/path/to/file"); + let r = Request::try_from(url); + assert!(r.is_err()); + } + + #[test] + fn parse_ok_hostname_bad_scheme() { + let url = String::from("https://hostname.com/path/to/file"); + let r = Request::try_from(url); + assert!(r.is_err()); + } } -- 2.45.2