Moved http service out of Registry into separate struct

This commit is contained in:
Dreaded_X 2025-04-16 02:15:53 +02:00
parent 693df6817a
commit 4fe64981d0
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
6 changed files with 174 additions and 151 deletions

View File

@ -53,7 +53,7 @@ impl ForwardAuth {
} }
} }
pub async fn check_auth( pub async fn check(
&self, &self,
methods: &Method, methods: &Method,
headers: &HeaderMap<HeaderValue>, headers: &HeaderMap<HeaderValue>,

View File

@ -12,9 +12,11 @@ mod stats;
mod tui; mod tui;
mod tunnel; mod tunnel;
mod units; mod units;
mod web;
mod wrapper; mod wrapper;
pub use ldap::Ldap; pub use ldap::Ldap;
pub use server::Server; pub use server::Server;
pub use tunnel::Registry; pub use tunnel::Registry;
pub use tunnel::Tunnel; pub use tunnel::Tunnel;
pub use web::Service;

View File

@ -8,7 +8,7 @@ use rand::rngs::OsRng;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
use tunnel_rs::{Ldap, Registry, Server, auth::ForwardAuth}; use tunnel_rs::{Ldap, Registry, Server, Service, auth::ForwardAuth};
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<()> { async fn main() -> color_eyre::Result<()> {
@ -43,14 +43,14 @@ async fn main() -> color_eyre::Result<()> {
let authz_address = std::env::var("AUTHZ_ENDPOINT").wrap_err("AUTHZ_ENDPOINT is not set")?; let authz_address = std::env::var("AUTHZ_ENDPOINT").wrap_err("AUTHZ_ENDPOINT is not set")?;
let ldap = Ldap::start_from_env().await?; let ldap = Ldap::start_from_env().await?;
let registry = Registry::new(domain);
let auth = ForwardAuth::new(authz_address); let mut ssh = Server::new(ldap, registry.clone());
let tunnels = Registry::new(domain, auth);
let mut ssh = Server::new(ldap, tunnels.clone());
let addr = SocketAddr::from(([0, 0, 0, 0], ssh_port)); let addr = SocketAddr::from(([0, 0, 0, 0], ssh_port));
tokio::spawn(async move { ssh.run(key, addr).await }); tokio::spawn(async move { ssh.run(key, addr).await });
info!("SSH is available on {addr}"); info!("SSH is available on {addr}");
let auth = ForwardAuth::new(authz_address);
let service = Service::new(registry, auth);
let addr = SocketAddr::from(([0, 0, 0, 0], http_port)); let addr = SocketAddr::from(([0, 0, 0, 0], http_port));
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
info!("HTTP is available on {addr}"); info!("HTTP is available on {addr}");
@ -60,12 +60,12 @@ async fn main() -> color_eyre::Result<()> {
let (stream, _) = listener.accept().await?; let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream); let io = TokioIo::new(stream);
let tunnels = tunnels.clone(); let service = service.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(err) = http1::Builder::new() if let Err(err) = http1::Builder::new()
.preserve_header_case(true) .preserve_header_case(true)
.title_case_headers(true) .title_case_headers(true)
.serve_connection(io, tunnels) .serve_connection(io, service)
.with_upgrades() .with_upgrades()
.await .await
{ {

View File

@ -1,14 +1,15 @@
mod registry;
mod tui;
use registry::RegistryEntry; use registry::RegistryEntry;
use std::sync::Arc; use std::sync::Arc;
use tracing::trace; use tracing::trace;
use russh::server::Handle; use russh::server::Handle;
use tokio::sync::RwLock; use tokio::sync::{RwLock, RwLockReadGuard};
use crate::{stats::Stats, wrapper::Wrapper}; use crate::{stats::Stats, wrapper::Wrapper};
mod registry;
pub mod tui;
pub use registry::Registry; pub use registry::Registry;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -43,6 +44,14 @@ impl TunnelInner {
Ok(Wrapper::new(channel.into_stream(), self.stats.clone())) Ok(Wrapper::new(channel.into_stream(), self.stats.clone()))
} }
pub async fn is_public(&self) -> bool {
matches!(*self.access.read().await, TunnelAccess::Public)
}
pub async fn get_access(&self) -> RwLockReadGuard<'_, TunnelAccess> {
self.access.read().await
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -82,10 +91,6 @@ impl Tunnel {
*self.inner.access.write().await = access; *self.inner.access.write().await = access;
} }
pub async fn is_public(&self) -> bool {
matches!(*self.inner.access.read().await, TunnelAccess::Public)
}
pub fn get_address(&self) -> Option<&String> { pub fn get_address(&self) -> Option<&String> {
self.registry_entry.get_address() self.registry_entry.get_address()
} }

View File

@ -1,29 +1,12 @@
use std::{ use std::{
collections::{HashMap, hash_map::Entry}, collections::{HashMap, hash_map::Entry},
ops::Deref,
pin::Pin,
sync::Arc, sync::Arc,
}; };
use bytes::Bytes;
use http_body_util::{BodyExt as _, Empty, combinators::BoxBody};
use hyper::{
Request, Response, StatusCode,
body::Incoming,
client::conn::http1::Builder,
header::{self, HOST},
service::Service,
};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{debug, error, trace, warn}; use tracing::trace;
use crate::{ use crate::{Tunnel, animals::get_animal_name};
Tunnel,
animals::get_animal_name,
auth::{AuthStatus, ForwardAuth},
helper::response,
tunnel::TunnelAccess,
};
use super::TunnelInner; use super::TunnelInner;
@ -73,15 +56,13 @@ impl Drop for RegistryEntry {
pub struct Registry { pub struct Registry {
tunnels: Arc<RwLock<HashMap<String, TunnelInner>>>, tunnels: Arc<RwLock<HashMap<String, TunnelInner>>>,
domain: String, domain: String,
auth: ForwardAuth,
} }
impl Registry { impl Registry {
pub fn new(domain: impl Into<String>, auth: ForwardAuth) -> Self { pub fn new(domain: impl Into<String>) -> Self {
Self { Self {
tunnels: Arc::new(RwLock::new(HashMap::new())), tunnels: Arc::new(RwLock::new(HashMap::new())),
domain: domain.into(), domain: domain.into(),
auth,
} }
} }
@ -142,120 +123,8 @@ impl Registry {
tunnel.registry_entry.name = name.into(); tunnel.registry_entry.name = name.into();
self.register(tunnel).await; self.register(tunnel).await;
} }
}
impl Service<Request<Incoming>> for Registry { pub async fn get(&self, address: &str) -> Option<TunnelInner> {
type Response = Response<BoxBody<Bytes, hyper::Error>>; self.tunnels.read().await.get(address).cloned()
type Error = hyper::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
trace!("{:#?}", req);
let Some(authority) = req
.uri()
.authority()
.as_ref()
.map(|a| a.to_string())
.or_else(|| {
req.headers()
.get(HOST)
.and_then(|h| h.to_str().ok().map(|s| s.to_owned()))
})
else {
let resp = response(
StatusCode::BAD_REQUEST,
"Missing or invalid authority or host header",
);
return Box::pin(async { Ok(resp) });
};
debug!(authority, "Tunnel request");
let s = self.clone();
Box::pin(async move {
let Some(entry) = s.tunnels.read().await.get(&authority).cloned() else {
debug!(tunnel = authority, "Unknown tunnel");
let resp = response(StatusCode::NOT_FOUND, "Unknown tunnel");
return Ok(resp);
};
if !matches!(entry.access.read().await.deref(), TunnelAccess::Public) {
let user = match s.auth.check_auth(req.method(), req.headers()).await {
Ok(AuthStatus::Authenticated(user)) => user,
Ok(AuthStatus::Unauthenticated(location)) => {
let resp = Response::builder()
.status(StatusCode::FOUND)
.header(header::LOCATION, location)
.body(
Empty::new()
// NOTE: I have NO idea why this is able to convert from Innfallible to hyper::Error
.map_err(|never| match never {})
.boxed(),
)
.expect("configuration should be valid");
return Ok(resp);
}
Ok(AuthStatus::Unauthorized) => {
let resp = response(
StatusCode::FORBIDDEN,
"You do not have permission to access this tunnel",
);
return Ok(resp);
}
Err(err) => {
error!("Unexpected error during authentication: {err}");
let resp = response(
StatusCode::FORBIDDEN,
"Unexpected error during authentication",
);
return Ok(resp);
}
};
trace!("Tunnel is getting accessed by {user:?}");
if let TunnelAccess::Private(owner) = entry.access.read().await.deref() {
if !user.is(owner) {
let resp = response(
StatusCode::FORBIDDEN,
"You do not have permission to access this tunnel",
);
return Ok(resp);
}
}
}
let io = match entry.open().await {
Ok(io) => io,
Err(err) => {
warn!(tunnel = authority, "Failed to open tunnel: {err}");
let resp = response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to open tunnel");
return Ok(resp);
}
};
let (mut sender, conn) = Builder::new()
.preserve_header_case(true)
.title_case_headers(true)
.handshake(io)
.await?;
tokio::spawn(async move {
if let Err(err) = conn.await {
warn!(runnel = authority, "Connection failed: {err}");
}
});
let resp = sender.send_request(req).await?;
Ok(resp.map(|b| b.boxed()))
})
} }
} }

147
src/web.rs Normal file
View File

@ -0,0 +1,147 @@
use crate::Registry;
use std::{ops::Deref, pin::Pin};
use bytes::Bytes;
use http_body_util::{BodyExt as _, Empty, combinators::BoxBody};
use hyper::{
Request, Response, StatusCode,
body::Incoming,
client::conn::http1::Builder,
header::{self, HOST},
};
use tracing::{debug, error, trace, warn};
use crate::{
auth::{AuthStatus, ForwardAuth},
helper::response,
tunnel::TunnelAccess,
};
#[derive(Debug, Clone)]
pub struct Service {
registry: Registry,
auth: ForwardAuth,
}
impl Service {
pub fn new(registry: Registry, auth: ForwardAuth) -> Self {
Self { registry, auth }
}
}
impl hyper::service::Service<Request<Incoming>> for Service {
type Response = Response<BoxBody<Bytes, hyper::Error>>;
type Error = hyper::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
trace!("{:#?}", req);
let Some(authority) = req
.uri()
.authority()
.as_ref()
.map(|a| a.to_string())
.or_else(|| {
req.headers()
.get(HOST)
.and_then(|h| h.to_str().ok().map(|s| s.to_owned()))
})
else {
let resp = response(
StatusCode::BAD_REQUEST,
"Missing or invalid authority or host header",
);
return Box::pin(async { Ok(resp) });
};
debug!(authority, "Tunnel request");
let registry = self.registry.clone();
let auth = self.auth.clone();
Box::pin(async move {
let Some(entry) = registry.get(&authority).await else {
debug!(tunnel = authority, "Unknown tunnel");
let resp = response(StatusCode::NOT_FOUND, "Unknown tunnel");
return Ok(resp);
};
if !entry.is_public().await {
let user = match auth.check(req.method(), req.headers()).await {
Ok(AuthStatus::Authenticated(user)) => user,
Ok(AuthStatus::Unauthenticated(location)) => {
let resp = Response::builder()
.status(StatusCode::FOUND)
.header(header::LOCATION, location)
.body(
Empty::new()
// NOTE: I have NO idea why this is able to convert from Innfallible to hyper::Error
.map_err(|never| match never {})
.boxed(),
)
.expect("configuration should be valid");
return Ok(resp);
}
Ok(AuthStatus::Unauthorized) => {
let resp = response(
StatusCode::FORBIDDEN,
"You do not have permission to access this tunnel",
);
return Ok(resp);
}
Err(err) => {
error!("Unexpected error during authentication: {err}");
let resp = response(
StatusCode::FORBIDDEN,
"Unexpected error during authentication",
);
return Ok(resp);
}
};
trace!("Tunnel is getting accessed by {user:?}");
if let TunnelAccess::Private(owner) = entry.get_access().await.deref() {
if !user.is(owner) {
let resp = response(
StatusCode::FORBIDDEN,
"You do not have permission to access this tunnel",
);
return Ok(resp);
}
}
}
let io = match entry.open().await {
Ok(io) => io,
Err(err) => {
warn!(tunnel = authority, "Failed to open tunnel: {err}");
let resp = response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to open tunnel");
return Ok(resp);
}
};
let (mut sender, conn) = Builder::new()
.preserve_header_case(true)
.title_case_headers(true)
.handshake(io)
.await?;
tokio::spawn(async move {
if let Err(err) = conn.await {
warn!(runnel = authority, "Connection failed: {err}");
}
});
let resp = sender.send_request(req).await?;
Ok(resp.map(|b| b.boxed()))
})
}
}