use std::path::Path;
use crate::error::{GitwayError, GitwayErrorKind};
pub const DEFAULT_GITHUB_HOST: &str = "github.com";
pub const GITHUB_FALLBACK_HOST: &str = "ssh.github.com";
pub const DEFAULT_GITLAB_HOST: &str = "gitlab.com";
pub const GITLAB_FALLBACK_HOST: &str = "altssh.gitlab.com";
pub const DEFAULT_CODEBERG_HOST: &str = "codeberg.org";
pub const DEFAULT_PORT: u16 = 22;
pub const FALLBACK_PORT: u16 = 443;
#[deprecated(since = "0.2.0", note = "use GITHUB_FALLBACK_HOST instead")]
pub const FALLBACK_HOST: &str = GITHUB_FALLBACK_HOST;
pub const GITHUB_FINGERPRINTS: &[&str] = &[
"SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU", "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM", "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s", ];
pub const GITLAB_FINGERPRINTS: &[&str] = &[
"SHA256:eUXGGm1YGsMAS7vkcx6JOJdOGHPem5gQp4taiCfCLB8", "SHA256:HbW3g8zUjNSksFbqTiUWPWg2Bq1x8xdGUrliXFzSnUw", "SHA256:ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ", ];
pub const CODEBERG_FINGERPRINTS: &[&str] = &[
"SHA256:mIlxA9k46MmM6qdJOdMnAQpzGxF4WIVVL+fj+wZbw0g", "SHA256:T9FYDEHELhVkulEKKwge5aVhVTbqCW0MIRwAfpARs/E", "SHA256:6QQmYi4ppFS4/+zSZ5S4IU+4sa6rwvQ4PbhCtPEBekQ", ];
fn fingerprints_from_known_hosts(
path: &Path,
hostname: &str,
) -> Result<Vec<String>, GitwayError> {
let content = std::fs::read_to_string(path)?;
let mut fps = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.splitn(2, ' ');
let Some(host_part) = parts.next() else {
continue;
};
let Some(fp_part) = parts.next() else {
continue;
};
if host_part == hostname {
fps.push(fp_part.trim().to_owned());
}
}
Ok(fps)
}
fn default_known_hosts_path() -> Option<std::path::PathBuf> {
dirs::config_dir().map(|d| d.join("gitway").join("known_hosts"))
}
pub fn fingerprints_for_host(
host: &str,
custom_path: &Option<std::path::PathBuf>,
) -> Result<Vec<String>, GitwayError> {
let mut fps: Vec<String> = match host {
"github.com" | "ssh.github.com" => {
GITHUB_FINGERPRINTS.iter().map(|&s| s.to_owned()).collect()
}
"gitlab.com" | "altssh.gitlab.com" => {
GITLAB_FINGERPRINTS.iter().map(|&s| s.to_owned()).collect()
}
"codeberg.org" => {
CODEBERG_FINGERPRINTS.iter().map(|&s| s.to_owned()).collect()
}
_ => Vec::new(),
};
let known_hosts_path = custom_path.clone().or_else(default_known_hosts_path);
if let Some(ref path) = known_hosts_path {
if path.exists() {
let extras = fingerprints_from_known_hosts(path, host)?;
fps.extend(extras);
}
}
if fps.is_empty() {
return Err(GitwayError::new(GitwayErrorKind::InvalidConfig {
message: format!(
"no fingerprints found for host '{host}'; \
add an entry to ~/.config/gitway/known_hosts"
),
}));
}
Ok(fps)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn github_com_returns_three_fingerprints() {
let fps = fingerprints_for_host("github.com", &None).unwrap();
assert_eq!(fps.len(), 3);
}
#[test]
fn ssh_github_com_returns_same_fingerprints() {
let fps = fingerprints_for_host("ssh.github.com", &None).unwrap();
assert_eq!(fps.len(), 3);
}
#[test]
fn gitlab_com_returns_three_fingerprints() {
let fps = fingerprints_for_host("gitlab.com", &None).unwrap();
assert_eq!(fps.len(), 3);
}
#[test]
fn altssh_gitlab_com_returns_same_fingerprints_as_gitlab() {
let primary = fingerprints_for_host("gitlab.com", &None).unwrap();
let fallback = fingerprints_for_host("altssh.gitlab.com", &None).unwrap();
assert_eq!(primary, fallback);
}
#[test]
fn codeberg_org_returns_three_fingerprints() {
let fps = fingerprints_for_host("codeberg.org", &None).unwrap();
assert_eq!(fps.len(), 3);
}
#[test]
fn all_github_fingerprints_start_with_sha256_prefix() {
for fp in GITHUB_FINGERPRINTS {
assert!(fp.starts_with("SHA256:"), "malformed fingerprint: {fp}");
}
}
#[test]
fn all_gitlab_fingerprints_start_with_sha256_prefix() {
for fp in GITLAB_FINGERPRINTS {
assert!(fp.starts_with("SHA256:"), "malformed fingerprint: {fp}");
}
}
#[test]
fn all_codeberg_fingerprints_start_with_sha256_prefix() {
for fp in CODEBERG_FINGERPRINTS {
assert!(fp.starts_with("SHA256:"), "malformed fingerprint: {fp}");
}
}
#[test]
fn unknown_host_without_known_hosts_is_error() {
let result = fingerprints_for_host("git.example.com", &None);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("git.example.com"));
}
}