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