maj-async-std #4
36
Cargo.toml
36
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 = [
|
||||
|
|
|
@ -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"] }
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use tokio_rustls::rustls;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn config() -> rustls::ClientConfig {
|
||||
|
|
|
@ -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 = ".." }
|
|
@ -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<Vec<PrivateKey>> {
|
|||
.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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
<esc>: 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
|
||||
|
|
|
@ -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<T>(u: T, cfg: tokio_rustls::rustls::ClientConfig) -> Result<crate::Response, Error>
|
||||
pub async fn get<T>(u: T, cfg: rustls::ClientConfig) -> Result<crate::Response, Error>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
|
@ -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
|
||||
|
|
|
@ -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<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)]
|
||||
mod routes;
|
||||
|
@ -22,116 +36,107 @@ pub use routes::*;
|
|||
|
||||
#[async_trait]
|
||||
pub trait Handler {
|
||||
async fn handle(&self, r: Request) -> Result<Response, Error>;
|
||||
async fn handle(&self, r: Request) -> Result<Response>;
|
||||
}
|
||||
|
||||
pub async fn serve(
|
||||
h: &(dyn Handler + Sync),
|
||||
h: Arc<dyn Handler + Send + Sync>,
|
||||
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 addr = stream.peer_addr().unwrap();
|
||||
let fut = async {
|
||||
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 result = acceptor.accept(stream).await;
|
||||
let addr = stream.peer_addr().unwrap();
|
||||
|
||||
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<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,
|
||||
) -> 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<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.
|
||||
if url.scheme() != "gemini" {
|
||||
Err(RequestParsingError::InvalidScheme(url.scheme().to_string()))?
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
async fn handle<T>(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
|
||||
|
|
Loading…
Reference in New Issue