mannequin/src/server.rs
use axum::{
body::Bytes,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::Deserialize;
use std::sync::Arc;
use crate::auth::AuthService;
use crate::config::MannequinConfig;
use crate::event_log::{Direction, EventLog, SystemKind};
use crate::nexus::MockNexusService;
use crate::{OidcService, PkiService};
pub struct MannequinServer {
pub pki: Arc<PkiService>,
pub oidc: Arc<OidcService>,
pub auth: Arc<AuthService>,
pub nexus: Arc<MockNexusService>,
pub event_log: EventLog,
pub port: u16,
}
#[derive(Clone)]
struct AppState {
pki: Arc<PkiService>,
oidc: Arc<OidcService>,
auth: Arc<AuthService>,
nexus: Arc<MockNexusService>,
event_log: EventLog,
}
impl MannequinServer {
pub fn new(port: u16) -> anyhow::Result<Self> {
Self::from_config(MannequinConfig {
port,
..Default::default()
})
}
pub fn from_config(config: MannequinConfig) -> anyhow::Result<Self> {
let port = config.port;
let pki = Arc::new(PkiService::from_config(&config)?);
let oidc = Arc::new(OidcService::new(format!("http://127.0.0.1:{}", port)));
let auth = Arc::new(AuthService::new());
let base_url = format!("http://127.0.0.1:{}", port);
let nexus = Arc::new(MockNexusService::new(&base_url));
let event_log = EventLog::new();
Ok(Self {
pki,
oidc,
auth,
nexus,
event_log,
port,
})
}
pub fn router(&self) -> Router {
let state = AppState {
pki: self.pki.clone(),
oidc: self.oidc.clone(),
auth: self.auth.clone(),
nexus: self.nexus.clone(),
event_log: self.event_log.clone(),
};
Router::new()
// PKI endpoints
.route("/pki/ca.pem", get(pki_ca_pem))
.route("/pki/issue/server", post(pki_issue_server))
.route("/pki/issue/client", post(pki_issue_client))
.route("/pki/list", get(pki_list))
// OIDC endpoints
.route("/.well-known/openid-configuration", get(oidc_discovery))
.route("/token", post(oidc_token))
.route("/userinfo", get(oidc_userinfo))
// Auth endpoints (user management + credential issuance)
.route("/auth/users", get(auth_list_users))
.route("/auth/credentials/{username}", post(auth_issue_credentials))
.route("/auth/validate", get(auth_validate))
// Mock Nexus endpoints
.route("/service/rest/v1/repositories", get(nexus_list_repos))
.route(
"/service/rest/v1/repositories/raw/hosted",
post(nexus_create_repo),
)
.route("/service/rest/v1/assets", get(nexus_list_assets))
.route(
"/repository/{repo}/{*path}",
get(nexus_download).put(nexus_upload),
)
// Health
.route("/health", get(health))
.with_state(state)
}
pub async fn serve(self) -> anyhow::Result<()> {
let addr = format!("0.0.0.0:{}", self.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Mannequin listening on {}", addr);
// TCP_NODELAY on every connection (HOL-PERF-3): axum 0.8 leaves Nagle on.
let listener =
axum::serve::ListenerExt::tap_io(listener, |tcp: &mut tokio::net::TcpStream| {
let _ = tcp.set_nodelay(true);
});
axum::serve(listener, self.router()).await?;
Ok(())
}
}
// === Health ===
async fn health() -> &'static str {
"OK"
}
// === PKI handlers ===
async fn pki_ca_pem(State(state): State<AppState>) -> String {
state
.event_log
.log(SystemKind::Pki, Direction::Inbound, "GET /pki/ca.pem", None);
state.pki.ca_cert_pem()
}
#[derive(Deserialize)]
struct IssueServerReq {
cn: String,
sans: Option<Vec<String>>,
}
#[derive(serde::Serialize)]
struct CertResponse {
cert_pem: String,
key_pem: String,
}
async fn pki_issue_server(
State(state): State<AppState>,
Json(req): Json<IssueServerReq>,
) -> Result<Json<CertResponse>, StatusCode> {
state.event_log.log(
SystemKind::Pki,
Direction::Inbound,
format!("POST /pki/issue/server cn={}", req.cn),
None,
);
let sans: Vec<&str> = req
.sans
.as_ref()
.map(|v| v.iter().map(|s| s.as_str()).collect())
.unwrap_or_default();
let cert = state
.pki
.issue_server_cert(&req.cn, sans)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
state.event_log.log(
SystemKind::Pki,
Direction::Outbound,
format!("Issued server cert: {}", req.cn),
None,
);
Ok(Json(CertResponse {
cert_pem: cert.cert_pem,
key_pem: cert.key_pem,
}))
}
#[derive(Deserialize)]
struct IssueClientReq {
cn: String,
}
async fn pki_issue_client(
State(state): State<AppState>,
Json(req): Json<IssueClientReq>,
) -> Result<Json<CertResponse>, StatusCode> {
let cert = state
.pki
.issue_client_cert(&req.cn)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(CertResponse {
cert_pem: cert.cert_pem,
key_pem: cert.key_pem,
}))
}
async fn pki_list(State(state): State<AppState>) -> Json<Vec<String>> {
Json(state.pki.list_issued())
}
// === OIDC handlers ===
async fn oidc_discovery(State(state): State<AppState>) -> impl IntoResponse {
Json(state.oidc.discovery())
}
#[derive(Deserialize)]
struct OidcTokenForm {
username: Option<String>,
client_id: Option<String>,
}
async fn oidc_token(
State(state): State<AppState>,
Json(form): Json<OidcTokenForm>,
) -> impl IntoResponse {
let subject = form
.username
.or(form.client_id)
.unwrap_or_else(|| "anonymous".to_string());
state.event_log.log(
SystemKind::Oidc,
Direction::Inbound,
format!("POST /token sub={}", subject),
None,
);
let resp = state.oidc.issue_token(&subject);
state.event_log.log(
SystemKind::Oidc,
Direction::Outbound,
format!("Token issued: {}", &resp.access_token[..8]),
None,
);
Json(resp)
}
async fn oidc_userinfo(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
let token = headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let subject = state
.oidc
.validate_token(token)
.ok_or(StatusCode::UNAUTHORIZED)?;
Ok(Json(serde_json::json!({ "sub": subject })))
}
// === Mock Nexus handlers ===
async fn nexus_list_repos(
State(state): State<AppState>,
) -> Json<Vec<crate::nexus::NexusRepository>> {
Json(state.nexus.list_repositories())
}
#[derive(Deserialize)]
struct CreateRepoReq {
#[allow(dead_code)]
name: String,
}
async fn nexus_create_repo(
State(_state): State<AppState>,
Json(_req): Json<CreateRepoReq>,
) -> StatusCode {
// Repos are fixed (proxy-only, prod, upload) — acknowledge but no-op
StatusCode::CREATED
}
#[derive(Deserialize)]
struct AssetQuery {
repository: String,
}
async fn nexus_list_assets(
State(state): State<AppState>,
Query(q): Query<AssetQuery>,
) -> Json<crate::nexus::AssetPage> {
let items = state.nexus.list_assets(&q.repository);
Json(crate::nexus::AssetPage {
items,
continuation_token: None,
})
}
async fn nexus_upload(
State(state): State<AppState>,
Path((repo, path)): Path<(String, String)>,
body: Bytes,
) -> StatusCode {
state.event_log.log(
SystemKind::Nexus,
Direction::Inbound,
format!("PUT /{}/{} ({}B)", repo, path, body.len()),
None,
);
if state.nexus.upload(&repo, &path, body.to_vec()) {
state.event_log.log(
SystemKind::Nexus,
Direction::Outbound,
format!("201 stored {}", path),
None,
);
StatusCode::CREATED
} else {
StatusCode::FORBIDDEN
}
}
async fn nexus_download(
State(state): State<AppState>,
Path((repo, path)): Path<(String, String)>,
) -> Result<Bytes, StatusCode> {
state.event_log.log(
SystemKind::Nexus,
Direction::Inbound,
format!("GET /{}/{}", repo, path),
None,
);
state
.nexus
.download(&repo, &path)
.await
.map(|data| {
state.event_log.log(
SystemKind::Nexus,
Direction::Outbound,
format!("200 {} ({}B)", path, data.len()),
None,
);
Bytes::from(data)
})
.ok_or(StatusCode::NOT_FOUND)
}
// === Auth handlers ===
async fn auth_list_users(State(state): State<AppState>) -> Json<Vec<String>> {
Json(state.auth.list_users())
}
#[derive(serde::Serialize)]
struct CredentialsResponse {
username: String,
access_token: String,
client_cert_pem: String,
client_key_pem: String,
}
async fn auth_issue_credentials(
State(state): State<AppState>,
Path(username): Path<String>,
) -> Result<Json<CredentialsResponse>, StatusCode> {
state.event_log.log(
SystemKind::Oidc,
Direction::Inbound,
format!("POST /auth/credentials/{}", username),
None,
);
let creds = state
.auth
.issue_credentials(&username, &state.oidc, &state.pki)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
state.event_log.log(
SystemKind::Oidc,
Direction::Outbound,
format!("Issued creds for {}", username),
None,
);
Ok(Json(CredentialsResponse {
username: creds.username,
access_token: creds.access_token,
client_cert_pem: creds.client_cert_pem,
client_key_pem: creds.client_key_pem,
}))
}
async fn auth_validate(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
) -> Result<Json<serde_json::Value>, StatusCode> {
// Try Bearer token first, then check X-Client-CN header (simulating mTLS CN extraction)
if let Some(auth_header) = headers.get("Authorization") {
if let Some(token) = auth_header
.to_str()
.ok()
.and_then(|v| v.strip_prefix("Bearer "))
{
if let Some(identity) = state.auth.validate_oidc_token(token, &state.oidc) {
return Ok(Json(serde_json::json!({
"subject": identity.subject,
"method": "oidc",
"roles": identity.roles,
})));
}
}
}
if let Some(cn_header) = headers.get("X-Client-CN") {
if let Ok(cn) = cn_header.to_str() {
if let Some(identity) = state.auth.validate_client_cert(cn) {
return Ok(Json(serde_json::json!({
"subject": identity.subject,
"method": "mtls",
"roles": identity.roles,
})));
}
}
}
Err(StatusCode::UNAUTHORIZED)
}