-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit acef45c
Showing
7 changed files
with
664 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/target | ||
Cargo.lock | ||
/cert/ | ||
/public/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
|
Oops, something went wrong.