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 max_object_size: Option<u64>,
19    pub repo_quota: Option<u64>,
20    pub auth: Auth,
21}
22
23#[derive(Debug, Clone)]
24pub enum Auth {
25    Forge {
26        provider: Provider,
27        api_url: String,
28        cache_ttl: Duration,
29        rejection_ttl: Duration,
30    },
31    Disabled,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Provider {
36    Github,
37    Gitlab,
38}
39
40impl Provider {
41    fn default_api_url(self) -> &'static str {
42        match self {
43            Self::Github => "https://api.github.com",
44            Self::Gitlab => "https://gitlab.com/api/v4",
45        }
46    }
47
48    fn api_url_variable(self) -> &'static str {
49        match self {
50            Self::Github => "LFSX_GITHUB_API_URL",
51            Self::Gitlab => "LFSX_GITLAB_API_URL",
52        }
53    }
54}
55
56const CACHE_TTL: Duration = Duration::from_secs(60);
57const REJECTION_TTL: Duration = Duration::from_secs(10);
58const GC_GRACE: Duration = Duration::from_secs(14 * 24 * 60 * 60);
59const STAGING_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
60
61impl Config {
62    pub fn from_env() -> Self {
63        let bind = std::env::var("LFSX_BIND")
64            .ok()
65            .and_then(|raw| raw.parse().ok())
66            .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 8080)));
67
68        let storage_root = std::env::var("LFSX_STORAGE_ROOT")
69            .map(PathBuf::from)
70            .unwrap_or_else(|_| PathBuf::from("/var/lib/lfsx"));
71
72        let public_url = std::env::var("LFSX_PUBLIC_URL")
73            .ok()
74            .filter(|url| !url.is_empty())
75            .map(|url| url.trim_end_matches('/').to_owned());
76
77        Self {
78            bind,
79            storage_root,
80            public_url,
81            action_lifetime: 1800,
82            gc_grace: seconds("LFSX_GC_GRACE").unwrap_or(GC_GRACE),
83            staging_max_age: seconds("LFSX_STAGING_MAX_AGE").unwrap_or(STAGING_MAX_AGE),
84            max_object_size: bytes("LFSX_MAX_OBJECT_SIZE"),
85            repo_quota: bytes("LFSX_REPO_QUOTA"),
86            auth: Auth::from_env(),
87        }
88    }
89
90    pub fn base_url(&self, headers: &HeaderMap) -> String {
91        if let Some(configured) = &self.public_url {
92            return configured.clone();
93        }
94
95        let scheme = headers
96            .get("x-forwarded-proto")
97            .and_then(|value| value.to_str().ok())
98            .and_then(|value| value.split(',').next())
99            .map(str::trim)
100            .filter(|scheme| !scheme.is_empty())
101            .unwrap_or("http");
102
103        let authority = headers
104            .get(header::HOST)
105            .and_then(|value| value.to_str().ok())
106            .map(str::trim)
107            .filter(|host| !host.is_empty())
108            .unwrap_or("localhost");
109
110        format!("{scheme}://{authority}")
111    }
112
113    pub fn object_url(&self, base: &str, ns: &Namespace, oid: &str) -> String {
114        format!("{base}/{ns}/objects/{oid}")
115    }
116
117    pub fn verify_url(&self, base: &str, ns: &Namespace) -> String {
118        format!("{base}/{ns}/objects/verify")
119    }
120
121    pub fn action(&self, href: String) -> Action {
122        Action {
123            href,
124            expires_in: self.action_lifetime,
125        }
126    }
127}
128
129impl Auth {
130    fn from_env() -> Self {
131        if std::env::var("LFSX_AUTH").as_deref() == Ok("disabled") {
132            tracing::warn!(
133                "LFSX_AUTH=disabled — every request is accepted, run this on a trusted network only"
134            );
135            return Self::Disabled;
136        }
137
138        let provider = match std::env::var("LFSX_AUTH").as_deref() {
139            Ok("gitlab") => Provider::Gitlab,
140            _ => Provider::Github,
141        };
142
143        let api_url = std::env::var(provider.api_url_variable())
144            .unwrap_or_else(|_| provider.default_api_url().to_owned())
145            .trim_end_matches('/')
146            .to_owned();
147
148        Self::Forge {
149            provider,
150            api_url,
151            cache_ttl: seconds("LFSX_AUTH_CACHE_TTL").unwrap_or(CACHE_TTL),
152            rejection_ttl: seconds("LFSX_AUTH_REJECTION_TTL").unwrap_or(REJECTION_TTL),
153        }
154    }
155}
156
157// Unset means unlimited, which is what a server on its own volume wants. Zero
158// would refuse every push, so it is read as a typo rather than as a policy
159// nobody would choose deliberately.
160fn bytes(variable: &str) -> Option<u64> {
161    let configured = std::env::var(variable).ok()?.trim().parse().ok()?;
162
163    if configured == 0 {
164        tracing::warn!("{variable}=0 would refuse every upload — ignoring it");
165        return None;
166    }
167
168    Some(configured)
169}
170
171fn seconds(variable: &str) -> Option<Duration> {
172    std::env::var(variable)
173        .ok()
174        .and_then(|raw| raw.parse().ok())
175        .map(Duration::from_secs)
176}