maj-async-std #4

Merged
cadey merged 11 commits from maj-async-std into main 2020-07-28 01:36:02 +00:00
12 changed files with 367 additions and 210 deletions

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.4.0
Tokio has been somewhat purged.
The gemini module now includes a document builder and rendering tool.
## 0.3.0 ## 0.3.0
### maj ### maj

View File

@ -15,36 +15,41 @@ num = "0.2"
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
rustls = { version = "0.18", optional = true, features = ["dangerous_configuration"] } rustls = { version = "0.18", optional = true, features = ["dangerous_configuration"] }
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21.0", optional = true } webpki = { version = "0.21.0", optional = true }
webpki-roots = { version = "0.20", optional = true } webpki-roots = { version = "0.20", optional = true }
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"], optional = true } tokio = { version = "0.2", features = ["full"], optional = true }
tokio-io-timeout = "0.4" async-tls = { default-features = false, optional = true, git = "https://github.com/Xe/async-tls" }
async-std = { version = "1.6", optional = true }
log = "0.4" log = "0.4"
url = "2" url = "2"
thiserror = "1" thiserror = "1"
structopt = "0.3" structopt = "0.3"
once_cell = "1.4"
[dev-dependencies] [dev-dependencies]
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
[dependencies.tokio]
version = "0.2"
features = [
"macros",
"net",
"tcp",
"io-util",
"rt-threaded",
"time",
"stream"
]
optional = true
[features] [features]
default = ["client", "server"] default = ["client", "server"]
client = ["rustls", "webpki", "webpki-roots", "tokio", "tokio-rustls"] client = [
server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"] "tokio-rustls",
"webpki",
"webpki-roots",
"tokio",
"async-std",
"async-tls/client"
]
server = [
"rustls",
"webpki",
"webpki-roots",
"async-trait",
"async-std",
"async-tls/server"
]
[workspace] [workspace]
members = [ members = [

View File

@ -1 +1 @@
0.3.0 0.4.0

View File

@ -11,7 +11,6 @@ cursive = "0.15"
log = "0.4" log = "0.4"
url = "2" url = "2"
webpki = "0.21.0" webpki = "0.21.0"
tokio-rustls = { version = "0.14", features = ["dangerous_configuration"] }
rustls = { version = "0.18", features = ["dangerous_configuration"] } rustls = { version = "0.18", features = ["dangerous_configuration"] }
smol = { version = "0.3", features = ["tokio02"] } smol = { version = "0.3", features = ["tokio02"] }

View File

@ -7,7 +7,7 @@ use cursive::{
}; };
use maj::{self, Response}; use maj::{self, Response};
use std::str; use std::str;
use tokio_rustls::rustls::ClientConfig; use rustls::ClientConfig;
/// The state of the browser. /// The state of the browser.
#[derive(Clone)] #[derive(Clone)]

View File

@ -1,4 +1,3 @@
use tokio_rustls::rustls;
use std::sync::Arc; use std::sync::Arc;
pub fn config() -> rustls::ClientConfig { pub fn config() -> rustls::ClientConfig {

View File

@ -7,12 +7,12 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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" 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 = ".." } maj = { path = "..", features = ["server"], default-features = false }

View File

@ -1,9 +1,19 @@
use async_std::task;
use maj::{
gemini::Builder,
route, seg,
server::{Error, Handler as MajHandler, Request},
split, Response,
};
use rustls::internal::pemfile::{certs, rsa_private_keys};
use rustls::{
AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig,
};
use std::fs::File; use std::fs::File;
use std::io::{self, BufReader}; use std::io::{self, BufReader};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use tokio_rustls::rustls::internal::pemfile::{certs, rsa_private_keys};
use tokio_rustls::rustls::{Certificate, NoClientAuth, PrivateKey, ServerConfig};
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct Options { struct Options {
@ -42,29 +52,34 @@ fn load_keys(path: &Path) -> io::Result<Vec<PrivateKey>> {
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))
} }
#[tokio::main] fn main() -> Result<(), maj::server::Error> {
async fn main() -> Result<(), maj::server::Error> {
pretty_env_logger::init(); pretty_env_logger::init();
let opts = Options::from_args(); let opts = Options::from_args();
let certs = load_certs(&opts.cert)?; let certs = load_certs(&opts.cert)?;
let mut keys = load_keys(&opts.key)?; let mut keys = load_keys(&opts.key)?;
log::info!("{:?}", opts); log::info!(
"serving gemini://{} on {}:{}",
opts.hostname,
opts.host,
opts.port
);
let mut config = ServerConfig::new(NoClientAuth::new()); let mut config = ServerConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new(
RootCertStore::empty(),
));
config config
.set_single_cert(certs, keys.remove(0)) .set_single_cert(certs, keys.remove(0))
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
maj::server::serve( task::block_on(maj::server::serve(
&Handler { Arc::new(Handler {
hostname: opts.hostname, hostname: opts.hostname,
}, }),
config, config,
opts.host, opts.host,
opts.port, opts.port,
) ))?;
.await?;
Ok(()) Ok(())
} }
@ -73,50 +88,48 @@ struct Handler {
hostname: String, hostname: String,
} }
fn index() -> Result<maj::Response, maj::server::Error> { async fn index() -> Result<maj::Response, maj::server::Error> {
let msg = include_bytes!("index.gmi"); let msg = include_bytes!("index.gmi");
Ok(Response::gemini(msg.to_vec()))
Ok(maj::Response {
status: maj::StatusCode::Success,
meta: "text/gemini".to_string(),
body: msg.to_vec(),
})
} }
fn majc() -> Result<maj::Response, maj::server::Error> { async fn majc() -> Result<maj::Response, maj::server::Error> {
let msg = include_bytes!("majc.gmi"); let msg = include_bytes!("majc.gmi");
Ok(Response::gemini(msg.to_vec()))
}
Ok(maj::Response { async fn need_cert(req: Request) -> Result<Response, Error> {
status: maj::StatusCode::Success, match req.certs {
meta: "text/gemini".to_string(), None => Ok(Response::need_cert("test")),
body: msg.to_vec(), Some(certs) => Ok(Response::render(
}) Builder::new()
.heading(1, "Cert test")
.text(format!("{:?}", certs))
.build(),
)),
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl maj::server::Handler for Handler { impl MajHandler for Handler {
async fn handle(&self, r: maj::server::Request) -> Result<maj::Response, maj::server::Error> { async fn handle(&self, req: Request) -> Result<Response, Error> {
if r.url.has_host() && r.url.host_str().unwrap().to_string() != self.hostname { if req.url.has_host() && req.url.host_str().unwrap().to_string() != self.hostname {
return Ok(maj::Response { return Ok(Response::no_proxy());
status: maj::StatusCode::ProxyRequestRefused,
meta: "Wrong host".to_string(),
body: vec![],
});
} }
match r.url.path() { if req.url.path() == "" {
"" => Ok(maj::Response { return Ok(Response::perm_redirect(format!(
status: maj::StatusCode::PermanentRedirect, "gemini://{}/",
meta: format!("gemini://{}/", self.hostname), self.hostname
body: vec![], )));
}),
"/" => index(),
"/majc" => majc(),
_ => Ok(maj::Response {
status: maj::StatusCode::NotFound,
meta: "Not found".to_string(),
body: vec![],
}),
} }
route!(req.url.path(), {
(/) => index().await;
(/"cert") => need_cert(req).await;
(/"majc") => majc().await;
});
Ok(Response::not_found())
} }
} }

View File

@ -1,39 +1,24 @@
# majc
``` ```
__ __
_____ _____ |__| ____ _____ _____ |__| ____
/ \ \__ \ | |_/ ___\ / \ \__ \ | |_/ ___\
| Y Y \ / __ \_ | |\ \___ | Y Y \ / __ \_ | |\ \___
|__|_| /(____ //\__| | \___ > |__|_| /(____ //\__| | \___ >
\/ \/ \______| \/ \/ \/ \______| \/
``` ```
A curses client for Gemini! A curses client for Gemini!
## Homepage ## Homepage
The main homepage for majc is on tulpa.dev: The main homepage for majc is on tulpa.dev:
=> https://tulpa.dev/cadey/maj => 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 ## Important Keys
<esc>: opens the menubar <esc>: opens the menubar
c: closes the active window c: closes the active window
o: prompts to open a URL o: prompts to open a URL
h: shows history
l: shows active links in the page
q: quits majc q: quits majc
?: shows this screen ?: shows this screen
~: toggles the debug logging pane ~: toggles the debug logging pane
---
=> / Go back

View File

@ -1,6 +1,79 @@
/// This module implements a simple text/gemini parser based on the description /// This module implements a simple text/gemini parser based on the description
/// here: https://gemini.circumlunar.space/docs/specification.html /// here: https://gemini.circumlunar.space/docs/specification.html
use std::io::Write; use std::io::{self, Write};
/// Build a gemini document up from a series of nodes.
#[derive(Default)]
pub struct Builder {
nodes: Vec<Node>,
}
impl Builder {
pub fn new() -> Builder {
Builder::default()
}
pub fn text<T: Into<String>>(mut self, data: T) -> Builder {
self.nodes.push(Node::Text(data.into()));
self
}
pub fn link<T: Into<String>>(mut self, to: T, name: Option<String>) -> Builder {
self.nodes.push(Node::Link {
to: to.into(),
name: name,
});
self
}
pub fn preformatted<T: Into<String>>(mut self, data: T) -> Builder {
self.nodes.push(Node::Preformatted(data.into()));
self
}
pub fn heading<T: Into<String>>(mut self, level: u8, body: T) -> Builder {
self.nodes.push(Node::Heading {
level: level,
body: body.into(),
});
self
}
pub fn list_item<T: Into<String>>(mut self, item: T) -> Builder {
self.nodes.push(Node::ListItem(item.into()));
self
}
pub fn quote<T: Into<String>>(mut self, body: T) -> Builder {
self.nodes.push(Node::Quote(body.into()));
self
}
pub fn build(self) -> Vec<Node> {
self.nodes
}
}
/// Render a set of nodes as a document to a writer.
pub fn render(nodes: Vec<Node>, out: &mut impl Write) -> io::Result<()> {
use Node::*;
for node in nodes {
match node {
Text(body) => write!(out, "{}\n", body)?,
Link{to, name} => match name {
Some(name) => write!(out, "{} {}\n", to, name)?,
None => write!(out, "{}\n", to)?,
},
Preformatted(body) => write!(out, "```\n{}\n```\n", body)?,
Heading { level, body} => write!(out, "{} {}\n", "#".repeat(level as usize), body)?,
ListItem(body) => write!(out, "* {}\n", body)?,
Quote(body) => write!(out, "> {}\n", body)?,
};
}
Ok(())
}
/// Individual nodes of the document. Each node correlates to a line in the file. /// Individual nodes of the document. Each node correlates to a line in the file.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]

View File

@ -1,6 +1,6 @@
use crate::StatusCode; use crate::{gemini, StatusCode};
use num::FromPrimitive; use num::FromPrimitive;
use std::io::{prelude::*, ErrorKind, self}; use std::io::{self, prelude::*, ErrorKind};
/// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html). /// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html).
#[derive(Default)] #[derive(Default)]
@ -10,6 +10,67 @@ pub struct Response {
pub body: Vec<u8>, pub body: Vec<u8>,
} }
impl Response {
pub fn gemini(body: Vec<u8>) -> Response {
Response {
status: StatusCode::Success,
meta: "text/gemini".to_string(),
body: body,
}
}
pub fn render(body: Vec<gemini::Node>) -> Response {
let mut buf: Vec<u8> = vec![];
gemini::render(body, &mut buf).unwrap();
Response {
status: StatusCode::Success,
meta: "text/gemini".to_string(),
body: buf,
}
}
pub fn perm_redirect(to: String) -> Response {
Response {
status: StatusCode::PermanentRedirect,
meta: to,
body: vec![],
}
}
pub fn no_proxy() -> Response {
Response {
status: StatusCode::ProxyRequestRefused,
meta: "Wrong host".to_string(),
body: vec![],
}
}
pub fn not_found() -> Response {
Response {
status: StatusCode::NotFound,
meta: "Not found".to_string(),
body: vec![],
}
}
pub fn input<T: Into<String>>(msg: T) -> Response {
Response {
status: StatusCode::Input,
meta: msg.into(),
body: vec![],
}
}
pub fn need_cert<T: Into<String>>(msg: T) -> Response {
Response {
status: StatusCode::ClientCertificateRequired,
meta: msg.into(),
body: vec![],
}
}
}
/// The parser state. /// The parser state.
#[derive(Debug)] #[derive(Debug)]
enum State { enum State {
@ -103,7 +164,7 @@ impl Response {
} }
_ => { _ => {
if data.len() == 1024 { if data.len() == 1024 {
return Err(Error::ResponseMetaTooLong) return Err(Error::ResponseMetaTooLong);
} }
data.push(buf[0]); data.push(buf[0]);
} }
@ -168,11 +229,13 @@ mod tests {
match Response::parse(&mut fin) { match Response::parse(&mut fin) {
Ok(_) => panic!("wanted error but didn't get one"), Ok(_) => panic!("wanted error but didn't get one"),
Err(why) => if let ResponseError::ResponseMetaTooLong = why { Err(why) => {
if let ResponseError::ResponseMetaTooLong = why {
println!("ok"); println!("ok");
} else { } else {
panic!("wanted ResponseError::ResponseMetaTooLong") panic!("wanted ResponseError::ResponseMetaTooLong")
}, }
}
} }
} }
} }

View File

@ -1,10 +1,14 @@
use crate::{Response, StatusCode}; 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 async_trait::async_trait;
use rustls::{Certificate, Session}; use rustls::Certificate;
use std::{error::Error as StdError, net::SocketAddr, sync::Arc}; 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; use url::Url;
/// A Gemini request and its associated metadata. /// A Gemini request and its associated metadata.
@ -15,6 +19,16 @@ pub struct Request {
} }
pub type Error = Box<dyn StdError + Sync + Send>; pub type Error = Box<dyn StdError + Sync + Send>;
type Result<T = ()> = std::result::Result<T, Error>;
#[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)] #[allow(dead_code, unused_assignments, unused_mut, unused_variables)]
mod routes; mod routes;
@ -22,132 +36,132 @@ pub use routes::*;
#[async_trait] #[async_trait]
pub trait Handler { pub trait Handler {
async fn handle(&self, r: Request) -> Result<Response, Error>; async fn handle(&self, r: Request) -> Result<Response>;
} }
pub async fn serve( pub async fn serve(
h: &(dyn Handler + Sync), h: Arc<dyn Handler + Send + Sync>,
cfg: rustls::ServerConfig, cfg: rustls::ServerConfig,
host: String, host: String,
port: u16, port: u16,
) -> Result<(), Error> ) -> Result
where where
{ {
let cfg = Arc::new(cfg); 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 mut incoming = listener.incoming();
let acceptor = TlsAcceptor::from(cfg.clone()); let acceptor = Arc::new(TlsAcceptor::from(cfg.clone()));
while let Some(Ok(stream)) = incoming.next().await {
while let Some(stream) = incoming.next().await { let h = h.clone();
let stream = stream?;
let addr = stream.peer_addr().unwrap();
let fut = async {
let acceptor = acceptor.clone(); let acceptor = acceptor.clone();
let result = acceptor.accept(stream).await; let addr = stream.peer_addr().unwrap();
let port = port.clone();
if result.is_err() { task::spawn(handle_request(h, stream, acceptor, addr, port));
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);
} }
Ok(()) Ok(())
} }
async fn handle<T>(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<TlsAcceptor>,
addr: SocketAddr,
port: u16,
) -> Result {
// Perform handshake.
let mut stream = acceptor.clone().accept(stream).await?;
match parse_request(&mut stream).await {
Ok(url) => {
if let Some(u_port) = url.port() {
if port != u_port {
let _ = respond(&mut stream, "53", &["Cannot proxy"]).await;
return Ok(());
}
}
if url.scheme() != "gemini" {
let _ = respond(&mut stream, "53", &["Cannot proxy outside geminispace"]).await;
Err(RequestParsingError::InvalidScheme(url.scheme().to_string()))?
}
let req = Request {
url: url,
certs: stream.peer_certificates(),
};
handle(h, req, &mut stream, addr).await;
}
Err(e) => {
respond(&mut stream, "59", &["Invalid request."]).await?;
log::error!("error from {}: {:?}", addr, e);
}
}
Ok(())
}
async fn respond<W: Write + Unpin>(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<R: Read + Unpin>(mut stream: R) -> Result<Url> {
// 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.
Ok(url)
}
async fn handle<T>(h: Arc<(dyn Handler + Send + Sync)>, req: Request, stream: &mut T, addr: SocketAddr)
where where
T: AsyncWriteExt + Unpin, T: Write + Unpin,
{ {
let u = req.url.clone(); let u = req.url.clone();
match h.handle(req).await { match h.handle(req).await {
Ok(resp) => { Ok(resp) => {
stream let _ = stream
.write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes()) .write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes())
.await .await;
.unwrap(); let _ = stream.write(&resp.body).await;
stream.write(&resp.body).await.unwrap(); log::info!("{}: {} {} {:?}", addr, u, resp.meta, resp.status);
log::info!("{}: {} {:?}", addr, u, resp.status);
} }
Err(why) => { Err(why) => {
stream let _ = stream
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes()) .write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes())
.await .await;
.unwrap();
log::error!("{}: {}: {:?}", addr, u, why); log::error!("{}: {}: {:?}", addr, u, why);
} }
}; };