Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
panicbit committed Oct 31, 2020
0 parents commit acef45c
Show file tree
Hide file tree
Showing 7 changed files with 664 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target
Cargo.lock
/cert/
/public/
18 changes: 18 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "northstar"
version = "0.1.0"
authors = ["panicbit <[email protected]>"]
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 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions examples/serve_dir.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 199 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)
}


Loading

0 comments on commit acef45c

Please sign in to comment.