This commit is contained in:
hexlocation 2025-08-10 15:44:37 +02:00
parent 7757ef32f4
commit 663d508093
6 changed files with 187 additions and 53 deletions

View file

@ -5,6 +5,7 @@ use tokio_postgres::{Client, Row};
const ENDPOINT_TABLE: &str = "endpoints"; const ENDPOINT_TABLE: &str = "endpoints";
const HOSTS_RELATION_TABLE: &str = "hosts"; const HOSTS_RELATION_TABLE: &str = "hosts";
const CERTIFICATES_TABLE: &str = "certs";
#[derive(Debug)] #[derive(Debug)]
pub struct BoxyDatabase { pub struct BoxyDatabase {
@ -18,6 +19,86 @@ pub struct Endpoint {
pub callback: String, pub callback: String,
} }
pub struct Certificate {
pub hostname: String,
pub cert_data: Vec<u8>,
pub key_data: Vec<u8>,
}
impl Certificate {
pub async fn new(hostname: String, cert_data: Vec<u8>, key_data: Vec<u8>) -> Self {
Self {
hostname,
cert_data,
key_data,
}
}
pub async fn get_by_hostname(
db: &BoxyDatabase,
hostname: String,
) -> Result<Self, Box<dyn Error>> {
let row = db
.client
.query_one(
format!(
"SELECT * FROM {HOSTS_RELATION_TABLE}
WHERE hostname = $1"
)
.as_str(),
&[&hostname],
)
.await?;
Ok(row.into())
}
pub async fn get_all(db: &BoxyDatabase) -> Result<Vec<Self>, Box<dyn Error>> {
let mut result: Vec<Self> = Vec::new();
let rows = db
.client
.query(format!("SELECT * FROM {CERTIFICATES_TABLE}").as_str(), &[])
.await?;
for row in rows {
result.push(row.into());
}
Ok(result)
}
}
impl Certificate {
pub async fn delete(self, db: &mut BoxyDatabase) -> Result<(), tokio_postgres::Error> {
let tx = db.client.transaction().await?;
tx.execute(
format!(
"DELETE FROM {CERTIFICATES_TABLE}
WHERE hostname = $1"
)
.as_str(),
&[&self.hostname],
)
.await?;
tx.commit().await?;
warn!("Removed certificate for host {}", self.hostname);
Ok(())
}
}
impl From<Row> for Certificate {
fn from(value: Row) -> Self {
Self {
hostname: value.get("hostname"),
cert_data: value.get("certificate"),
key_data: value.get("key"),
}
}
}
impl Endpoint { impl Endpoint {
pub async fn new(id: Option<u32>, address: IpAddr, port: u16, callback: String) -> Self { pub async fn new(id: Option<u32>, address: IpAddr, port: u16, callback: String) -> Self {
Self { Self {
@ -119,7 +200,7 @@ impl Endpoint {
let id = self.id.unwrap() as i32; let id = self.id.unwrap() as i32;
tx.execute( tx.execute(
format!("DELETE FROM {ENDPOINT_TABLE} where id = $1").as_str(), format!("DELETE FROM {ENDPOINT_TABLE} WHERE id = $1").as_str(),
&[&id], &[&id],
) )
.await?; .await?;
@ -205,6 +286,21 @@ impl BoxyDatabase {
) )
.await?; .await?;
c.execute(
format!(
"CREATE TABLE IF NOT EXISTS {CERTIFICATES_TABLE}
(
hostname text PRIMARY KEY,
certificate bytea,
key bytea
)
"
)
.as_str(),
&[],
)
.await?;
Ok(BoxyDatabase { client: c }) Ok(BoxyDatabase { client: c })
} }
} }

View file

@ -16,7 +16,7 @@ use log::{debug, error, info};
use matchers::api::ApiMatcher; use matchers::api::ApiMatcher;
use server::Server; use server::Server;
use services::{controller::ControllerService, matcher::Matcher}; use services::{controller::ControllerService, matcher::Matcher};
use tls::TlsOption; use tls::{TlsManager, TlsOption};
use tokio::{ use tokio::{
sync::Mutex, sync::Mutex,
time::{self}, time::{self},
@ -72,6 +72,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}); });
info!("Connected to database."); info!("Connected to database.");
let database = Box::new(BoxyDatabase::new(client).await.unwrap()); let database = Box::new(BoxyDatabase::new(client).await.unwrap());
@ -87,7 +88,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.await .await
.unwrap(); .unwrap();
let proxy_server = Server::new(svc, (config.proxy.listen, config.proxy.port), TlsOption::NoTls) let proxy_server = Server::new(svc, (config.proxy.listen, config.proxy.port), TlsOption::Tls(TlsManager.clone))
.await .await
.unwrap(); .unwrap();

View file

@ -11,7 +11,7 @@ use tokio::{net::TcpStream, sync::Mutex};
use crate::{ use crate::{
config::{Client, Config}, config::{Client, Config},
db::BoxyDatabase, db::BoxyDatabase,
routes::api::{AddHost, RegisterEndpoint, RemoveHost}, routes::api::{AddHostToEndpoint, RegisterEndpoint, RemoveHost},
server::{GeneralResponse, custom_resp}, server::{GeneralResponse, custom_resp},
services::matcher::Matcher, services::matcher::Matcher,
}; };
@ -119,7 +119,7 @@ impl Matcher for ApiMatcher {
fn retrieve(&self) -> Vec<Arc<dyn crate::services::matcher::Route<Self> + Sync + Send>> { fn retrieve(&self) -> Vec<Arc<dyn crate::services::matcher::Route<Self> + Sync + Send>> {
vec![ vec![
Arc::new(RegisterEndpoint {}), Arc::new(RegisterEndpoint {}),
Arc::new(AddHost {}), Arc::new(AddHostToEndpoint {}),
Arc::new(RemoveHost {}), Arc::new(RemoveHost {}),
] ]
} }

View file

@ -10,10 +10,14 @@ use crate::{
services::matcher::Route, services::matcher::Route,
}; };
pub struct AddHost {} pub struct LinkHost {}
pub struct UnlinkHost {}
pub struct GetHostStatus {}
pub struct RegisterEndpoint {}
pub struct DeregisterEndpoint {}
#[async_trait] #[async_trait]
impl Route<ApiMatcher> for AddHost { impl Route<ApiMatcher> for LinkHost {
fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool { fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool {
req.uri().path().starts_with("/endpoint/") && req.method() == Method::POST req.uri().path().starts_with("/endpoint/") && req.method() == Method::POST
} }
@ -63,8 +67,6 @@ impl Route<ApiMatcher> for AddHost {
} }
} }
pub struct RegisterEndpoint {}
#[async_trait] #[async_trait]
impl Route<ApiMatcher> for RegisterEndpoint { impl Route<ApiMatcher> for RegisterEndpoint {
fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool { fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool {
@ -113,10 +115,9 @@ impl Route<ApiMatcher> for RegisterEndpoint {
} }
} }
pub struct RemoveHost {}
#[async_trait] #[async_trait]
impl Route<ApiMatcher> for RemoveHost { impl Route<ApiMatcher> for DeregisterEndpoint {
fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool { fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool {
req.uri().path().starts_with("/endpoint/") && req.method() == Method::DELETE req.uri().path().starts_with("/endpoint/") && req.method() == Method::DELETE
} }

View file

@ -11,7 +11,12 @@ use hyper::{
use hyper_util::rt::TokioIo; use hyper_util::rt::TokioIo;
use json::JsonValue; use json::JsonValue;
use log::{error, info}; use log::{error, info};
use rustls::server::Acceptor; use rustls::{
ConfigBuilder, ServerConfig,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
server::Acceptor,
sign::SingleCertAndKey,
};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::{LazyConfigAcceptor, StartHandshake}; use tokio_rustls::{LazyConfigAcceptor, StartHandshake};
@ -73,7 +78,7 @@ where
S::ResBody: Send, S::ResBody: Send,
<S as HttpService<Incoming>>::Future: Send, <S as HttpService<Incoming>>::Future: Send,
{ {
pub async fn handle(&self) { pub async fn handle(&'static self) {
info!( info!(
"Server started at http://{} for service: {}", "Server started at http://{} for service: {}",
self.listener.local_addr().unwrap(), self.listener.local_addr().unwrap(),
@ -84,11 +89,10 @@ where
let (tcp_stream, _) = self.listener.accept().await.unwrap(); let (tcp_stream, _) = self.listener.accept().await.unwrap();
let mut svc_clone = self.service.clone(); let mut svc_clone = self.service.clone();
let tls = self.tls.clone();
tokio::task::spawn(async move { tokio::task::spawn(async move {
svc_clone.stream(&tcp_stream); svc_clone.stream(&tcp_stream);
match tls { match &self.tls {
TlsOption::NoTls => { TlsOption::NoTls => {
if let Err(err) = http1::Builder::new() if let Err(err) = http1::Builder::new()
.writev(false) .writev(false)
@ -103,14 +107,31 @@ where
match acceptor.await { match acceptor.await {
Ok(y) => { Ok(y) => {
let mut manager = x.lock().await;
let hello = y.client_hello(); let hello = y.client_hello();
let hostname = hello.server_name().clone().unwrap(); let hostname = hello.server_name().unwrap();
let config = Arc::new(x.matcher(hostname).unwrap());
let stream = y let raw_certificate =
.into_stream(config) manager.get_certificate(hostname.clone()).await.unwrap();
.await
let cert_chain = CertificateDer::pem_slice_iter(
raw_certificate.cert_data.as_slice(),
)
.map(|cert| cert.unwrap())
.collect();
let key = PrivateKeyDer::from_pem_slice(
raw_certificate.key_data.as_slice(),
)
.unwrap();
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, key)
.unwrap(); .unwrap();
let stream = y.into_stream(Arc::new(config)).await.unwrap();
if let Err(err) = http1::Builder::new() if let Err(err) = http1::Builder::new()
.writev(false) .writev(false)
.serve_connection(TokioIo::new(stream), svc_clone) .serve_connection(TokioIo::new(stream), svc_clone)
@ -119,10 +140,7 @@ where
error!("Error while trying to serve connection: {err}") error!("Error while trying to serve connection: {err}")
} }
} }
Err(e) => { Err(e) => error!("Error while initiating handshake: {e}"),
error!("Error while initiating handshake: {e}");
return;
}
} }
} }
}; };

View file

@ -1,48 +1,66 @@
use std::{ use std::{collections::HashMap, error::Error, sync::Arc};
error::{self, Error},
fs::File, use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
io, use tokio::{
path::{self, Path}, fs::{self, File},
sync::Arc, sync::Mutex,
}; };
use rustls::{ use crate::db::BoxyDatabase;
ServerConfig, crypto,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
sign::CertifiedKey,
};
#[derive(Clone)]
pub enum TlsOption { pub enum TlsOption {
NoTls, NoTls,
Tls(FileTls), Tls(Mutex<TlsManager>),
} }
#[derive(Clone)]
pub struct FileTls { pub struct TlsManager {
pub certs_path: String, pub certs_path: String,
pub certificates: HashMap<String, RawCertificate>,
pub database: Arc<Mutex<&'static mut BoxyDatabase>>,
} }
impl FileTls { impl RawCertificate {
pub fn matcher(&self, hostname: &str) -> Result<ServerConfig, Box<dyn Error>> { pub fn new(cert_data: Vec<u8>, key_data: Vec<u8>) -> Self {
Self {
cert_data,
key_data,
}
}
}
impl TlsManager {
pub async fn get_certificate(
&mut self,
hostname: &str,
) -> Result<&RawCertificate, Box<dyn Error>> {
if self.certificates.contains_key(hostname) {
return Ok(self.certificates.get(hostname).unwrap());
}
let path_to_pem = let path_to_pem =
safe_path::scoped_join(self.certs_path.clone(), format!("{hostname}.pem"))?; safe_path::scoped_join(self.certs_path.clone(), format!("{hostname}.pem"))?;
let path_to_key = let path_to_key =
safe_path::scoped_join(self.certs_path.clone(), format!("{hostname}.key"))?; safe_path::scoped_join(self.certs_path.clone(), format!("{hostname}.key"))?;
let certfile = File::open(path_to_pem)?; let cert_file = fs::read(path_to_pem).await.unwrap();
let mut cert_reader = io::BufReader::new(certfile); let key_file = fs::read(path_to_key).await.unwrap();
let certs = rustls_pemfile::certs(&mut cert_reader)
.map(|x| x.unwrap())
.collect();
let keyfile = File::open(path_to_key)?; self.certificates.insert(
let mut key_reader = io::BufReader::new(keyfile); hostname.to_string(),
let key = rustls_pemfile::private_key(&mut key_reader).map(|key| key.unwrap())?; RawCertificate::new(cert_file, key_file),
);
Ok(ServerConfig::builder() // fucking borrow checker
.with_no_client_auth() Ok(self.certificates.get(hostname).unwrap())
.with_single_cert(certs, key) }
.map_err(|e| e)?) }
impl TlsManager {
pub async fn new(path: String) -> Self {
Self {
certs_path: path,
certificates: HashMap::new(),
}
} }
} }