gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **The fake company IdP over https, under a throwaway CA it hands out** (the estate-box
//! gate's SSO1, 2026-10-12: gunnar-ui sends a person only to an https identity provider —
//! `sso_error=unsafe-provider` against the http fake). `fake_idp --https --oidc-bind <ip>`
//! serves its OpenID provider at `https://<ip>:<port>`, its certificate issued for that
//! address under a CA it prints as `oidc_ca_pem` — the CA an account admin hands gunnar as
//! the provider's `idp_ca_pem`. Discovery is read here over TLS verified against that CA
//! alone.

use std::io::{BufRead, BufReader, Read, Write};
use std::path::PathBuf;
use std::sync::Arc;

/// The example binary cargo built beside this test (`target/<profile>/examples/fake_idp`).
///
/// `cargo test` builds every example beside the tests; `cargo llvm-cov` runs
/// `cargo test --tests`, which builds none, so the nightly coverage run found no
/// binary here (`No such file or directory`, 2026-10-31). Then it is built here: into
/// the same target directory and profile, with the flags this test inherited from the
/// cargo that started it, so the coverage run measures the example too.
fn fake_idp() -> PathBuf {
    let me = std::env::current_exe().expect("this test's path");
    let profile = me
        .parent()
        .and_then(|deps| deps.parent())
        .expect("target/<profile>");
    let binary = profile.join("examples").join("fake_idp");
    if !binary.exists() {
        let target_dir = profile.parent().expect("the target directory");
        let mut cargo = std::process::Command::new(env!("CARGO"));
        cargo
            .args(["build", "-p", "gunnar-server", "--example", "fake_idp"])
            .args([
                "--manifest-path",
                concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"),
            ])
            .arg("--target-dir")
            .arg(target_dir);
        match profile.file_name().and_then(|n| n.to_str()) {
            Some("debug") | None => {}
            Some(named) => {
                cargo.args(["--profile", named]);
            }
        }
        let status = cargo.status().expect("run cargo build --example fake_idp");
        assert!(status.success(), "cargo build --example fake_idp: {status}");
    }
    binary
}

/// The CA's DER, read as gunnar reads an account's IdP CA.
fn ca_der(pem: &str) -> Vec<u8> {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as i64;
    gunnar_grpc::sso::idp_ca::parse_idp_ca(pem, now)
        .expect("an IdP CA gunnar takes")
        .remove(0)
}

/// GET `path` from `issuer` over TLS, `ca_pem` the only root: (status line, body).
fn get_https(issuer: &str, ca_pem: &str, path: &str) -> (String, String) {
    let addr = issuer.strip_prefix("https://").expect("an https issuer");
    let host = addr.rsplit_once(':').expect("host:port").0.to_owned();
    let mut roots = rustls::RootCertStore::empty();
    roots
        .add(rustls::pki_types::CertificateDer::from(ca_der(ca_pem)))
        .expect("the CA");
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(
        rustls::crypto::ring::default_provider(),
    ))
    .with_safe_default_protocol_versions()
    .expect("versions")
    .with_root_certificates(roots)
    .with_no_client_auth();
    let name = rustls::pki_types::ServerName::try_from(host).expect("a name");
    let conn = rustls::ClientConnection::new(Arc::new(cfg), name).expect("a client");
    let sock = std::net::TcpStream::connect(addr).expect("the fake listens");
    let mut tls = rustls::StreamOwned::new(conn, sock);
    write!(
        tls,
        "GET {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n\r\n"
    )
    .expect("a request");
    let mut raw = Vec::new();
    let _ = tls.read_to_end(&mut raw);
    let text = String::from_utf8_lossy(&raw).to_string();
    let (head, body) = text.split_once("\r\n\r\n").expect("an HTTP answer");
    (
        head.lines().next().unwrap_or_default().to_owned(),
        body.to_owned(),
    )
}

/// The fake, killed whatever the test's end.
struct Running(std::process::Child);

impl Drop for Running {
    fn drop(&mut self) {
        let _ = self.0.kill();
        let _ = self.0.wait();
    }
}

#[test]
fn the_fake_idp_serves_its_openid_provider_over_https_and_hands_out_its_ca() {
    let child = std::process::Command::new(fake_idp())
        .args([
            "--client-id",
            "bench",
            "--client-secret",
            "s3cret",
            "--https",
            "--oidc-bind",
            "127.0.0.1",
        ])
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("the example runs");
    let mut child = Running(child);
    let mut line = String::new();
    let mut out = BufReader::new(child.0.stdout.take().expect("stdout"));
    out.read_line(&mut line).expect("its line");
    let up: serde_json::Value =
        serde_json::from_str(&line).unwrap_or_else(|e| panic!("{e}: {line:?}"));
    let issuer = up["oidc_issuer"].as_str().expect("an issuer").to_owned();
    assert!(issuer.starts_with("https://127.0.0.1:"), "{issuer}");
    let ca = up["oidc_ca_pem"]
        .as_str()
        .expect("the CA it is issued under")
        .to_owned();
    assert!(ca.contains("BEGIN CERTIFICATE") && !ca.contains("PRIVATE KEY"));
    assert!(
        !line.contains("s3cret"),
        "the client secret is never printed"
    );
    // gunnar takes it as an IdP CA (a CA, valid now).
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as i64;
    assert_eq!(
        gunnar_grpc::sso::idp_ca::parse_idp_ca(&ca, now)
            .expect("an IdP CA gunnar takes")
            .len(),
        1
    );
    let (status, body) = get_https(&issuer, &ca, "/.well-known/openid-configuration");
    assert!(status.contains("200"), "{status}");
    let doc: serde_json::Value = serde_json::from_str(&body).expect("discovery");
    assert_eq!(doc["issuer"], issuer);
    assert!(doc["authorization_endpoint"]
        .as_str()
        .unwrap()
        .starts_with("https://"));
}