add majsite server example
This commit is contained in:
parent
a0c8ea4f90
commit
bd1d37548a
20
Cargo.toml
20
Cargo.toml
|
@ -7,24 +7,38 @@ 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]
|
||||||
|
async-trait = { version = "0", optional = true }
|
||||||
num = "0.2"
|
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"] }
|
||||||
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 }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
url = "2"
|
url = "2"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
structopt = "0.3"
|
||||||
[dev-dependencies]
|
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
version = "0.2.0"
|
||||||
|
features = [
|
||||||
|
"macros",
|
||||||
|
"net",
|
||||||
|
"tcp",
|
||||||
|
"io-util",
|
||||||
|
"rt-core",
|
||||||
|
"time",
|
||||||
|
"stream"
|
||||||
|
]
|
||||||
|
optional = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["client", "server"]
|
default = ["client", "server"]
|
||||||
|
|
||||||
client = ["rustls", "webpki", "webpki-roots"]
|
client = ["rustls", "webpki", "webpki-roots"]
|
||||||
server = ["rustls", "webpki", "webpki-roots"]
|
server = ["rustls", "webpki", "webpki-roots", "tokio", "async-trait", "tokio-rustls"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, BufReader};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
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 {
|
||||||
|
/// host to listen on
|
||||||
|
#[structopt(short = "H", long, env = "HOST", default_value = "0.0.0.0")]
|
||||||
|
host: String,
|
||||||
|
|
||||||
|
/// port to listen on
|
||||||
|
#[structopt(short = "p", long, env = "PORT", default_value = "1965")]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
/// cert file
|
||||||
|
#[structopt(short = "c", long = "cert", env = "CERT_FILE")]
|
||||||
|
cert: PathBuf,
|
||||||
|
|
||||||
|
/// key file
|
||||||
|
#[structopt(short = "k", long = "key", env = "KEY_FILE")]
|
||||||
|
key: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
|
||||||
|
certs(&mut BufReader::new(File::open(path)?))
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_keys(path: &Path) -> io::Result<Vec<PrivateKey>> {
|
||||||
|
rsa_private_keys(&mut BufReader::new(File::open(path)?))
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), maj::server::Error> {
|
||||||
|
pretty_env_logger::init();
|
||||||
|
let opts = Options::from_args();
|
||||||
|
let certs = load_certs(&opts.cert)?;
|
||||||
|
let mut keys = load_keys(&opts.key)?;
|
||||||
|
|
||||||
|
let mut config = ServerConfig::new(NoClientAuth::new());
|
||||||
|
config
|
||||||
|
.set_single_cert(certs, keys.remove(0))
|
||||||
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||||
|
|
||||||
|
maj::server::serve(Handler{}, config, opts.host, opts.port).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Handler {}
|
||||||
|
|
||||||
|
fn index() -> Result<maj::Response, maj::server::Error> {
|
||||||
|
let msg = include_bytes!("../../majc/src/help.gmi");
|
||||||
|
|
||||||
|
Ok(maj::Response {
|
||||||
|
status: maj::StatusCode::Success,
|
||||||
|
meta: "text/gemini".to_string(),
|
||||||
|
body: msg.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl maj::server::Handler for Handler {
|
||||||
|
async fn handle(&self, r: maj::server::Request) -> Result<maj::Response, maj::server::Error> {
|
||||||
|
match r.url.path() {
|
||||||
|
"/" | "" => index(),
|
||||||
|
_ => Ok(maj::Response {
|
||||||
|
status: maj::StatusCode::NotFound,
|
||||||
|
meta: format!("{} not found", r.url),
|
||||||
|
body: vec![],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,4 @@ mod client;
|
||||||
pub use client::{get, Error as ClientError};
|
pub use client::{get, Error as ClientError};
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
mod server;
|
pub mod server;
|
||||||
#[cfg(feature = "server")]
|
|
||||||
pub use server::{serve, serve_plain, Error as AnyError, Handler};
|
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
use crate::Response;
|
use crate::{Response, StatusCode};
|
||||||
use std::{error::Error as StdError, io};
|
use async_trait::async_trait;
|
||||||
|
use rustls::{Certificate, Session};
|
||||||
|
use std::{error::Error as StdError, 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.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
url: Url,
|
pub url: Url,
|
||||||
|
pub certs: Option<Vec<Certificate>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Error = Box<dyn StdError + Sync + Send>;
|
pub type Error = Box<dyn StdError + Sync + Send>;
|
||||||
|
@ -13,14 +20,103 @@ pub type Error = Box<dyn StdError + Sync + Send>;
|
||||||
mod routes;
|
mod routes;
|
||||||
pub use routes::*;
|
pub use routes::*;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait Handler {
|
pub trait Handler {
|
||||||
fn handle(r: Request) -> Result<Response, Error>;
|
async fn handle(&self, r: Request) -> Result<Response, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve(_h: impl Handler, _port: u16) -> io::Result<()> {
|
pub async fn serve<T>(h: T, cfg: rustls::ServerConfig, host: String, port: u16) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
T: Handler,
|
||||||
|
{
|
||||||
|
let cfg = Arc::new(cfg);
|
||||||
|
let mut 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 acceptor = acceptor.clone();
|
||||||
|
let mut stream = acceptor.accept(stream).await?;
|
||||||
|
let mut rd = BufReader::new(&mut stream);
|
||||||
|
let mut u = String::new();
|
||||||
|
rd.read_line(&mut u).await?;
|
||||||
|
if u.len() > 1025 {
|
||||||
|
stream
|
||||||
|
.write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes())
|
||||||
|
.await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let u = Url::parse(&u)?;
|
||||||
|
match h
|
||||||
|
.handle(Request {
|
||||||
|
url: u.clone(),
|
||||||
|
certs: stream.get_ref().1.get_peer_certificates(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resp) => {
|
||||||
|
stream
|
||||||
|
.write(format!("{} {}\r\n", resp.status as u8, resp.meta).as_bytes())
|
||||||
|
.await?;
|
||||||
|
stream.write(&resp.body).await?;
|
||||||
|
log::info!("{}: {} {:?}", addr, u, resp.status);
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
stream
|
||||||
|
.write(format!("{} {:?}\r\n", StatusCode::PermanentFailure as u8, why).as_bytes())
|
||||||
|
.await?;
|
||||||
|
log::error!("{}: {}: {:?}", addr, u, why);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve_plain(_h: impl Handler, _port: u16) -> io::Result<()> {
|
pub async fn serve_plain<T>(h: T, host: String, port: u16) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
T: Handler,
|
||||||
|
{
|
||||||
|
let mut listener = TcpListener::bind(&format!("{}:{}", host, port)).await?;
|
||||||
|
let mut incoming = listener.incoming();
|
||||||
|
while let Some(stream) = incoming.next().await {
|
||||||
|
let mut stream = stream?;
|
||||||
|
let mut rd = BufReader::new(&mut stream);
|
||||||
|
let mut u = String::new();
|
||||||
|
rd.read_line(&mut u).await?;
|
||||||
|
if u.len() > 1025 {
|
||||||
|
stream
|
||||||
|
.write(format!("{} URL too long", StatusCode::BadRequest as u8).as_bytes())
|
||||||
|
.await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let u = Url::parse(&u)?;
|
||||||
|
match h
|
||||||
|
.handle(Request {
|
||||||
|
url: u.clone(),
|
||||||
|
certs: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resp) => {
|
||||||
|
stream
|
||||||
|
.write(format!("{} {}", resp.status as u8, resp.meta).as_bytes())
|
||||||
|
.await?;
|
||||||
|
stream.write(&resp.body).await?;
|
||||||
|
log::info!("{}: {} {:?}", stream.peer_addr().unwrap(), u, resp.status);
|
||||||
|
}
|
||||||
|
Err(why) => {
|
||||||
|
stream
|
||||||
|
.write(format!("{} {:?}", StatusCode::PermanentFailure as u8, why).as_bytes())
|
||||||
|
.await?;
|
||||||
|
log::error!("{}: {}: {:?}", stream.peer_addr().unwrap(), u, why);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue