1use crate::client::Client;
2use crate::mediaconn::MediaConn;
3use anyhow::{Result, anyhow};
4use std::io::{Seek, SeekFrom, Write};
5
6pub use wacore::download::{DownloadUtils, Downloadable, MediaDecryption, MediaType};
7
8impl From<&MediaConn> for wacore::download::MediaConnection {
9 fn from(conn: &MediaConn) -> Self {
10 wacore::download::MediaConnection {
11 hosts: conn
12 .hosts
13 .iter()
14 .map(|h| wacore::download::MediaHost {
15 hostname: h.hostname.clone(),
16 })
17 .collect(),
18 auth: conn.auth.clone(),
19 }
20 }
21}
22
23struct DownloadParams {
25 direct_path: String,
26 media_key: Option<Vec<u8>>,
27 file_sha256: Vec<u8>,
28 file_enc_sha256: Option<Vec<u8>>,
29 file_length: u64,
30 media_type: MediaType,
31}
32
33impl Downloadable for DownloadParams {
34 fn direct_path(&self) -> Option<&str> {
35 Some(&self.direct_path)
36 }
37 fn media_key(&self) -> Option<&[u8]> {
38 self.media_key.as_deref()
39 }
40 fn file_enc_sha256(&self) -> Option<&[u8]> {
41 self.file_enc_sha256.as_deref()
42 }
43 fn file_sha256(&self) -> Option<&[u8]> {
44 Some(&self.file_sha256)
45 }
46 fn file_length(&self) -> Option<u64> {
47 Some(self.file_length)
48 }
49 fn app_info(&self) -> MediaType {
50 self.media_type
51 }
52}
53
54impl Client {
55 pub async fn download(&self, downloadable: &dyn Downloadable) -> Result<Vec<u8>> {
56 let requests = self.prepare_requests(downloadable).await?;
57
58 for request in requests {
59 match self.download_with_request(&request).await {
60 Ok(data) => return Ok(data),
61 Err(e) => {
62 log::warn!(
63 "Failed to download from URL {}: {:?}. Trying next host.",
64 request.url,
65 e
66 );
67 continue;
68 }
69 }
70 }
71
72 Err(anyhow!("Failed to download from all available media hosts"))
73 }
74
75 pub async fn download_to_file<W: Write + Seek + Send + Unpin>(
76 &self,
77 downloadable: &dyn Downloadable,
78 mut writer: W,
79 ) -> Result<()> {
80 let data = self.download(downloadable).await?;
81 writer.seek(SeekFrom::Start(0))?;
82 writer.write_all(&data)?;
83 Ok(())
84 }
85
86 pub async fn download_from_params(
88 &self,
89 direct_path: &str,
90 media_key: &[u8],
91 file_sha256: &[u8],
92 file_enc_sha256: &[u8],
93 file_length: u64,
94 media_type: MediaType,
95 ) -> Result<Vec<u8>> {
96 let params = DownloadParams {
97 direct_path: direct_path.to_string(),
98 media_key: Some(media_key.to_vec()),
99 file_sha256: file_sha256.to_vec(),
100 file_enc_sha256: Some(file_enc_sha256.to_vec()),
101 file_length,
102 media_type,
103 };
104 self.download(¶ms).await
105 }
106
107 async fn prepare_requests(
108 &self,
109 downloadable: &dyn Downloadable,
110 ) -> Result<Vec<wacore::download::DownloadRequest>> {
111 let media_conn = self.refresh_media_conn(false).await?;
112 let core_media_conn = wacore::download::MediaConnection::from(&media_conn);
113 DownloadUtils::prepare_download_requests(downloadable, &core_media_conn)
114 }
115
116 async fn download_with_request(
117 &self,
118 request: &wacore::download::DownloadRequest,
119 ) -> Result<Vec<u8>> {
120 let url = request.url.clone();
121 let decryption = request.decryption.clone();
122 let http_request = crate::http::HttpRequest::get(url);
123 let response = self.http_client.execute(http_request).await?;
124
125 if response.status_code >= 300 {
126 return Err(anyhow!(
127 "Download failed with status: {}",
128 response.status_code
129 ));
130 }
131
132 match decryption {
133 MediaDecryption::Encrypted {
134 media_key,
135 media_type,
136 } => {
137 tokio::task::spawn_blocking(move || {
138 DownloadUtils::decrypt_stream(&response.body[..], &media_key, media_type)
139 })
140 .await?
141 }
142 MediaDecryption::Plaintext { file_sha256 } => {
143 let body = response.body;
144 tokio::task::spawn_blocking(move || {
145 DownloadUtils::validate_plaintext_sha256(&body, &file_sha256)?;
146 Ok(body)
147 })
148 .await?
149 }
150 }
151 }
152
153 pub async fn download_to_writer<W: Write + Seek + Send + 'static>(
160 &self,
161 downloadable: &dyn Downloadable,
162 writer: W,
163 ) -> Result<W> {
164 let requests = self.prepare_requests(downloadable).await?;
165
166 let mut writer = writer;
167 let mut last_err: Option<anyhow::Error> = None;
168 for request in requests {
169 let (w, result) = self
170 .streaming_download_and_decrypt(&request, writer)
171 .await?;
172 writer = w;
173 match result {
174 Ok(()) => return Ok(writer),
175 Err(e) => {
176 log::warn!(
177 "Failed to stream-download from URL {}: {:?}. Trying next host.",
178 request.url,
179 e
180 );
181 last_err = Some(e);
182 continue;
183 }
184 }
185 }
186
187 match last_err {
188 Some(err) => Err(err),
189 None => Err(anyhow!("Failed to download from all available media hosts")),
190 }
191 }
192
193 #[allow(clippy::too_many_arguments)]
196 pub async fn download_from_params_to_writer<W: Write + Seek + Send + 'static>(
197 &self,
198 direct_path: &str,
199 media_key: &[u8],
200 file_sha256: &[u8],
201 file_enc_sha256: &[u8],
202 file_length: u64,
203 media_type: MediaType,
204 writer: W,
205 ) -> Result<W> {
206 let params = DownloadParams {
207 direct_path: direct_path.to_string(),
208 media_key: Some(media_key.to_vec()),
209 file_sha256: file_sha256.to_vec(),
210 file_enc_sha256: Some(file_enc_sha256.to_vec()),
211 file_length,
212 media_type,
213 };
214 self.download_to_writer(¶ms, writer).await
215 }
216
217 async fn streaming_download_and_decrypt<W: Write + Seek + Send + 'static>(
220 &self,
221 request: &wacore::download::DownloadRequest,
222 writer: W,
223 ) -> Result<(W, Result<()>)> {
224 let http_client = self.http_client.clone();
225 let url = request.url.clone();
226 let decryption = request.decryption.clone();
227
228 tokio::task::spawn_blocking(move || {
229 let mut writer = writer;
230
231 if let Err(e) = writer.seek(SeekFrom::Start(0)) {
233 return Ok((writer, Err(e.into())));
234 }
235
236 let result = (|| -> Result<()> {
237 let http_request = crate::http::HttpRequest::get(url);
238 let resp = http_client.execute_streaming(http_request)?;
239
240 if resp.status_code >= 300 {
241 return Err(anyhow!("Download failed with status: {}", resp.status_code));
242 }
243
244 match &decryption {
245 MediaDecryption::Encrypted {
246 media_key,
247 media_type,
248 } => {
249 DownloadUtils::decrypt_stream_to_writer(
250 resp.body,
251 media_key,
252 *media_type,
253 &mut writer,
254 )?;
255 }
256 MediaDecryption::Plaintext { file_sha256 } => {
257 DownloadUtils::copy_and_validate_plaintext_to_writer(
258 resp.body,
259 file_sha256,
260 &mut writer,
261 )?;
262 }
263 }
264 writer.seek(SeekFrom::Start(0))?;
265 Ok(())
266 })();
267
268 Ok((writer, result))
269 })
270 .await?
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use std::io::Cursor;
278
279 #[test]
280 fn process_downloaded_media_ok() {
281 let data = b"Hello media test";
282 let enc = wacore::upload::encrypt_media(data, MediaType::Image)
283 .expect("encryption should succeed");
284 let mut cursor = Cursor::new(Vec::<u8>::new());
285 let plaintext = DownloadUtils::verify_and_decrypt(
286 &enc.data_to_upload,
287 &enc.media_key,
288 MediaType::Image,
289 )
290 .expect("decryption should succeed");
291 cursor.write_all(&plaintext).expect("write should succeed");
292 assert_eq!(cursor.into_inner(), data);
293 }
294
295 #[test]
296 fn process_downloaded_media_bad_mac() {
297 let data = b"Tamper";
298 let mut enc = wacore::upload::encrypt_media(data, MediaType::Image)
299 .expect("encryption should succeed");
300 let last = enc.data_to_upload.len() - 1;
301 enc.data_to_upload[last] ^= 0x01;
302
303 let err = DownloadUtils::verify_and_decrypt(
304 &enc.data_to_upload,
305 &enc.media_key,
306 MediaType::Image,
307 )
308 .unwrap_err();
309
310 assert!(err.to_string().to_lowercase().contains("invalid mac"));
311 }
312}