libverify_github/
config.rs1use anyhow::{Context, Result, bail};
2use std::process::Command;
3
4pub struct GitHubConfig {
5 pub token: String,
6 pub repo: String,
7 pub host: String,
8}
9
10impl GitHubConfig {
11 pub fn load() -> Result<Self> {
12 let token = resolve_token()?;
13 let repo = std::env::var("GH_REPO").unwrap_or_default();
14 let host = std::env::var("GH_HOST").unwrap_or_else(|_| "api.github.com".to_string());
15 validate_host(&host)?;
16 Ok(Self { token, repo, host })
17 }
18}
19
20fn validate_host(host: &str) -> Result<()> {
21 if host.is_empty() {
22 bail!("invalid host: empty");
23 }
24 if host.starts_with("localhost") {
25 bail!("invalid host: localhost not allowed");
26 }
27 if host.as_bytes()[0].is_ascii_digit() {
28 bail!("invalid host: IP addresses not allowed");
29 }
30 if host.starts_with("169.254.169.254") {
32 bail!("invalid host: IMDS endpoint not allowed");
33 }
34 if host.starts_with("fd00::") {
35 bail!("invalid host: ULA IPv6 address not allowed");
36 }
37 if host.starts_with("fe80::") {
38 bail!("invalid host: link-local IPv6 address not allowed");
39 }
40 if host.starts_with("0.0.0.0") {
41 bail!("invalid host: 0.0.0.0 not allowed");
42 }
43 if host.starts_with("[::") {
44 bail!("invalid host: unspecified IPv6 address not allowed");
45 }
46 if !host.contains('.') {
47 bail!("invalid host: must contain a dot");
48 }
49 Ok(())
50}
51
52fn normalize_secret(value: &str) -> Option<String> {
53 let trimmed = value.trim();
54 (!trimmed.is_empty()).then(|| trimmed.to_string())
55}
56
57fn non_empty_env_var(name: &str) -> Option<String> {
58 std::env::var(name)
59 .ok()
60 .and_then(|value| normalize_secret(&value))
61}
62
63fn resolve_token() -> Result<String> {
64 if let Some(token) = non_empty_env_var("GH_TOKEN") {
65 return Ok(token);
66 }
67 if let Some(token) = non_empty_env_var("GH_ENTERPRISE_TOKEN") {
68 return Ok(token);
69 }
70 let output = Command::new("gh")
72 .args(["auth", "token"])
73 .output()
74 .context("failed to run `gh auth token`")?;
75 let token = String::from_utf8(output.stdout)
76 .context("invalid UTF-8 in gh auth token output")?
77 .trim()
78 .to_string();
79 if token.is_empty() {
80 bail!("no GitHub token found. Set GH_TOKEN or run `gh auth login`");
81 }
82 Ok(token)
83}
84
85#[cfg(test)]
86mod tests {
87 use super::{non_empty_env_var, normalize_secret};
88
89 #[test]
90 fn normalize_secret_rejects_empty_and_whitespace() {
91 assert_eq!(normalize_secret(""), None);
92 assert_eq!(normalize_secret(" \n\t "), None);
93 }
94
95 #[test]
96 fn normalize_secret_trims_valid_token() {
97 assert_eq!(
98 normalize_secret(" gho_test "),
99 Some("gho_test".to_string())
100 );
101 }
102
103 #[test]
104 fn non_empty_env_var_returns_none_when_not_set() {
105 assert!(non_empty_env_var("GH_VERIFY_TEST_TOKEN__NOT_SET").is_none());
106 }
107}