continue on handlers

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt
2026-04-29 15:32:32 +02:00
parent 1df0de1662
commit b260aeed50
4 changed files with 112 additions and 43 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
//! Utilities for working with the authentik API client.
use ak_client::apis::configuration::Configuration;
use ak_client::models::Pagination;
use ak_client::{apis::configuration::Configuration, models::Pagination};
use eyre::{Result, eyre};
use url::Url;
+9 -9
View File
@@ -1,8 +1,7 @@
use url::Url;
use ak_client::models::ProxyOutpostConfig;
use eyre::{Result, eyre};
use tracing::instrument;
use url::Url;
use crate::outpost::proxy::ProxyOutpost;
@@ -10,20 +9,20 @@ const REDIRECT_PARAM: &str = "rd";
const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback";
const LOGOUT_SIGNATURE: &str = "X-authentik-logout";
#[derive(Debug)]
pub(super) struct Application {
pub(super) host: String,
pub(super) provider: ProxyOutpostConfig,
}
impl Application {
#[instrument(skip_all)]
pub(super) fn new(
_existing_apps: &ProxyOutpost,
provider: &ProxyOutpostConfig,
) -> Result<Self> {
pub(super) fn new(_existing_apps: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result<Self> {
let external_url = Url::parse(&provider.external_host)?;
let external_host = external_url
.host_str()
.ok_or_else(|| eyre!("no host in external host"))?;
if !external_url.has_authority() {
return Err(eyre!("no host in external host"));
}
let external_host = external_url.authority();
let _redirect_url = {
let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?;
@@ -33,6 +32,7 @@ impl Application {
Ok(Self {
host: external_host.to_owned(),
provider,
})
}
}
+38 -11
View File
@@ -1,12 +1,15 @@
use ak_axum::extract::host::Host;
use axum::extract::State;
use axum::http::Method;
use metrics::histogram;
use std::sync::Arc;
use tokio::time::Instant;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use ak_axum::{error::Result, extract::host::Host};
use axum::{
extract::{Request, State},
http::{Method, StatusCode, header::CONTENT_TYPE},
response::{IntoResponse as _, Response},
};
use metrics::histogram;
use serde_json::json;
use tokio::time::Instant;
use tracing::{instrument, trace, warn};
use crate::outpost::proxy::ProxyOutpost;
@@ -14,7 +17,7 @@ pub(super) async fn handle_ping(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
) -> impl IntoResponse {
) -> Response {
let start = Instant::now();
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
@@ -24,9 +27,33 @@ pub(super) async fn handle_ping(
"type" => "ping",
)
.record(start.elapsed().as_secs_f64());
StatusCode::NO_CONTENT
StatusCode::NO_CONTENT.into_response()
}
pub(super) async fn default(State(_outpost): State<Arc<ProxyOutpost>>) -> impl IntoResponse {
StatusCode::NOT_FOUND
#[instrument(skip(request, outpost))]
pub(super) async fn default(
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
request: Request,
) -> Result<Response> {
let Some(_app) = outpost.lookup_app(&host) else {
trace!(headers = ?request.headers(), "tracing headers for no hostname match");
warn!("no app for hostname");
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(CONTENT_TYPE, "application/json")
.body(
json!({
"message": "no app for hostname",
"host": host,
"detail": format!("check the outpost settings and make sure '{host}' is included."),
})
.to_string()
.into(),
)
.expect("infallible"));
};
trace!("passing to application");
Ok(StatusCode::NOT_FOUND.into_response())
}
+64 -21
View File
@@ -1,17 +1,15 @@
use ak_common::config;
use std::{collections::HashMap, sync::Arc};
use ak_axum::router::wrap_router;
use ak_client::apis::outposts_api::outposts_proxy_list;
use ak_common::{Tasks, api::fetch_all};
use ak_client::{apis::outposts_api::outposts_proxy_list, models::ProxyMode};
use ak_common::{Tasks, api::fetch_all, config};
use arc_swap::ArcSwap;
use argh::FromArgs;
use axum::Router;
use eyre::Result;
use tracing::{debug, error, info, instrument, warn};
use tracing::{debug, error, info, instrument, trace, warn};
use crate::outpost::proxy::application::Application;
use crate::outpost::{Outpost, OutpostController};
use crate::outpost::{Outpost, OutpostController, proxy::application::Application};
mod application;
mod handlers;
@@ -27,7 +25,7 @@ pub(crate) struct Cli {}
pub(crate) struct ProxyOutpost {
controller: Arc<OutpostController>,
applications: ArcSwap<HashMap<String, Application>>,
apps: ArcSwap<HashMap<String, Arc<Application>>>,
}
impl Outpost for ProxyOutpost {
@@ -39,7 +37,7 @@ impl Outpost for ProxyOutpost {
async fn new(controller: Arc<OutpostController>) -> Result<Self> {
Ok(Self {
controller,
applications: ArcSwap::from_pointee(HashMap::with_capacity(0)),
apps: ArcSwap::from_pointee(HashMap::with_capacity(0)),
})
}
@@ -97,21 +95,18 @@ impl Outpost for ProxyOutpost {
let mut apps = HashMap::with_capacity(providers.len());
for provider in providers {
let Ok(application) = Application::new(self, &provider)
let name = provider.name.clone();
let Ok(application) = Application::new(self, provider)
.inspect_err(|err| warn!(?err, "failed to setup application, skipping provider"))
else {
continue;
};
info!(
name = provider.name,
host = application.host,
"loaded application"
);
info!(name, host = application.host, "loaded application");
apps.insert(application.host.clone(), application);
apps.insert(application.host.clone(), Arc::new(application));
}
self.applications.store(Arc::new(apps));
self.apps.store(Arc::new(apps));
Ok(())
}
@@ -123,20 +118,68 @@ impl Outpost for ProxyOutpost {
}
}
// TODO: actually write this
fn build_static_router() -> Router {
Router::new()
impl ProxyOutpost {
#[instrument(skip(self))]
fn lookup_app(&self, host: &str) -> Option<Arc<Application>> {
let apps = self.apps.load();
// If we only have a single app, host name switching doesn't matter.
if apps.len() == 1
&& let Some(app) = apps.values().next()
{
debug!(app = app.provider.name, "found a single app, using it");
return Some(Arc::clone(app));
}
if let Some(app) = apps.get(host) {
debug!(app = app.provider.name, "found app based direct host match");
return Some(Arc::clone(app));
}
// For forward_auth_domain, we don't have a direct app to domain relationship.
// Check through all apps, and check how much of their cookie domain matches the host.
// Return the application that has the longest match.
let mut longest_match = None;
let mut longest_len = 0_usize;
for app in apps.values() {
if app.provider.mode != Some(ProxyMode::ForwardDomain) {
continue;
}
if let Some(cookie_domain) = app.provider.cookie_domain.as_deref() {
// Check if the cookie domain has a leading period for a wildcard.
// This will decrease the weight of a wildcard domain, but a request to example.com
// with the cookie domain set to example.com will still be routed correctly.
let domain = cookie_domain.trim_start_matches('.');
if host.ends_with(domain) && domain.len() > longest_len {
longest_len = domain.len();
longest_match = Some(Arc::clone(app));
}
// For forward_auth_domain, we need to response on the external domain too.
if app.provider.external_host == host {
debug!(app = app.provider.name, "found app based on external_host");
return Some(Arc::clone(app));
}
}
}
if let Some(app) = &longest_match {
debug!(app = app.provider.name, "found app based on cookie domain");
}
longest_match
}
}
fn build_router(outpost: Arc<ProxyOutpost>) -> Router {
// TODO: static files
wrap_router(
Router::new()
.nest(
"/outpost.goauthentik.io/ping",
Router::new().fallback(handlers::handle_ping),
)
// .nest("/outpost.goauthentik.io/static", build_static_router())
.fallback(handlers::default)
.with_state(outpost),
true,