forked from cadey/maj
Merge pull request 'maj-async-std' (#4) from maj-async-std into main
Reviewed-on: cadey/maj#4
This commit is contained in:
commit
cd3cd13cd2
|
@ -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
|
||||||
|
|
39
Cargo.toml
39
Cargo.toml
|
@ -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 = [
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }
|
105
site/src/main.rs
105
site/src/main.rs
|
@ -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(),
|
route!(req.url.path(), {
|
||||||
_ => Ok(maj::Response {
|
(/) => index().await;
|
||||||
status: maj::StatusCode::NotFound,
|
(/"cert") => need_cert(req).await;
|
||||||
meta: "Not found".to_string(),
|
(/"majc") => majc().await;
|
||||||
body: vec![],
|
});
|
||||||
}),
|
|
||||||
}
|
Ok(Response::not_found())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# majc
|
|
||||||
```
|
```
|
||||||
__
|
__
|
||||||
_____ _____ |__| ____
|
_____ _____ |__| ____
|
||||||
|
@ -7,33 +6,19 @@
|
||||||
|__|_| /(____ //\__| | \___ >
|
|__|_| /(____ //\__| | \___ >
|
||||||
\/ \/ \______| \/
|
\/ \/ \______| \/
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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")
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue