This commit is contained in:
panicbit 2020-10-31 20:53:03 +01:00
commit acef45c75c
7 changed files with 664 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
Cargo.lock
/cert/
/public/

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "northstar"
version = "0.1.0"
authors = ["panicbit <panicbit.dev@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.33"
tokio-rustls = "0.20.0"
tokio = { version = "0.3.1", features = ["full"] }
mime = "0.3.16"
uriparse = "0.6.3"
percent-encoding = "2.1.0"
futures = "0.3.7"
itertools = "0.9.0"
log = "0.4.11"

33
README.md Normal file
View file

@ -0,0 +1,33 @@
```
__ __ __
____ ____ _____/ /_/ /_ _____/ /_____ ______
/ __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/
/ / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / /
/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/
```
- [Documentation](https://docs.rs/northstar)
- [GitHub](https://github.com/panicbit/northstar)
# Usage
Add the latest version of northstar to your `Cargo.toml`.
## Manually
```toml
northstar = "0.1.0" # check crates.io for the latest version
```
## Automatically
```sh
cargo add northstar
```
# Generating a key & certificate
```sh
mkdir cert && cd cert
openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
```

20
examples/serve_dir.rs Normal file
View file

@ -0,0 +1,20 @@
use anyhow::*;
use futures::{future::BoxFuture, FutureExt};
use northstar::{Server, Request, Response, GEMINI_PORT};
#[tokio::main]
async fn main() -> Result<()> {
Server::bind(("localhost", GEMINI_PORT))
.serve(handle_request)
.await
}
fn handle_request(request: Request) -> BoxFuture<'static, Result<Response>> {
async move {
let path = request.path_segments();
let response = northstar::util::serve_dir("public", &path).await?;
Ok(response)
}
.boxed()
}

199
src/lib.rs Normal file
View file

@ -0,0 +1,199 @@
#[macro_use] extern crate log;
use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc};
use futures::{future::BoxFuture, FutureExt};
use mime::Mime;
use tokio::{
prelude::*,
io::{self, BufStream},
net::{TcpStream, ToSocketAddrs},
};
use tokio::net::TcpListener;
use tokio_rustls::{rustls, TlsAcceptor};
use rustls::*;
use anyhow::*;
use uri::URIReference;
pub mod types;
pub mod util;
pub use mime;
pub use uriparse as uri;
pub use types::*;
pub const REQUEST_URI_MAX_LEN: usize = 1024;
pub const GEMINI_PORT: u16 = 1965;
type Handler = Arc<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
type HandlerResponse = BoxFuture<'static, Result<Response>>;
#[derive(Clone)]
pub struct Server {
tls_acceptor: TlsAcceptor,
listener: Arc<TcpListener>,
handler: Handler,
}
impl Server {
pub fn bind<A: ToSocketAddrs>(addr: A) -> Builder<A> {
Builder::bind(addr)
}
async fn serve(self) -> Result<()> {
loop {
let (stream, _addr) = self.listener.accept().await?;
let this = self.clone();
tokio::spawn(async move {
if let Err(err) = this.serve_client(stream).await {
error!("{}", err);
}
});
}
}
async fn serve_client(self, stream: TcpStream) -> Result<()> {
let stream = self.tls_acceptor.accept(stream).await?;
let mut stream = BufStream::new(stream);
let request = receive_request(&mut stream).await?;
debug!("Client requested: {}", request.uri());
let handler = (self.handler)(request);
let handler = AssertUnwindSafe(handler);
let response = handler.catch_unwind().await
.unwrap_or_else(|_| Response::server_error(""))
.or_else(|err| {
error!("Handler: {}", err);
Response::server_error("")
})?;
send_response(response, &mut stream).await?;
stream.flush().await?;
Ok(())
}
}
pub struct Builder<A> {
addr: A,
}
impl<A: ToSocketAddrs> Builder<A> {
fn bind(addr: A) -> Self {
Self { addr }
}
pub async fn serve<F>(self, handler: F) -> Result<()>
where
F: Fn(Request) -> HandlerResponse + Send + Sync + 'static,
{
let config = tls_config()?;
let server = Server {
tls_acceptor: TlsAcceptor::from(config),
listener: Arc::new(TcpListener::bind(self.addr).await?),
handler: Arc::new(handler),
};
server.serve().await
}
}
async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result<Request> {
let limit = REQUEST_URI_MAX_LEN + "\r\n".len();
let mut stream = stream.take(limit as u64);
let mut uri = Vec::new();
stream.read_until(b'\n', &mut uri).await?;
if !uri.ends_with(b"\r\n") {
if uri.len() < REQUEST_URI_MAX_LEN {
bail!("Request header not terminated with CRLF")
} else {
bail!("Request URI too long")
}
}
// Strip CRLF
uri.pop();
uri.pop();
let uri = URIReference::try_from(&*uri)?.into_owned();
let request = Request::from_uri(uri)?;
Ok(request)
}
async fn send_response(mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
send_response_header(response.header(), stream).await?;
if let Some(body) = response.take_body() {
send_response_body(body, stream).await?;
}
Ok(())
}
async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
let header = format!(
"{status} {meta}\r\n",
status = header.status.code(),
meta = header.meta.as_str(),
);
stream.write_all(header.as_bytes()).await?;
Ok(())
}
async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
match body {
Body::Bytes(bytes) => stream.write_all(&bytes).await?,
Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; },
}
Ok(())
}
fn tls_config() -> Result<Arc<ServerConfig>> {
let mut config = ServerConfig::new(NoClientAuth::new());
let cert_chain = load_cert_chain()?;
let key = load_key()?;
config.set_single_cert(cert_chain, key)?;
Ok(config.into())
}
fn load_cert_chain() -> Result<Vec<Certificate>> {
let certs = std::fs::File::open("cert/cert.pem")?;
let mut certs = BufReader::new(certs);
let certs = rustls::internal::pemfile::certs(&mut certs)
.map_err(|_| anyhow!("failed to load certs"))?;
Ok(certs)
}
fn load_key() -> Result<PrivateKey> {
let mut keys = BufReader::new(std::fs::File::open("cert/key.pem")?);
let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys)
.map_err(|_| anyhow!("failed to load key"))?;
ensure!(!keys.is_empty(), "no key found");
let key = keys.swap_remove(0);
Ok(key)
}
const GEMINI_MIME: &str = "text/gemini";
pub fn gemini_mime() -> Result<Mime> {
let mime = GEMINI_MIME.parse()?;
Ok(mime)
}

287
src/types.rs Normal file
View file

@ -0,0 +1,287 @@
use std::ops;
use anyhow::*;
use mime::Mime;
use percent_encoding::percent_decode_str;
use tokio::{io::AsyncRead, fs::File};
use uriparse::URIReference;
pub struct Request {
uri: URIReference<'static>,
input: Option<String>,
}
impl Request {
pub fn from_uri(mut uri: URIReference<'static>) -> Result<Self> {
uri.normalize();
let input = match uri.query() {
None => None,
Some(query) => {
let input = percent_decode_str(query.as_str())
.decode_utf8()?
.into_owned();
Some(input)
}
};
Ok(Self {
uri,
input,
})
}
pub fn uri(&self) -> &URIReference {
&self.uri
}
pub fn path_segments(&self) -> Vec<String> {
self.uri()
.path()
.segments()
.iter()
.map(|segment| percent_decode_str(segment.as_str()).decode_utf8_lossy().into_owned())
.collect::<Vec<String>>()
}
pub fn input(&self) -> Option<&str> {
self.input.as_deref()
}
}
impl ops::Deref for Request {
type Target = URIReference<'static>;
fn deref(&self) -> &Self::Target {
&self.uri
}
}
#[derive(Debug,Clone)]
pub struct ResponseHeader {
pub status: Status,
pub meta: Meta,
}
impl ResponseHeader {
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
Ok(Self {
status: Status::INPUT,
meta: Meta::new(prompt)?,
})
}
pub fn success(mime: &Mime) -> Result<Self> {
Ok(Self {
status: Status::SUCCESS,
meta: Meta::new(mime.to_string())?,
})
}
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
Ok(Self {
status: Status::PERMANENT_FAILURE,
meta: Meta::new(reason)?,
})
}
pub fn not_found() -> Result<Self> {
Ok(Self {
status: Status::NOT_FOUND,
meta: Meta::new("Not found")?,
})
}
pub fn status(&self) -> &Status {
&self.status
}
pub fn meta(&self) -> &Meta {
&self.meta
}
}
#[derive(Debug,Copy,Clone,PartialEq,Eq)]
pub struct Status(u8);
impl Status {
pub const INPUT: Self = Self(10);
pub const SENSITIVE_INPUT: Self = Self(11);
pub const SUCCESS: Self = Self(20);
pub const REDIRECT_TEMPORARY: Self = Self(30);
pub const REDIRECT_PERMANENT: Self = Self(31);
pub const TEMPORARY_FAILURE: Self = Self(40);
pub const SERVER_UNAVAILABLE: Self = Self(41);
pub const CGI_ERROR: Self = Self(42);
pub const PROXY_ERROR: Self = Self(43);
pub const SLOW_DOWN: Self = Self(44);
pub const PERMANENT_FAILURE: Self = Self(50);
pub const NOT_FOUND: Self = Self(51);
pub const GONE: Self = Self(52);
pub const PROXY_REQUEST_REFUSED: Self = Self(53);
pub const BAD_REQUEST: Self = Self(59);
pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60);
pub fn code(&self) -> u8 {
self.0
}
pub fn is_success(&self) -> bool {
self.category().is_success()
}
pub fn category(&self) -> StatusCategory {
let class = self.0 / 10;
match class {
1 => StatusCategory::Input,
2 => StatusCategory::Success,
3 => StatusCategory::Redirect,
4 => StatusCategory::TemporaryFailure,
5 => StatusCategory::PermanentFailure,
6 => StatusCategory::ClientCertificateRequired,
_ => StatusCategory::PermanentFailure,
}
}
}
#[derive(Copy,Clone,PartialEq,Eq)]
pub enum StatusCategory {
Input,
Success,
Redirect,
TemporaryFailure,
PermanentFailure,
ClientCertificateRequired,
}
impl StatusCategory {
pub fn is_input(&self) -> bool {
*self == Self::Input
}
pub fn is_success(&self) -> bool {
*self == Self::Success
}
pub fn redirect(&self) -> bool {
*self == Self::Redirect
}
pub fn is_temporary_failure(&self) -> bool {
*self == Self::TemporaryFailure
}
pub fn is_permanent_failure(&self) -> bool {
*self == Self::PermanentFailure
}
pub fn is_client_certificate_required(&self) -> bool {
*self == Self::ClientCertificateRequired
}
}
#[derive(Debug,Clone,PartialEq,Eq,Default)]
pub struct Meta(String);
impl Meta {
pub fn new(meta: impl AsRef<str> + Into<String>) -> Result<Self> {
ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines");
Ok(Self(meta.into()))
}
pub fn empty() -> Self {
Self::default()
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_mime(&self) -> Result<Mime> {
let mime = self.as_str().parse::<Mime>()?;
Ok(mime)
}
}
pub struct Response {
header: ResponseHeader,
body: Option<Body>,
}
impl Response {
pub fn new(header: ResponseHeader) -> Self {
Self {
header,
body: None,
}
}
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
let header = ResponseHeader::input(prompt)?;
Ok(Self::new(header))
}
pub fn success(mime: &Mime) -> Result<Self> {
let header = ResponseHeader::success(&mime)?;
Ok(Self::new(header))
}
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
let header = ResponseHeader::server_error(reason)?;
Ok(Self::new(header))
}
pub fn not_found() -> Result<Self> {
let header = ResponseHeader::not_found()?;
Ok(Self::new(header))
}
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
self.body = Some(body.into());
self
}
pub fn header(&self) -> &ResponseHeader {
&self.header
}
pub fn take_body(&mut self) -> Option<Body> {
self.body.take()
}
}
pub enum Body {
Bytes(Vec<u8>),
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
}
impl From<Vec<u8>> for Body {
fn from(bytes: Vec<u8>) -> Self {
Self::Bytes(bytes)
}
}
impl<'a> From<&'a [u8]> for Body {
fn from(bytes: &[u8]) -> Self {
Self::Bytes(bytes.to_owned())
}
}
impl From<String> for Body {
fn from(text: String) -> Self {
Self::Bytes(text.into_bytes())
}
}
impl<'a> From<&'a str> for Body {
fn from(text: &str) -> Self {
Self::Bytes(text.to_owned().into_bytes())
}
}
impl From<File> for Body {
fn from(file: File) -> Self {
Self::Reader(Box::new(file))
}
}

103
src/util.rs Normal file
View file

@ -0,0 +1,103 @@
use std::path::Path;
use mime::Mime;
use percent_encoding::utf8_percent_encode;
use anyhow::*;
use tokio::{
fs::{self, File},
io,
};
use crate::{GEMINI_MIME, Response, gemini_mime};
use itertools::Itertools;
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response> {
let path = path.as_ref();
let file = match File::open(path).await {
Ok(file) => file,
Err(err) => match err.kind() {
io::ErrorKind::NotFound => return Ok(Response::not_found()?),
_ => return Err(err.into()),
}
};
Ok(Response::success(&mime)?.with_body(file))
}
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Result<Response> {
debug!("Dir: {}", dir.as_ref().display());
let dir = dir.as_ref().canonicalize()?;
let mut path = dir.to_path_buf();
for segment in virtual_path {
path.push(segment);
}
let path = path.canonicalize()?;
if !path.starts_with(&dir) {
return Ok(Response::not_found()?);
}
if !path.is_dir() {
let mime = guess_mime_from_path(&path);
return serve_file(path, &mime).await;
}
serve_dir_listing(path, virtual_path).await
}
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
use std::fmt::Write;
let mut dir = match fs::read_dir(path).await {
Ok(dir) => dir,
Err(err) => match err.kind() {
io::ErrorKind::NotFound => return Ok(Response::not_found()?),
_ => return Err(err.into()),
}
};
let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/");
let mut listing = String::new();
writeln!(listing, "# Index of /{}", breadcrumbs)?;
writeln!(listing)?;
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
writeln!(listing, "=> .. 📁 ../")?;
}
while let Some(entry) = dir.next_entry().await? {
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
let is_dir = entry.file_type().await?.is_dir();
writeln!(
listing,
"=> {link}{trailing_slash} {icon} {name}{trailing_slash}",
icon = if is_dir { '📁' } else { '📄' },
link = utf8_percent_encode(&file_name, percent_encoding::NON_ALPHANUMERIC),
trailing_slash = if is_dir { "/" } else { "" },
name = file_name,
)?;
}
Ok(Response::success(&gemini_mime()?)?.with_body(listing))
}
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
let path = path.as_ref();
let extension = path.extension().and_then(|s| s.to_str());
let mime = match extension {
Some(extension) => match extension {
"gemini" => GEMINI_MIME,
"txt" => "text/plain",
"jpeg" | "jpg" | "jpe" => "image/jpeg",
"png" => "image/png",
_ => "application/octet-stream",
},
None => "application/octet-stream",
};
mime.parse::<Mime>().unwrap_or(mime::APPLICATION_OCTET_STREAM)
}