whatsapp_rust/
download.rs1use crate::client::Client;
2use crate::mediaconn::MediaConn;
3use anyhow::{Result, anyhow};
4use std::io::{Seek, SeekFrom, Write};
5
6pub use wacore::download::{DownloadUtils, Downloadable, 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
23impl Client {
24 pub async fn download(&self, downloadable: &dyn Downloadable) -> Result<Vec<u8>> {
25 let media_conn = self.refresh_media_conn(false).await?;
26
27 let core_media_conn = wacore::download::MediaConnection::from(&media_conn);
28 let requests = DownloadUtils::prepare_download_requests(downloadable, &core_media_conn)?;
29
30 for request in requests {
31 match self.download_and_decrypt_with_request(&request).await {
32 Ok(data) => return Ok(data),
33 Err(e) => {
34 log::warn!(
35 "Failed to download from URL {}: {:?}. Trying next host.",
36 request.url,
37 e
38 );
39 continue;
40 }
41 }
42 }
43
44 Err(anyhow!("Failed to download from all available media hosts"))
45 }
46
47 async fn download_and_decrypt_with_request(
48 &self,
49 request: &wacore::download::DownloadRequest,
50 ) -> Result<Vec<u8>> {
51 let url = request.url.clone();
52 let media_key = request.media_key.clone();
53 let app_info = request.app_info;
54 tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
55 let resp = ureq::get(&url).call()?;
56 let mut body = resp.into_body();
57 let reader = body.as_reader();
58 DownloadUtils::decrypt_stream(reader, &media_key, app_info)
59 })
60 .await?
61 }
62
63 pub async fn download_to_file<W: Write + Seek + Send + Unpin>(
64 &self,
65 downloadable: &dyn Downloadable,
66 mut writer: W,
67 ) -> Result<()> {
68 let media_conn = self.refresh_media_conn(false).await?;
69 let core_media_conn = wacore::download::MediaConnection::from(&media_conn);
70 let requests = DownloadUtils::prepare_download_requests(downloadable, &core_media_conn)?;
71 let mut last_err: Option<anyhow::Error> = None;
72 for req in requests {
73 match self
74 .download_and_write(&req.url, &req.media_key, req.app_info, &mut writer)
75 .await
76 {
77 Ok(()) => return Ok(()),
78 Err(e) => {
79 last_err = Some(e);
80 continue;
81 }
82 }
83 }
84 Err(last_err.unwrap_or_else(|| anyhow!("All media hosts failed")))
85 }
86
87 async fn download_and_write<W: Write + Seek + Send + Unpin>(
88 &self,
89 url: &str,
90 media_key: &[u8],
91 media_type: MediaType,
92 writer: &mut W,
93 ) -> Result<()> {
94 let url = url.to_string();
95 let media_key = media_key.to_vec();
96
97 let plaintext = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
98 let mut resp = ureq::get(&url).call()?;
99 if resp.status().as_u16() >= 300 {
100 return Err(anyhow!("Download failed with status: {}", resp.status()));
101 }
102
103 let encrypted_bytes = resp.body_mut().read_to_vec()?;
104
105 DownloadUtils::verify_and_decrypt(&encrypted_bytes, &media_key, media_type)
106 })
107 .await??;
108
109 writer.seek(SeekFrom::Start(0))?;
110 writer.write_all(&plaintext)?;
111 Ok(())
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use std::io::Cursor;
119
120 #[test]
121 fn process_downloaded_media_ok() {
122 let data = b"Hello media test";
123 let enc = wacore::upload::encrypt_media(data, MediaType::Image).unwrap();
124 let mut cursor = Cursor::new(Vec::<u8>::new());
125 let plaintext = DownloadUtils::verify_and_decrypt(
126 &enc.data_to_upload,
127 &enc.media_key,
128 MediaType::Image,
129 )
130 .unwrap();
131 cursor.write_all(&plaintext).unwrap();
132 assert_eq!(cursor.into_inner(), data);
133 }
134
135 #[test]
136 fn process_downloaded_media_bad_mac() {
137 let data = b"Tamper";
138 let mut enc = wacore::upload::encrypt_media(data, MediaType::Image).unwrap();
139 let last = enc.data_to_upload.len() - 1;
140 enc.data_to_upload[last] ^= 0x01;
141
142 let err = DownloadUtils::verify_and_decrypt(
143 &enc.data_to_upload,
144 &enc.media_key,
145 MediaType::Image,
146 )
147 .unwrap_err();
148
149 assert!(err.to_string().to_lowercase().contains("invalid mac"));
150 }
151}