use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::process::Command;
use url::Url;
pub struct Teleport;
#[derive(Debug, Deserialize)]
pub struct TeleportStatusResponse {
pub active: Option<TeleportActive>,
}
#[derive(Debug, Deserialize)]
pub struct TeleportActive {
pub profile_url: String,
pub username: String,
pub cluster: String,
}
#[derive(Debug, Deserialize)]
struct TeleportNode {
spec: TeleportNodeSpec,
}
#[derive(Debug, Deserialize)]
struct TeleportNodeSpec {
hostname: String,
}
impl Teleport {
pub fn new() -> Self {
Self
}
pub fn get_status(&self) -> Result<TeleportActive> {
self.try_get_status()?
.ok_or_else(|| anyhow::anyhow!("Not logged into Teleport. Run 'tsh login' first."))
}
fn try_get_status(&self) -> Result<Option<TeleportActive>> {
let output = Command::new("tsh")
.args(["status", "--format=json"])
.output()
.context("Failed to execute tsh status")?;
if !output.status.success() {
return Ok(None);
}
let response: TeleportStatusResponse =
serde_json::from_slice(&output.stdout).context("Failed to parse tsh status output")?;
Ok(response.active)
}
pub fn get_proxy(&self, status: &TeleportActive) -> Result<String> {
let url =
Url::parse(&status.profile_url).context("Failed to parse Teleport profile URL")?;
let host = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("No host in Teleport profile URL"))?;
let port = url.port().unwrap_or(443);
if port == 443 {
Ok(host.to_string())
} else {
Ok(format!("{}:{}", host, port))
}
}
pub fn list_nodes(&self) -> Result<Vec<String>> {
let output = Command::new("tsh")
.args(["ls", "--format=json"])
.output()
.context("Failed to execute tsh ls")?;
if !output.status.success() {
bail!("tsh ls failed: {}", String::from_utf8_lossy(&output.stderr));
}
let nodes: Vec<TeleportNode> =
serde_json::from_slice(&output.stdout).context("Failed to parse tsh ls output")?;
Ok(nodes.into_iter().map(|n| n.spec.hostname).collect())
}
pub fn get_subsystem(&self, hostname: &str) -> Result<String> {
let detect_script = r#"find /usr -name "sftp-server" -type f 2>/dev/null | head -1"#;
let output = Command::new("tsh")
.args(["ssh", hostname, detect_script])
.output()
.context("Failed to detect sftp-server on remote")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let path = stdout.trim();
if path.is_empty() || !output.status.success() {
Ok("/usr/lib/openssh/sftp-server".to_string())
} else {
Ok(path.to_string())
}
}
}
impl Default for Teleport {
fn default() -> Self {
Self::new()
}
}