Skip to main content

spotify_launcher/
apt.rs

1use crate::crypto;
2use crate::deb::{self, Pkg};
3use crate::errors::*;
4use crate::http;
5use crate::pgp;
6use crate::progress::ProgressBar;
7use sha2::{Digest, Sha256};
8use std::fs;
9use std::path::Path;
10
11pub const DEFAULT_DOWNLOAD_ATTEMPTS: usize = 5;
12
13pub struct Client {
14    client: http::Client,
15}
16
17impl Client {
18    pub fn new(timeout: Option<u64>) -> Result<Client> {
19        let client = http::Client::new(timeout)?;
20        Ok(Client { client })
21    }
22
23    pub async fn fetch_pkg_release(&self, keyring_path: &Path) -> Result<Pkg> {
24        info!("Downloading release file...");
25        let release = self
26            .client
27            .fetch("http://repository.spotify.com/dists/testing/Release")
28            .await?;
29
30        info!("Downloading signature...");
31        let sig = self
32            .client
33            .fetch("http://repository.spotify.com/dists/testing/Release.gpg")
34            .await?;
35
36        info!("Verifying pgp signature...");
37        let tmp = tempfile::tempdir().context("Failed to create temporary directory")?;
38        let tmp_path = tmp.path();
39
40        let artifact_path = tmp_path.join("artifact");
41        fs::write(&artifact_path, &release)?;
42        let sig_path = tmp_path.join("sig");
43        fs::write(&sig_path, &sig)?;
44
45        pgp::verify_sig::<&Path>(&sig_path, &artifact_path, keyring_path).await?;
46
47        info!("Signature verified successfully!");
48        let release = deb::parse_release_file(&String::from_utf8(release)?)?;
49        let arch = deb::Architecture::current();
50        let debian_arch_str = arch.to_debian_str();
51
52        if !release.architectures.iter().any(|a| a == debian_arch_str) {
53            bail!(
54                "There are no packages for your cpu's architecture (cpu={:?}, supported={:?})",
55                debian_arch_str,
56                release.architectures
57            )
58        }
59
60        let packages_path = format!("non-free/binary-{debian_arch_str}/Packages");
61
62        let packages_sha256sum = release
63            .sha256_sums
64            .get(&packages_path)
65            .context("Missing sha256sum for package index")?;
66
67        info!("Downloading package index...");
68        let pkg_index = self
69            .client
70            .fetch(&format!(
71                "http://repository.spotify.com/dists/testing/{packages_path}"
72            ))
73            .await?;
74
75        info!("Verifying with sha256sum hash...");
76        let downloaded_sha256sum = crypto::sha256sum(&pkg_index);
77        if *packages_sha256sum != downloaded_sha256sum {
78            bail!(
79                "Downloaded bytes don't match signed sha256sum (signed: {:?}, downloaded: {:?})",
80                packages_sha256sum,
81                downloaded_sha256sum
82            );
83        }
84
85        let pkg_index = deb::parse_package_index(&String::from_utf8(pkg_index)?)?;
86        debug!("Parsed package index: {:?}", pkg_index);
87        let pkg = pkg_index
88            .into_iter()
89            .find(|p| p.package == "spotify-client")
90            .context("Repository didn't contain spotify-client")?;
91
92        debug!("Found package: {:?}", pkg);
93        Ok(pkg)
94    }
95
96    async fn attempt_download(
97        &self,
98        url: &str,
99        deb: &mut Vec<u8>,
100        hasher: &mut Sha256,
101        pb: &mut ProgressBar,
102        offset: &mut Option<u64>,
103    ) -> Result<()> {
104        let mut dl = self.client.fetch_stream(url, *offset).await?;
105        while let Some(chunk) = dl.chunk().await? {
106            deb.extend(&chunk);
107            hasher.update(&chunk);
108            *offset = Some(dl.progress);
109
110            let progress = (dl.progress as f64 / dl.total as f64 * 100.0) as u64;
111            pb.update(progress).await?;
112            debug!(
113                "Download progress: {}%, {}/{}",
114                progress, dl.progress, dl.total
115            );
116        }
117        Ok(())
118    }
119
120    pub async fn download_pkg(&self, pkg: &Pkg, max_download_attempts: usize) -> Result<Vec<u8>> {
121        let filename = pkg
122            .filename
123            .rsplit_once('/')
124            .map(|(_, x)| x)
125            .unwrap_or("???");
126
127        info!(
128            "Downloading deb file for {:?} version={:?} ({:?})",
129            filename, pkg.package, pkg.version
130        );
131        let url = pkg.download_url();
132
133        // download
134        let mut pb = ProgressBar::spawn()?;
135        let mut deb = Vec::new();
136        let mut hasher = Sha256::new();
137        let mut offset = None;
138
139        let mut i: usize = 0;
140        loop {
141            // increast the counter until usize::MAX, but do not overflow
142            i = i.saturating_add(1);
143            if max_download_attempts > 0 && i > max_download_attempts {
144                // number of download attempts exceeded
145                break;
146            }
147
148            if i > 1 {
149                info!("Retrying download...");
150            }
151
152            if let Err(err) = self
153                .attempt_download(&url, &mut deb, &mut hasher, &mut pb, &mut offset)
154                .await
155            {
156                warn!("Download has failed: {err:#}");
157            } else {
158                pb.close().await?;
159
160                // verify checksum
161                info!("Verifying with sha256sum hash...");
162                let downloaded_sha256sum = format!("{:x}", hasher.finalize());
163                if pkg.sha256sum != downloaded_sha256sum {
164                    bail!(
165                        "Downloaded bytes don't match signed sha256sum (signed: {:?}, downloaded: {:?})",
166                        pkg.sha256sum,
167                        downloaded_sha256sum
168                    );
169                }
170
171                return Ok(deb);
172            }
173        }
174
175        pb.close().await?;
176        bail!("Exceeded number of retries for download");
177    }
178}