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 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 i = i.saturating_add(1);
143 if max_download_attempts > 0 && i > max_download_attempts {
144 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 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}