#![feature(try_trait_v2)] pub mod config; pub mod error; pub mod files; pub mod proxy; use crate::{ config::{Config, HostConfig}, files::serve_files, proxy::proxy_request, }; use anyhow::{anyhow, bail, Context, Result}; use error::ServiceError; use http_body_util::{combinators::BoxBody, BodyExt}; use hyper::{ body::Incoming, header::{CONTENT_TYPE, HOST, SERVER}, http::HeaderValue, server::conn::http1, service::service_fn, Request, Response, StatusCode, }; use log::{debug, info, warn}; use std::{fs::File, io::BufReader, net::SocketAddr, path::Path, sync::Arc}; use tokio::{ io::{AsyncRead, AsyncWrite}, net::TcpListener, signal::ctrl_c, }; use tokio_rustls::TlsAcceptor; #[tokio::main] async fn main() -> anyhow::Result<()> { env_logger::init_from_env("LOG"); let config_path = std::env::args().skip(1).next().ok_or(anyhow!( "first argument is expected to be the configuration file" ))?; let config = Arc::new(Config::load(&config_path)?); tokio::spawn(serve_http(config.clone())); tokio::spawn(serve_https(config.clone())); ctrl_c().await.unwrap(); Ok(()) } async fn serve_http(config: Arc) -> Result<()> { let http_config = match &config.http { Some(n) => n, None => return Ok(()), }; let listener = TcpListener::bind(http_config.bind).await?; info!("serving http"); loop { let (stream, addr) = listener.accept().await.context("accepting connection")?; debug!("connection from {addr}"); let config = config.clone(); tokio::spawn(async move { serve_stream(config, stream, addr).await }); } } async fn serve_https(config: Arc) -> Result<()> { let https_config = match &config.https { Some(n) => n, None => return Ok(()), }; let tls_config = { let certs = load_certs(&https_config.tls_cert)?; let key = load_private_key(&https_config.tls_key)?; let mut cfg = rustls::ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(certs, key)?; cfg.alpn_protocols = vec![ //b"h2".to_vec(), b"http/1.1".to_vec(), ]; Arc::new(cfg) }; let listener = TcpListener::bind(https_config.bind).await?; let tls_acceptor = Arc::new(TlsAcceptor::from(tls_config)); info!("serving https"); loop { let (stream, addr) = listener.accept().await.context("accepting connection")?; let config = config.clone(); let tls_acceptor = tls_acceptor.clone(); tokio::task::spawn(async move { debug!("connection from {addr}"); match tls_acceptor.accept(stream).await { Ok(stream) => serve_stream(config, stream, addr).await, Err(e) => warn!("error accepting tls: {e}"), }; }); } } pub async fn serve_stream( config: Arc, stream: T, addr: SocketAddr, ) { let conn = http1::Builder::new() .serve_connection( stream, service_fn(move |req| { let config = config.clone(); async move { match service(config, req, addr).await { Ok(r) => Ok(r), Err(ServiceError::Hyper(e)) => Err(e), Err(error) => Ok({ let mut resp = Response::new(format!("gnix encountered an issue: {error}")); *resp.status_mut() = StatusCode::BAD_REQUEST; resp.headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); resp } .map(|b| b.map_err(|e| match e {}).boxed())), } } }), ) .with_upgrades(); if let Err(err) = conn.await { warn!("error: {:?}", err); } } fn load_certs(path: &Path) -> anyhow::Result> { let mut reader = BufReader::new(File::open(path).context("reading tls certs")?); let certs = rustls_pemfile::certs(&mut reader).context("parsing tls certs")?; Ok(certs.into_iter().map(rustls::Certificate).collect()) } fn load_private_key(path: &Path) -> anyhow::Result { let mut reader = BufReader::new(File::open(path).context("reading tls private key")?); let keys = rustls_pemfile::pkcs8_private_keys(&mut reader).context("parsing tls private key")?; if keys.len() != 1 { bail!("expected a single private key, found {}", keys.len()) } Ok(rustls::PrivateKey(keys[0].clone())) } async fn service( config: Arc, req: Request, addr: SocketAddr, ) -> Result>, ServiceError> { debug!("{addr} ~> {:?} {}", req.headers().get(HOST), req.uri()); let route = config .hosts .get(remove_port( &req.headers() .get(HOST) .and_then(|e| e.to_str().ok()) .map(String::from) .unwrap_or(String::from("")), )) .ok_or(ServiceError::NoHost)?; let mut resp = match route { HostConfig::Backend { backend } => proxy_request(req, addr, backend).await, HostConfig::Files { files } => serve_files(req, files).await, }?; let server_header = resp.headers().get(SERVER).cloned(); resp.headers_mut().insert( SERVER, HeaderValue::from_str(&if let Some(o) = server_header { format!("{} via gnix", o.to_str().ok().unwrap_or("invalid")) } else { format!("gnix") }) .unwrap(), ); return Ok(resp); } pub fn remove_port(s: &str) -> &str { s.split_once(":").map(|(s, _)| s).unwrap_or(s) }