use crate::config::{self, ConfigEntity};
use crate::error::{Error, Result};
use crate::local_files::{self, FileSystem};
use crate::output::{CreateOutput, MergeOutput, RemoveResult};
use crate::paths;
use crate::project;
use crate::utils::io;
use serde::{Deserialize, Serialize};
use std::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Server {
#[serde(skip_deserializing, default)]
pub id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
pub host: String,
pub user: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub identity_file: Option<String>,
}
fn default_port() -> u16 {
22
}
impl Server {
pub fn is_valid(&self) -> bool {
!self.host.is_empty() && !self.user.is_empty()
}
}
impl ConfigEntity for Server {
const ENTITY_TYPE: &'static str = "server";
const DIR_NAME: &'static str = "servers";
fn id(&self) -> &str {
&self.id
}
fn set_id(&mut self, id: String) {
self.id = id;
}
fn not_found_error(id: String, suggestions: Vec<String>) -> Error {
Error::server_not_found(id, suggestions)
}
fn aliases(&self) -> &[String] {
&self.aliases
}
fn dependents(id: &str) -> Result<Vec<String>> {
let projects = project::list().unwrap_or_default();
Ok(projects
.iter()
.filter(|p| p.server_id.as_deref() == Some(id))
.map(|p| p.id.clone())
.collect())
}
}
entity_crud!(Server; merge);
pub fn find_by_host(host: &str) -> Option<Server> {
list().ok()?.into_iter().find(|s| s.host == host)
}
pub fn key_path(id: &str) -> Result<std::path::PathBuf> {
paths::key(id)
}
pub fn set_identity_file(id: &str, identity_file: Option<String>) -> Result<Server> {
let mut server = load(id)?;
server.identity_file = identity_file;
save(&server)?;
Ok(server)
}
#[derive(Debug, Clone, Serialize)]
pub struct KeyGenerateResult {
pub server: Server,
pub public_key: String,
pub identity_file: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct KeyImportResult {
pub server: Server,
pub public_key: String,
pub identity_file: String,
pub imported_from: String,
}
pub fn generate_key(server_id: &str) -> Result<KeyGenerateResult> {
load(server_id)?;
let key_path = key_path(server_id)?;
let key_path_str = key_path.to_string_lossy().to_string();
if let Some(parent) = key_path.parent() {
local_files::local().ensure_dir(parent)?;
}
let _ = std::fs::remove_file(&key_path);
let _ = std::fs::remove_file(format!("{}.pub", key_path_str));
let output = Command::new("ssh-keygen")
.args([
"-t",
"rsa",
"-b",
"4096",
"-f",
&key_path_str,
"-N",
"",
"-C",
&format!("homeboy-{}", server_id),
])
.output()
.map_err(|e| Error::internal_io(e.to_string(), Some("run ssh-keygen".to_string())))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::internal_unexpected(format!(
"ssh-keygen failed: {}",
stderr
)));
}
let server = set_identity_file(server_id, Some(key_path_str.clone()))?;
let pub_key_path = format!("{}.pub", key_path_str);
let public_key = local_files::local().read(std::path::Path::new(&pub_key_path))?;
Ok(KeyGenerateResult {
server,
public_key: public_key.trim().to_string(),
identity_file: key_path_str,
})
}
pub fn get_public_key(server_id: &str) -> Result<String> {
load(server_id)?;
let key_path = key_path(server_id)?;
let pub_key_path = format!("{}.pub", key_path.to_string_lossy());
let public_key = std::fs::read_to_string(&pub_key_path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Error::ssh_identity_file_not_found(server_id.to_string(), pub_key_path)
} else {
Error::internal_io(e.to_string(), Some("read ssh public key".to_string()))
}
})?;
Ok(public_key.trim().to_string())
}
pub fn import_key(server_id: &str, source_path: &str) -> Result<KeyImportResult> {
load(server_id)?;
let expanded_path = shellexpand::tilde(source_path).to_string();
let private_key = io::read_file(std::path::Path::new(&expanded_path), "read ssh private key")?;
if !private_key.contains("-----BEGIN") || !private_key.contains("PRIVATE KEY-----") {
return Err(Error::validation_invalid_argument(
"privateKeyPath",
"File doesn't appear to be a valid SSH private key",
Some(server_id.to_string()),
Some(vec![expanded_path.clone()]),
));
}
let output = Command::new("ssh-keygen")
.args(["-y", "-f", &expanded_path])
.output()
.map_err(|e| Error::internal_io(e.to_string(), Some("run ssh-keygen -y".to_string())))?;
if !output.status.success() {
return Err(Error::internal_unexpected(
"Failed to derive public key from private key".to_string(),
));
}
let public_key = String::from_utf8_lossy(&output.stdout).trim().to_string();
let key_path = key_path(server_id)?;
let key_path_str = key_path.to_string_lossy().to_string();
if let Some(parent) = key_path.parent() {
local_files::local().ensure_dir(parent)?;
}
io::write_file(&key_path, &private_key, "write ssh private key")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)).map_err(
|e| Error::internal_io(e.to_string(), Some("set ssh key permissions".to_string())),
)?;
}
let pub_key_path = format!("{}.pub", key_path_str);
io::write_file(
std::path::Path::new(&pub_key_path),
&public_key,
"write ssh public key",
)?;
let server = set_identity_file(server_id, Some(key_path_str.clone()))?;
Ok(KeyImportResult {
server,
public_key,
identity_file: key_path_str,
imported_from: expanded_path,
})
}
pub fn use_key(server_id: &str, key_path: &str) -> Result<Server> {
let expanded_path = shellexpand::tilde(key_path).to_string();
if !std::path::Path::new(&expanded_path).exists() {
return Err(Error::ssh_identity_file_not_found(
server_id.to_string(),
expanded_path,
));
}
set_identity_file(server_id, Some(expanded_path))
}
pub fn unset_key(server_id: &str) -> Result<Server> {
set_identity_file(server_id, None)
}