use k8s_openapi::api::core::v1::Secret; use kube::Api; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; const ERGO_HOST: &str = "irc-now-net"; const ERGO_PORT: u16 = 6667; const OPER_SECRET_NAME: &str = "irc-now-net-oper"; const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); async fn read_oper_password(kube: &kube::Client, namespace: &str) -> Result { let api: Api = Api::namespaced(kube.clone(), namespace); let secret = api .get(OPER_SECRET_NAME) .await .map_err(|e| format!("failed to get oper secret: {e}"))?; secret .data .as_ref() .and_then(|d| d.get("password")) .map(|b| String::from_utf8_lossy(&b.0).trim().to_string()) .ok_or_else(|| "missing 'password' key in oper secret".to_string()) } pub async fn ensure_ergo_account( kube: &kube::Client, namespace: &str, username: &str, password: &str, ) -> Result<(), String> { let oper_password = read_oper_password(kube, namespace).await?; tokio::time::timeout(TIMEOUT, async { let stream = TcpStream::connect((ERGO_HOST, ERGO_PORT)) .await .map_err(|e| format!("connect to ergo failed: {e}"))?; let (reader, mut writer) = stream.into_split(); let mut lines = BufReader::new(reader).lines(); writer .write_all(b"NICK ergo-admin\r\nUSER ergo-admin 0 * :admin\r\n") .await .map_err(|e| format!("write failed: {e}"))?; loop { let line = lines .next_line() .await .map_err(|e| format!("read failed: {e}"))? .ok_or("connection closed before welcome")?; if line.starts_with("PING") { let token = line.splitn(2, ' ').nth(1).unwrap_or(""); writer .write_all(format!("PONG {token}\r\n").as_bytes()) .await .map_err(|e| format!("write failed: {e}"))?; } if line.contains(" 001 ") { break; } } writer .write_all(format!("OPER admin {oper_password}\r\n").as_bytes()) .await .map_err(|e| format!("write failed: {e}"))?; loop { let line = lines .next_line() .await .map_err(|e| format!("read failed: {e}"))? .ok_or("connection closed before oper reply")?; if line.starts_with("PING") { let token = line.splitn(2, ' ').nth(1).unwrap_or(""); writer .write_all(format!("PONG {token}\r\n").as_bytes()) .await .map_err(|e| format!("write failed: {e}"))?; } if line.contains(" 381 ") { break; } if line.contains(" 491 ") || line.contains("ERR_NOOPERHOST") { return Err("OPER authentication failed".to_string()); } } writer .write_all( format!("NS SAREGISTER {username} {password}\r\n").as_bytes(), ) .await .map_err(|e| format!("write failed: {e}"))?; loop { let line = lines .next_line() .await .map_err(|e| format!("read failed: {e}"))? .ok_or("connection closed before SAREGISTER reply")?; if line.starts_with("PING") { let token = line.splitn(2, ' ').nth(1).unwrap_or(""); writer .write_all(format!("PONG {token}\r\n").as_bytes()) .await .map_err(|e| format!("write failed: {e}"))?; } if line.contains("NickServ") || line.contains("nickserv") { let lower = line.to_lowercase(); if lower.contains("successfully registered") || lower.contains("registered account") { tracing::info!("registered ergo account '{username}'"); let _ = writer.write_all(b"QUIT\r\n").await; return Ok(()); } if lower.contains("account already exists") { tracing::info!("ergo account '{username}' exists, resetting password"); writer .write_all( format!("NS SAPASSWD {username} {password}\r\n").as_bytes(), ) .await .map_err(|e| format!("write failed: {e}"))?; loop { let line2 = lines .next_line() .await .map_err(|e| format!("read failed: {e}"))? .ok_or("connection closed before SAPASSWD reply")?; if line2.starts_with("PING") { let token = line2.splitn(2, ' ').nth(1).unwrap_or(""); writer .write_all(format!("PONG {token}\r\n").as_bytes()) .await .map_err(|e| format!("write failed: {e}"))?; } if line2.contains("NickServ") || line2.contains("nickserv") { let lower2 = line2.to_lowercase(); let _ = writer.write_all(b"QUIT\r\n").await; if lower2.contains("error") || lower2.contains("fail") || lower2.contains("unknown command") { return Err(format!("SAPASSWD failed: {line2}")); } tracing::info!("reset ergo password for '{username}'"); return Ok(()); } } } if lower.contains("error") || lower.contains("fail") { let _ = writer.write_all(b"QUIT\r\n").await; return Err(format!("SAREGISTER failed: {line}")); } let _ = writer.write_all(b"QUIT\r\n").await; tracing::info!("SAREGISTER response: {line}"); return Ok(()); } } }) .await .map_err(|_| "ergo admin connection timed out".to_string())? } pub fn generate_password() -> String { use rand::Rng; const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let mut rng = rand::thread_rng(); (0..24) .map(|_| { let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect() }