use std::io::{BufRead, BufReader, Read, Write};
use std::path::PathBuf;
use std::sync::Arc;
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
}
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)
}
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(),
)
}
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"
);
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://"));
}