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>");
profile.join("examples").join("fake_idp")
}
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://"));
}