Skip to main content

apt_swarm/
config.rs

1use crate::args::Args;
2use crate::errors::*;
3use crate::keyring::Keyring;
4use crate::signed::Signed;
5use bstr::BString;
6use bytes::Bytes;
7use sequoia_openpgp::armor;
8use serde::{Deserialize, Serialize};
9use std::borrow::Cow;
10use std::io::prelude::*;
11use std::net::SocketAddr;
12use std::path::{Path, PathBuf};
13use tokio::fs;
14
15#[derive(Debug, PartialEq, Default)]
16pub struct Config {
17    pub data: ConfigData,
18    pub config_path: Option<PathBuf>,
19    pub data_path: Option<PathBuf>,
20}
21
22impl Config {
23    pub async fn load_with_args(args: &Args) -> Result<Self> {
24        let (config_path, data) = if let Some(path) = &args.config {
25            if path.to_str() == Some("#") {
26                debug!("Config loading has been explicitly disabled, using default config");
27                (None, ConfigData::default())
28            } else {
29                let data = ConfigData::load_config_from(path)
30                    .await
31                    .with_context(|| anyhow!("Failed to load configuration from {:?}", path))?;
32                (Some(path.to_owned()), data)
33            }
34        } else if let Some((path, buf)) = Self::find_config().await {
35            debug!("Using configuration from {:?}", path);
36            let data = ConfigData::load_config_from_str(&buf)?;
37            (Some(path), data)
38        } else {
39            (None, ConfigData::default())
40        };
41
42        Ok(Config {
43            data,
44            config_path,
45            data_path: args.data_path.clone(),
46        })
47    }
48
49    async fn find_config() -> Option<(PathBuf, String)> {
50        for path in [
51            Self::default_config_path(),
52            Ok("/etc/apt-swarm.conf".into()),
53        ]
54        .into_iter()
55        .flatten()
56        {
57            match fs::read_to_string(&path).await {
58                Ok(buf) => return Some((path, buf)),
59                Err(err) => {
60                    debug!("Attempt to read config from {path:?} failed: {err:#}");
61                }
62            }
63        }
64
65        None
66    }
67
68    pub fn apt_swarm_path(&self) -> Result<Cow<'_, PathBuf>> {
69        let path = if let Some(path) = &self.data_path {
70            Cow::Borrowed(path)
71        } else {
72            let data_dir = dirs::data_dir().context("Failed to detect data directory")?;
73            let path = data_dir.join("apt-swarm");
74            Cow::Owned(path)
75        };
76
77        Ok(path)
78    }
79
80    pub fn database_path(&self) -> Result<PathBuf> {
81        let data_dir = self.apt_swarm_path()?;
82        let path = data_dir.join("storage");
83        Ok(path)
84    }
85
86    pub fn database_migrate_path(&self) -> Result<PathBuf> {
87        let data_dir = self.apt_swarm_path()?;
88        let path = data_dir.join("storage~");
89        Ok(path)
90    }
91
92    pub fn database_delete_path(&self) -> Result<PathBuf> {
93        let data_dir = self.apt_swarm_path()?;
94        let path = data_dir.join("storage=");
95        Ok(path)
96    }
97
98    pub fn db_socket_path(&self) -> Result<PathBuf> {
99        let data_dir = self.apt_swarm_path()?;
100        let path = data_dir.join("db.sock");
101        Ok(path)
102    }
103
104    fn default_config_path() -> Result<PathBuf> {
105        let config_dir = dirs::config_dir().context("Failed to detect config directory")?;
106        let path = config_dir.join("apt-swarm.conf");
107        Ok(path)
108    }
109
110    pub fn peerdb_path(&self) -> Result<PathBuf> {
111        let data_dir = self.apt_swarm_path()?;
112        let path = data_dir.join("peerdb.json");
113        Ok(path)
114    }
115
116    pub fn peerdb_new_path(&self) -> Result<PathBuf> {
117        let data_dir = self.apt_swarm_path()?;
118        let path = data_dir.join("peerdb.json-");
119        Ok(path)
120    }
121}
122
123#[derive(Debug, PartialEq, Default, Serialize, Deserialize)]
124pub struct ConfigData {
125    #[serde(default)]
126    pub system: System,
127    #[serde(rename = "repository", default)]
128    pub repositories: Vec<Repository>,
129}
130
131impl ConfigData {
132    pub fn load_config_from_str(buf: &str) -> Result<Self> {
133        let config = toml::from_str(buf)?;
134        Ok(config)
135    }
136
137    pub async fn load_config_from(path: &Path) -> Result<Self> {
138        let buf = fs::read_to_string(&path).await?;
139        Self::load_config_from_str(&buf)
140    }
141}
142
143#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
144pub struct System {
145    #[serde(default)]
146    pub announce: Vec<SocketAddr>,
147}
148
149#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
150pub struct Repository {
151    #[serde(default)]
152    pub urls: Vec<UrlSource>,
153    pub keyring: String,
154}
155
156impl Repository {
157    pub fn contains_fingerprint(&self, fingerprints: &[sequoia_openpgp::Fingerprint]) -> bool {
158        let Ok(keyring) = Keyring::new(self.keyring.as_bytes()) else {
159            return false;
160        };
161        fingerprints.iter().any(|fp| keyring.get(fp).is_some())
162    }
163}
164
165#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
166#[serde(untagged)]
167pub enum UrlSource {
168    Url(String),
169    Detached { content: String, sig: String },
170}
171
172impl UrlSource {
173    pub async fn fetch(&self, client: &reqwest::Client) -> Result<Vec<Signed>> {
174        match self {
175            UrlSource::Url(url) => {
176                let body = Self::fetch_data(client, url).await?;
177
178                let signed = Signed::find_all_in_text(&body)
179                    .context("Failed to parse signed message from http response")?;
180
181                if signed.is_empty() {
182                    bail!("Failed to find any signed data in http response");
183                }
184
185                Ok(signed)
186            }
187            UrlSource::Detached { content, sig } => {
188                let content = Self::fetch_data(client, content).await?;
189                if !content.ends_with(b"\n") {
190                    bail!("Detached signatures are currently only supported if the signed data ends with a newline");
191                }
192                let sig = Self::fetch_data(client, sig).await?;
193
194                let mut reader = armor::Reader::from_bytes(
195                    &sig,
196                    armor::ReaderMode::Tolerant(Some(armor::Kind::Signature)),
197                );
198
199                let mut signature = Vec::new();
200                reader.read_to_end(&mut signature)?;
201
202                Ok(vec![Signed {
203                    content: BString::new(content.into()),
204                    signature,
205                }])
206            }
207        }
208    }
209
210    async fn fetch_data(client: &reqwest::Client, url: &str) -> Result<Bytes> {
211        info!("Fetching url {:?}...", url);
212        let r = client
213            .get(url)
214            .send()
215            .await
216            .context("Failed to send request")?
217            .error_for_status()
218            .context("Received http error")?;
219        let body = r
220            .bytes()
221            .await
222            .context("Failed to download http response")?;
223        Ok(body)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
231
232    const KEYRING: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----
233
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
270LuvwORq8EBWczHS6SjLdwmSVKGKSYtl2n6nCkloVY6tONMoiCWmtcq7SJMJoyZw3
271XIf82Z39tzn/conjQcP0aIOFzww1XG7YiaTAhsDZ62kchukI52jUYm2w8cTZMEZB
272oIwIWBpmLlyaDhjIM5neY5RuL7IbIpS/fdk2lwfAwcNq6z/ri2E5RWl3AEINdLUO
273gAiVMagNJaJ+ap7kMcwOLoI2GD84mmbtDWemdUZ3HnqLHv0mb1djsWL6LwjCuOgK
274l2GDrWCh18mE+9mVB1Lo7jzYXNSHXQP6FlDE6FhGO1nNBs2IJzDvmewpnO+a/0pw
275dCerATHWtrCKwMOHrbGLSiTKEjnNt/74gKjXxdFKQkpaEfMFCeiAOFP93tKjRRhP
2765wf1JHBZ1r1+pgfZlS5F20XnM2+f/K1dWmgh+4Grx8pEHGQGLP+A22O7iWjg9pS+
277LD3yikgyGGyQxgcN3sJBQ4yxakOUDZiljm3uNyklUMCiMjTvT/F02PalQMapvA5w
2787Gwg5mSI8NDs3RtiG1rKl9Ytpdq7uHaStlHwGXBVfvayDDKnlpmndee2GBiU/hc2
279ZsYHzEWKXME/ru6EZofUFxeVdev5+9ztYJBBZCGMug5Xp3Gxh/9JUWi6F1+9qAyz
280N+O606NOXLwcmq5KZL0g
281=zyVo
282-----END PGP PUBLIC KEY BLOCK-----
283";
284
285    #[test]
286    fn test_contains_fingerprint() {
287        let repo = Repository {
288            urls: vec![],
289            keyring: KEYRING.to_string(),
290        };
291
292        // Test with matching fingerprint
293        let fingerprints = vec!["DBA36B5181D0C816F630E889D980A17457F6FB06"
294            .parse::<sequoia_openpgp::Fingerprint>()
295            .unwrap()];
296        assert!(repo.contains_fingerprint(&fingerprints));
297
298        // Test with non-matching fingerprint (Debian bookworm keys)
299        let fingerprints = vec![
300            "4D64FEC119C2029067D6E791F8D2585B8783D481"
301                .parse::<sequoia_openpgp::Fingerprint>()
302                .unwrap(),
303            "05AB90340C0C5E797F44A8C8254CF3B5AEC0A8F0"
304                .parse::<sequoia_openpgp::Fingerprint>()
305                .unwrap(),
306        ];
307        assert!(!repo.contains_fingerprint(&fingerprints));
308
309        // Test with matching and non-matching fingerprints
310        let fingerprints = vec![
311            "4D64FEC119C2029067D6E791F8D2585B8783D481"
312                .parse::<sequoia_openpgp::Fingerprint>()
313                .unwrap(),
314            "DBA36B5181D0C816F630E889D980A17457F6FB06"
315                .parse::<sequoia_openpgp::Fingerprint>()
316                .unwrap(),
317            "05AB90340C0C5E797F44A8C8254CF3B5AEC0A8F0"
318                .parse::<sequoia_openpgp::Fingerprint>()
319                .unwrap(),
320        ];
321        assert!(repo.contains_fingerprint(&fingerprints));
322
323        // Test with an empty fingerprint list
324        assert!(!repo.contains_fingerprint(&[]));
325    }
326
327    #[test]
328    fn test_parse_empty_config() {
329        let buf = "";
330        let config = ConfigData::load_config_from_str(buf).unwrap();
331        assert_eq!(config, ConfigData::default());
332    }
333
334    #[test]
335    fn test_parse_empty_system_config() {
336        let buf = "[system]";
337        let config = ConfigData::load_config_from_str(buf).unwrap();
338        assert_eq!(config, ConfigData::default());
339    }
340
341    #[test]
342    fn test_parse_system_config_announce() {
343        let buf = r#"
344        [system]
345        announce = ["[2001:db8::1]:1337", "192.168.0.1:16169"]
346        "#;
347        let config = ConfigData::load_config_from_str(buf).unwrap();
348        assert_eq!(
349            config,
350            ConfigData {
351                system: System {
352                    announce: vec![
353                        SocketAddr::new(
354                            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
355                            1337
356                        ),
357                        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)), 16169),
358                    ],
359                },
360                ..Default::default()
361            }
362        );
363    }
364}