From f5a99679af85ce900cef289325c67a53c376edd4 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Mon, 27 Jul 2020 19:49:39 -0400 Subject: [PATCH] purge tokio --- Cargo.toml | 36 ++++---- majc/Cargo.toml | 1 - majc/src/gemini.rs | 2 +- majc/src/tls.rs | 1 - site/Cargo.toml | 12 +-- site/src/main.rs | 18 ++-- site/src/majc.gmi | 33 ++------ src/client.rs | 18 ++-- src/server/mod.rs | 199 +++++++++++++++++++++++---------------------- 9 files changed, 151 insertions(+), 169 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 98025f6..6b6fe9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,34 +17,36 @@ num-traits = "0.2" rustls = { version = "0.18", optional = true, features = ["dangerous_configuration"] } webpki = { version = "0.21.0", optional = true } webpki-roots = { version = "0.20", optional = true } -tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true } -tokio-io-timeout = "0.4" +async-tls = { version = "0.9.0", default-features = false, optional = true } +async-std = { version = "1.6", optional = true } log = "0.4" url = "2" thiserror = "1" structopt = "0.3" +once_cell = "1.4" [dev-dependencies] pretty_env_logger = "0.4" -[dependencies.tokio] -version = "0.2" -features = [ - "macros", - "net", - "tcp", - "io-util", - "rt-threaded", - "time", - "stream" -] -optional = true - [features] default = ["client", "server"] -client = ["rustls", "webpki", "webpki-roots", "tokio", "tokio-rustls"] -server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"] +client = [ + "rustls", + "webpki", + "webpki-roots", + "async-std", + "async-tls/client" +] + +server = [ + "rustls", + "webpki", + "webpki-roots", + "async-trait", + "async-std", + "async-tls/server" +] [workspace] members = [ diff --git a/majc/Cargo.toml b/majc/Cargo.toml index ae85c61..0b7048f 100644 --- a/majc/Cargo.toml +++ b/majc/Cargo.toml @@ -11,7 +11,6 @@ cursive = "0.15" log = "0.4" url = "2" webpki = "0.21.0" -tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] } rustls = { version = "0.18", features = ["dangerous_configuration"] } smol = { version = "0.3", features = ["tokio02"] } diff --git a/majc/src/gemini.rs b/majc/src/gemini.rs index 7ccb3ac..d779b03 100644 --- a/majc/src/gemini.rs +++ b/majc/src/gemini.rs @@ -7,7 +7,7 @@ use cursive::{ }; use maj::{self, Response}; use std::str; -use tokio_rustls::rustls::ClientConfig; +use rustls::ClientConfig; /// The state of the browser. #[derive(Clone)] diff --git a/majc/src/tls.rs b/majc/src/tls.rs index 7a6e853..95a2b7b 100644 --- a/majc/src/tls.rs +++ b/majc/src/tls.rs @@ -1,4 +1,3 @@ -use tokio_rustls::rustls; use std::sync::Arc; pub fn config() -> rustls::ClientConfig { diff --git a/site/Cargo.toml b/site/Cargo.toml index 87f4df9..c072779 100644 --- a/site/Cargo.toml +++ b/site/Cargo.toml @@ -7,12 +7,12 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -structopt = "0.3" -tokio = { version = "0.2", features = ["rt-threaded", "macros"] } -tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] } -async-trait = "0" -pretty_env_logger = "0.4" -log = "0" anyhow = "1" +async-std = "1.5" +async-trait = "0" +log = "0" +pretty_env_logger = "0.4" +rustls = { version = "0.18", features = ["dangerous_configuration"] } +structopt = "0.3" maj = { path = ".." } \ No newline at end of file diff --git a/site/src/main.rs b/site/src/main.rs index e96d57a..0d7387f 100644 --- a/site/src/main.rs +++ b/site/src/main.rs @@ -1,9 +1,11 @@ +use async_std::task; +use rustls::internal::pemfile::{certs, rsa_private_keys}; +use rustls::{Certificate, NoClientAuth, PrivateKey, ServerConfig}; use std::fs::File; use std::io::{self, BufReader}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use structopt::StructOpt; -use tokio_rustls::rustls::internal::pemfile::{certs, rsa_private_keys}; -use tokio_rustls::rustls::{Certificate, NoClientAuth, PrivateKey, ServerConfig}; #[derive(StructOpt, Debug)] struct Options { @@ -42,8 +44,7 @@ fn load_keys(path: &Path) -> io::Result> { .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) } -#[tokio::main] -async fn main() -> Result<(), maj::server::Error> { +fn main() -> Result<(), maj::server::Error> { pretty_env_logger::init(); let opts = Options::from_args(); let certs = load_certs(&opts.cert)?; @@ -56,15 +57,14 @@ async fn main() -> Result<(), maj::server::Error> { .set_single_cert(certs, keys.remove(0)) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - maj::server::serve( - &Handler { + task::block_on(maj::server::serve( + Arc::new(Handler { hostname: opts.hostname, - }, + }), config, opts.host, opts.port, - ) - .await?; + ))?; Ok(()) } diff --git a/site/src/majc.gmi b/site/src/majc.gmi index 93ba93a..80c3f74 100644 --- a/site/src/majc.gmi +++ b/site/src/majc.gmi @@ -1,39 +1,24 @@ -# majc ``` - __ - _____ _____ |__| ____ - / \ \__ \ | |_/ ___\ -| Y Y \ / __ \_ | |\ \___ -|__|_| /(____ //\__| | \___ > - \/ \/ \______| \/ + __ + _____ _____ |__| ____ + / \ \__ \ | |_/ ___\ + | Y Y \ / __ \_ | |\ \___ + |__|_| /(____ //\__| | \___ > + \/ \/ \______| \/ ``` + A curses client for Gemini! ## Homepage The main homepage for majc is on tulpa.dev: => https://tulpa.dev/cadey/maj -## Installation -majc can be installed using Nix: - -``` -$ nix-env -if https://tulpa.dev/cadey/maj/archive/master.tar.gz -A majc -``` - -Then you can run it with `majc`: - -``` -$ majc -``` - ## Important Keys : opens the menubar c: closes the active window o: prompts to open a URL +h: shows history +l: shows active links in the page q: quits majc ?: shows this screen ~: toggles the debug logging pane - ---- - -=> / Go back diff --git a/src/client.rs b/src/client.rs index e1db0a6..07d0a23 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,13 +1,8 @@ use crate::Response; +use async_std::{io::prelude::*, net::TcpStream}; +use async_tls::TlsConnector; +use rustls::TLSError; use std::{io::Cursor, sync::Arc}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpStream, -}; -use tokio_rustls::{ - rustls::{TLSError}, - TlsConnector, -}; use url::Url; #[derive(thiserror::Error, Debug)] @@ -31,7 +26,7 @@ pub enum Error { InvalidScheme(String), } -pub async fn get(u: T, cfg: tokio_rustls::rustls::ClientConfig) -> Result +pub async fn get(u: T, cfg: rustls::ClientConfig) -> Result where T: Into, { @@ -47,11 +42,10 @@ where let cfg = Arc::new(cfg); let host = ur.host_str().unwrap(); - let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?; let config = TlsConnector::from(cfg); let sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap())).await?; - let mut tls = config.connect(name_ref, sock).await?; + let mut tls = config.connect(host, sock).await?; let req = format!("{}\r\n", u); log::trace!("writing request {:?}", req); @@ -63,8 +57,6 @@ where #[cfg(test)] mod tests { - use tokio_rustls::rustls; - fn config() -> rustls::ClientConfig { let mut config = rustls::ClientConfig::new(); config diff --git a/src/server/mod.rs b/src/server/mod.rs index e6fad5f..4b3bac5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,10 +1,14 @@ use crate::{Response, StatusCode}; +use async_std::{ + io::prelude::*, + net::{TcpListener, TcpStream}, + stream::StreamExt, + task, +}; +use async_tls::TlsAcceptor; use async_trait::async_trait; -use rustls::{Certificate, Session}; +use rustls::Certificate; use std::{error::Error as StdError, net::SocketAddr, sync::Arc}; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::{net::TcpListener, stream::StreamExt}; -use tokio_rustls::TlsAcceptor; use url::Url; /// A Gemini request and its associated metadata. @@ -15,6 +19,16 @@ pub struct Request { } pub type Error = Box; +type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +enum RequestParsingError { + #[error("invalid scheme {0}")] + InvalidScheme(String), + + #[error("unexpected end of request")] + UnexpectedEnd, +} #[allow(dead_code, unused_assignments, unused_mut, unused_variables)] mod routes; @@ -22,116 +36,107 @@ pub use routes::*; #[async_trait] pub trait Handler { - async fn handle(&self, r: Request) -> Result; + async fn handle(&self, r: Request) -> Result; } pub async fn serve( - h: &(dyn Handler + Sync), + h: Arc, cfg: rustls::ServerConfig, host: String, port: u16, -) -> Result<(), Error> +) -> Result where { let cfg = Arc::new(cfg); - let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?; + let listener = TcpListener::bind(&format!("{}:{}", host, port)).await?; let mut incoming = listener.incoming(); - let acceptor = TlsAcceptor::from(cfg.clone()); - - while let Some(stream) = incoming.next().await { - let stream = stream?; + let acceptor = Arc::new(TlsAcceptor::from(cfg.clone())); + while let Some(Ok(stream)) = incoming.next().await { + let h = h.clone(); + let acceptor = acceptor.clone(); let addr = stream.peer_addr().unwrap(); - let fut = async { - let acceptor = acceptor.clone(); - let result = acceptor.accept(stream).await; - if result.is_err() { - return; - } - - let mut stream = result.unwrap(); - - let mut rd = BufReader::new(&mut stream); - let mut u = String::new(); - if let Err(why) = rd.read_line(&mut u).await { - log::error!("can't read request from {}: {:?}", addr, why); - let _ = stream - .write(format!("{} Invalid URL", StatusCode::BadRequest as u8).as_bytes()) - .await; - return; - } - u = u.trim().to_string(); - if u.len() >= 1025 { - let _ = stream - .write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes()) - .await; - return; - } - - if u.starts_with("//") { - u = format!("gemini:{}", u); - } - - match Url::parse(&u) { - Err(why) => { - let _ = stream - .write( - format!("{} bad URL: {:?}", StatusCode::BadRequest as u8, why) - .as_bytes(), - ) - .await; - } - Ok(u) => { - if u.scheme() != "gemini" { - let _ = stream - .write( - format!( - "{} Cannot handle that kind of url", - StatusCode::ProxyRequestRefused as u8 - ) - .as_bytes(), - ) - .await; - return; - } - - if let Some(u_port) = u.port() { - if port != u_port { - let _ = stream - .write( - format!( - "{} Cannot handle that kind of url", - StatusCode::ProxyRequestRefused as u8 - ) - .as_bytes(), - ) - .await; - return; - } - } - - tokio::join!(handle( - h, - Request { - url: u.clone(), - certs: stream.get_ref().1.get_peer_certificates(), - }, - &mut stream, - addr, - )); - } - } - }; - - tokio::join!(fut); + task::spawn(handle_request(h, stream, acceptor, addr)); } Ok(()) } -async fn handle(h: &(dyn Handler + Sync), req: Request, stream: &mut T, addr: SocketAddr) +/// Handle a single client session (request + response). +async fn handle_request( + h: Arc<(dyn Handler + Send + Sync)>, + stream: TcpStream, + acceptor: Arc, + addr: SocketAddr, +) -> Result { + // Perform handshake. + let mut stream = acceptor.clone().accept(stream).await?; + + match parse_request(&mut stream).await { + Ok(url) => { + let req = Request { + url: url, + certs: None, + }; + handle(h, req, &mut stream, addr).await; + } + Err(e) => { + respond(&mut stream, "59", &["Invalid request."]).await?; + return Err(e); + } + } + Ok(()) +} + +async fn respond(mut stream: W, status: &str, meta: &[&str]) -> Result { + stream.write_all(status.as_bytes()).await?; + stream.write_all(b" ").await?; + for m in meta { + stream.write_all(m.as_bytes()).await?; + } + stream.write_all(b"\r\n").await?; + Ok(()) +} + +/// Return the URL requested by the client. +async fn parse_request(mut stream: R) -> Result { + // Because requests are limited to 1024 bytes (plus 2 bytes for CRLF), we + // can use a fixed-sized buffer on the stack, avoiding allocations and + // copying, and stopping bad clients from making us use too much memory. + let mut request = [0; 1026]; + let mut buf = &mut request[..]; + let mut len = 0; + + // Read until CRLF, end-of-stream, or there's no buffer space left. + loop { + let bytes_read = stream.read(buf).await?; + len += bytes_read; + if request[..len].ends_with(b"\r\n") { + break; + } else if bytes_read == 0 { + Err(RequestParsingError::UnexpectedEnd)? + } + buf = &mut request[len..]; + } + let request = std::str::from_utf8(&request[..len - 2])?; + + // Handle scheme-relative URLs. + let url = if request.starts_with("//") { + Url::parse(&format!("gemini:{}", request))? + } else { + Url::parse(request)? + }; + + // Validate the URL. TODO: Check the hostname and port. + if url.scheme() != "gemini" { + Err(RequestParsingError::InvalidScheme(url.scheme().to_string()))? + } + Ok(url) +} + +async fn handle(h: Arc<(dyn Handler + Send + Sync)>, req: Request, stream: &mut T, addr: SocketAddr) where - T: AsyncWriteExt + Unpin, + T: Write + Unpin, { let u = req.url.clone(); match h.handle(req).await { @@ -141,7 +146,7 @@ where .await .unwrap(); stream.write(&resp.body).await.unwrap(); - log::info!("{}: {} {:?}", addr, u, resp.status); + log::info!("{}: {} {} {:?}", addr, u, resp.meta, resp.status); } Err(why) => { stream