1use server::config::{ConfigError, system_config};
2
3#[derive(Debug, Clone)]
8pub struct SshConnectInfo {
9 pub hostname: String,
10 pub port: u16,
11 pub host_fingerprint: String,
12}
13
14impl SshConnectInfo {
15 fn from_env() -> Self {
16 Self {
17 hostname: std::env::var("SSH_HOSTNAME")
18 .unwrap_or_else(|_| "ssh.example.invalid".to_string()),
19 port: std::env::var("SSH_PORT")
20 .ok()
21 .and_then(|raw| raw.parse::<u16>().ok())
22 .unwrap_or(2222),
23 host_fingerprint: std::env::var("SSH_HOST_FINGERPRINT")
24 .unwrap_or_else(|_| "(not configured)".to_string()),
25 }
26 }
27}
28
29pub fn env_override(raw: Option<String>) -> Option<String> {
38 raw.filter(|value| !value.trim().is_empty())
39}
40
41#[derive(Debug, Clone)]
42pub struct Config {
43 pub site_url: String,
44
45 pub redis_url: String,
46
47 pub access_token_max_age: i64,
50
51 pub refresh_token_max_age: i64,
52
53 pub ssh: SshConnectInfo,
54}
55
56impl Config {
57 pub async fn init() -> Result<Config, ConfigError> {
58 let site_url = system_config("site_url")
59 .await?
60 .ok_or(ConfigError::NoConfig("site_url".to_string()))?
61 .to_string();
62
63 let redis_url = match env_override(std::env::var("REDIS_URL").ok()) {
70 Some(url) => url,
71 None => system_config("redis_url")
72 .await?
73 .ok_or(ConfigError::NoConfig("redis_url".to_string()))?
74 .to_string(),
75 };
76
77 let access_token_max_age = system_config("access_token_maxage")
78 .await?
79 .ok_or(ConfigError::NoConfig("access_token_maxage".to_string()))?
80 .to_string();
81
82 let refresh_token_max_age = system_config("refresh_token_maxage")
83 .await?
84 .ok_or(ConfigError::NoConfig("refresh_token_maxage".to_string()))?
85 .to_string();
86
87 Ok(Config {
88 site_url,
89 redis_url,
90 access_token_max_age: parse_age(access_token_max_age, "access_token_maxage")?,
91 refresh_token_max_age: parse_age(refresh_token_max_age, "refresh_token_maxage")?,
92 ssh: SshConnectInfo::from_env(),
93 })
94 }
95}
96
97const MAX_TOKEN_AGE_MINUTES: i64 = 525_600;
103
104fn parse_age(raw: String, field: &str) -> Result<i64, ConfigError> {
109 raw.parse::<i64>()
110 .ok()
111 .filter(|minutes| (1..=MAX_TOKEN_AGE_MINUTES).contains(minutes))
112 .ok_or_else(|| ConfigError::NoConfig(field.to_string()))
113}
114
115#[cfg(test)]
116mod parse_age_tests {
117 use super::{MAX_TOKEN_AGE_MINUTES, parse_age};
118
119 #[test]
120 fn a_lifetime_that_cannot_be_minted_is_refused() {
121 for bad in ["0", "-15", "abc", "", "200000000000", &i64::MAX.to_string()] {
122 assert!(
123 parse_age(bad.to_string(), "access_token_maxage").is_err(),
124 "{bad:?} must be refused"
125 );
126 }
127 }
128
129 #[test]
130 fn an_ordinary_lifetime_is_accepted() {
131 assert_eq!(parse_age("15".to_string(), "f").ok(), Some(15));
132 assert_eq!(
133 parse_age(MAX_TOKEN_AGE_MINUTES.to_string(), "f").ok(),
134 Some(MAX_TOKEN_AGE_MINUTES)
135 );
136 }
137}