#![cfg(any(feature = "http-client", feature = "http2"))]
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
use rustls::pki_types::{CertificateDer, ServerName};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
pub(crate) const SERVICE_ACCOUNT_DIR: &str = "/var/run/secrets/kubernetes.io/serviceaccount";
pub(crate) struct InClusterConfig {
pub host: String,
pub port: u16,
pub token: String,
pub namespace: String,
pub client_config: Arc<ClientConfig>,
}
pub(crate) fn load() -> Result<InClusterConfig, String> {
let host = std::env::var("KUBERNETES_SERVICE_HOST")
.map_err(|_| "KUBERNETES_SERVICE_HOST is not set (not running inside a pod?)".to_string())?;
let port: u16 = std::env::var("KUBERNETES_SERVICE_PORT")
.unwrap_or_else(|_| "443".to_string())
.parse()
.map_err(|_| "KUBERNETES_SERVICE_PORT is not a valid port number".to_string())?;
let token = std::fs::read_to_string(format!("{SERVICE_ACCOUNT_DIR}/token"))
.map_err(|e| format!("failed to read service account token: {e}"))?
.trim()
.to_string();
let namespace = std::fs::read_to_string(format!("{SERVICE_ACCOUNT_DIR}/namespace"))
.unwrap_or_else(|_| "default".to_string())
.trim()
.to_string();
let ca_pem = std::fs::read_to_string(format!("{SERVICE_ACCOUNT_DIR}/ca.crt"))
.map_err(|e| format!("failed to read service account CA certificate: {e}"))?;
let client_config = build_client_config(&ca_pem)?;
Ok(InClusterConfig { host, port, token, namespace, client_config })
}
pub(crate) fn build_client_config(ca_pem: &str) -> Result<Arc<ClientConfig>, String> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let certs = parse_pem_certificates(ca_pem)?;
if certs.is_empty() {
return Err("no certificates found in CA bundle".to_string());
}
let mut store = RootCertStore::empty();
for cert in certs {
store.add(cert).map_err(|e| format!("invalid CA certificate: {e}"))?;
}
Ok(Arc::new(
ClientConfig::builder()
.with_root_certificates(store)
.with_no_client_auth(),
))
}
pub(crate) fn parse_pem_certificates(pem: &str) -> Result<Vec<CertificateDer<'static>>, String> {
let mut certs = Vec::new();
let mut lines = pem.lines();
while let Some(line) = lines.next() {
if line.trim() != "-----BEGIN CERTIFICATE-----" {
continue;
}
let mut b64 = String::new();
let mut closed = false;
for l in lines.by_ref() {
if l.trim() == "-----END CERTIFICATE-----" {
closed = true;
break;
}
b64.push_str(l.trim());
}
if !closed {
return Err("unterminated CERTIFICATE block in PEM input".to_string());
}
certs.push(CertificateDer::from(base64_decode_standard(&b64)?));
}
Ok(certs)
}
fn base64_decode_standard(s: &str) -> Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(s.len() * 3 / 4 + 1);
let mut buf = 0u32;
let mut bits = 0u32;
for ch in s.chars() {
if ch == '=' {
break;
}
let v: u32 = match ch {
'A'..='Z' => ch as u32 - 'A' as u32,
'a'..='z' => ch as u32 - 'a' as u32 + 26,
'0'..='9' => ch as u32 - '0' as u32 + 52,
'+' => 62,
'/' => 63,
_ => return Err(format!("invalid base64 character in PEM body: '{ch}'")),
};
buf = (buf << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
}
}
Ok(out)
}
pub(crate) fn https_get(
host: &str,
port: u16,
server_name: &str,
client_config: Arc<ClientConfig>,
token: &str,
path: &str,
read_timeout: Duration,
) -> Result<String, String> {
let addr = format!("{host}:{port}");
let tcp = TcpStream::connect(&addr)
.map_err(|e| format!("ingress watcher: connect to {addr} failed: {e}"))?;
tcp.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
tcp.set_write_timeout(Some(Duration::from_secs(5))).map_err(|e| e.to_string())?;
let name = ServerName::try_from(server_name.to_string())
.map_err(|e| format!("invalid server name '{server_name}': {e}"))?;
let conn = ClientConnection::new(client_config, name)
.map_err(|e| format!("TLS setup failed: {e}"))?;
let mut stream = StreamOwned::new(conn, tcp);
let auth_header = if token.is_empty() {
String::new()
} else {
format!("Authorization: Bearer {token}\r\n")
};
let request = format!(
"GET {path} HTTP/1.1\r\nHost: {server_name}\r\n{auth_header}Accept: application/json\r\nConnection: close\r\n\r\n"
);
stream
.write_all(request.as_bytes())
.map_err(|e| format!("ingress watcher: TLS write failed: {e}"))?;
let mut buf = Vec::with_capacity(8192);
let mut tmp = [0u8; 4096];
loop {
match stream.read(&mut tmp) {
Ok(0) => break,
Ok(n) => buf.extend_from_slice(&tmp[..n]),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(format!("ingress watcher: TLS read failed: {e}")),
}
}
super::parse_http1_response(&buf)
}
pub(crate) fn tls_connect(
host: &str,
port: u16,
server_name: &str,
client_config: Arc<ClientConfig>,
read_timeout: Duration,
) -> Result<StreamOwned<ClientConnection, TcpStream>, String> {
let addr = format!("{host}:{port}");
let tcp = TcpStream::connect(&addr)
.map_err(|e| format!("ingress watcher: connect to {addr} failed: {e}"))?;
tcp.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
tcp.set_write_timeout(Some(Duration::from_secs(5))).map_err(|e| e.to_string())?;
let name = ServerName::try_from(server_name.to_string())
.map_err(|e| format!("invalid server name '{server_name}': {e}"))?;
let conn = ClientConnection::new(client_config, name)
.map_err(|e| format!("TLS setup failed: {e}"))?;
Ok(StreamOwned::new(conn, tcp))
}