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