Moved http service out of Registry into separate struct
This commit is contained in:
parent
693df6817a
commit
4fe64981d0
|
@ -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>,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
|
@ -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
147
src/web.rs
Normal 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()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user