Skip to main content

lfsx_server/
config.rs

1use axum::http::{HeaderMap, header};
2
3use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::time::Duration;
6
7use crate::model::Action;
8use crate::namespace::Namespace;
9
10#[derive(Debug, Clone)]
11pub struct Config {
12    pub bind: SocketAddr,
13    pub storage_root: PathBuf,
14    pub public_url: Option<String>,
15    pub action_lifetime: u32,
16    pub gc_grace: Duration,
17    pub staging_max_age: Duration,
18    pub auth: Auth,
19}
20
21#[derive(Debug, Clone)]
22pub enum Auth {
23    Forge {
24        provider: Provider,
25        api_url: String,
26        cache_ttl: Duration,
27        rejection_ttl: Duration,
28    },
29    Disabled,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Provider {
34    Github,
35    Gitlab,
36}
37
38impl Provider {
39    fn default_api_url(self) -> &'static str {
40        match self {
41            Self::Github => "https://api.github.com",
42            Self::Gitlab => "https://gitlab.com/api/v4",
43        }
44    }
45
46    fn api_url_variable(self) -> &'static str {
47        match self {
48            Self::Github => "LFSX_GITHUB_API_URL",
49            Self::Gitlab => "LFSX_GITLAB_API_URL",
50        }
51    }
52}
53
54const CACHE_TTL: Duration = Duration::from_secs(60);
55const REJECTION_TTL: Duration = Duration::from_secs(10);
56const GC_GRACE: Duration = Duration::from_secs(14 * 24 * 60 * 60);
57const STAGING_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
58
59impl Config {
60    pub fn from_env() -> Self {
61        let bind = std::env::var("LFSX_BIND")
62            .ok()
63            .and_then(|raw| raw.parse().ok())
64            .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 8080)));
65
66        let storage_root = std::env::var("LFSX_STORAGE_ROOT")
67            .map(PathBuf::from)
68            .unwrap_or_else(|_| PathBuf::from("/var/lib/lfsx"));
69
70        let public_url = std::env::var("LFSX_PUBLIC_URL")
71            .ok()
72            .filter(|url| !url.is_empty())
73            .map(|url| url.trim_end_matches('/').to_owned());
74
75        Self {
76            bind,
77            storage_root,
78            public_url,
79            action_lifetime: 1800,
80            gc_grace: seconds("LFSX_GC_GRACE").unwrap_or(GC_GRACE),
81            staging_max_age: seconds("LFSX_STAGING_MAX_AGE").unwrap_or(STAGING_MAX_AGE),
82            auth: Auth::from_env(),
83        }
84    }
85
86    pub fn base_url(&self, headers: &HeaderMap) -> String {
87        if let Some(configured) = &self.public_url {
88            return configured.clone();
89        }
90
91        let scheme = headers
92            .get("x-forwarded-proto")
93            .and_then(|value| value.to_str().ok())
94            .and_then(|value| value.split(',').next())
95            .map(str::trim)
96            .filter(|scheme| !scheme.is_empty())
97            .unwrap_or("http");
98
99        let authority = headers
100            .get(header::HOST)
101            .and_then(|value| value.to_str().ok())
102            .map(str::trim)
103            .filter(|host| !host.is_empty())
104            .unwrap_or("localhost");
105
106        format!("{scheme}://{authority}")
107    }
108
109    pub fn object_url(&self, base: &str, ns: &Namespace, oid: &str) -> String {
110        format!("{base}/{ns}/objects/{oid}")
111    }
112
113    pub fn verify_url(&self, base: &str, ns: &Namespace) -> String {
114        format!("{base}/{ns}/objects/verify")
115    }
116
117    pub fn action(&self, href: String) -> Action {
118        Action {
119            href,
120            expires_in: self.action_lifetime,
121        }
122    }
123}
124
125impl Auth {
126    fn from_env() -> Self {
127        if std::env::var("LFSX_AUTH").as_deref() == Ok("disabled") {
128            tracing::warn!(
129                "LFSX_AUTH=disabled — every request is accepted, run this on a trusted network only"
130            );
131            return Self::Disabled;
132        }
133
134        let provider = match std::env::var("LFSX_AUTH").as_deref() {
135            Ok("gitlab") => Provider::Gitlab,
136            _ => Provider::Github,
137        };
138
139        let api_url = std::env::var(provider.api_url_variable())
140            .unwrap_or_else(|_| provider.default_api_url().to_owned())
141            .trim_end_matches('/')
142            .to_owned();
143
144        Self::Forge {
145            provider,
146            api_url,
147            cache_ttl: seconds("LFSX_AUTH_CACHE_TTL").unwrap_or(CACHE_TTL),
148            rejection_ttl: seconds("LFSX_AUTH_REJECTION_TTL").unwrap_or(REJECTION_TTL),
149        }
150    }
151}
152
153fn seconds(variable: &str) -> Option<Duration> {
154    std::env::var(variable)
155        .ok()
156        .and_then(|raw| raw.parse().ok())
157        .map(Duration::from_secs)
158}