Skip to main content

whatsapp_rust/
download.rs

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
23/// Implements `Downloadable` from raw media parameters.
24struct 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    /// Downloads and decrypts media from raw parameters without needing the original message.
87    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(&params).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    /// Downloads and decrypts media with streaming (constant memory usage).
154    ///
155    /// The entire HTTP download, decryption, and file write happen in a single
156    /// blocking thread. The writer is seeked back to position 0 before returning.
157    ///
158    /// Memory usage: ~40KB regardless of file size (8KB read buffer + decrypt state).
159    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    /// Streaming variant of `download_from_params` that writes to a writer
194    /// instead of buffering in memory.
195    #[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(&params, writer).await
215    }
216
217    /// Internal: stream download + decrypt to a writer in one blocking thread.
218    /// Always returns the writer (even on failure) so the caller can retry.
219    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            // Seek to start before each attempt so retries start fresh
232            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}