download_manager/
main.rs
1use std::{collections::HashMap, num::NonZeroU8, str::FromStr};
9
10use anyhow::Context;
11use arti_client::{TorAddr, TorClient, TorClientConfig};
12use clap::Parser;
13use http_body_util::{BodyExt, Empty};
14use hyper::{
15 Method, Request, StatusCode, Uri, body::Bytes, client::conn::http1::SendRequest, header,
16 http::uri::Scheme,
17};
18use hyper_util::rt::TokioIo;
19use sha2::{Digest, Sha256};
20use tokio::{fs::OpenOptions, io::AsyncWriteExt};
21use tor_rtcompat::PreferredRuntime;
22use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
23
24#[derive(Parser)]
28struct Args {
29 #[arg(long, short, default_value = "1")]
31 connections: NonZeroU8,
32 #[clap(default_value = "14.0.7")]
34 version: String,
35}
36
37async fn connect_to_url(
39 client: &TorClient<PreferredRuntime>,
40 uri: &Uri,
41) -> anyhow::Result<SendRequest<Empty<Bytes>>> {
42 let isolated = client.isolated_client();
44
45 let connector: tokio_native_tls::TlsConnector =
47 tokio_native_tls::native_tls::TlsConnector::new()
48 .unwrap()
49 .into();
50
51 if uri.scheme() != Some(&Scheme::HTTPS) {
53 return Err(anyhow::anyhow!("URL must use HTTPS"));
54 };
55
56 let host = uri.host().ok_or(anyhow::anyhow!("Missing URL host"))?;
58
59 let tor_addr = TorAddr::from((host, uri.port_u16().unwrap_or(443)))?;
61
62 tracing::debug!("Connecting to URL using Tor");
64 let stream = isolated.connect(tor_addr).await?;
65
66 tracing::debug!("Wrapping connection in TLS");
68 let tls_connection = connector.connect(host, stream).await?;
69
70 tracing::debug!("Performing HTTP Handshake");
72 let (sender, connection) = hyper::client::conn::http1::Builder::new()
73 .handshake(TokioIo::new(tls_connection))
74 .await?;
75
76 tokio::spawn(async move {
78 if let Err(e) = connection.await {
79 tracing::debug!("Connection closed: {e}");
80 }
81 });
82
83 Ok(sender)
84}
85
86async fn get_content_length(
88 http: &mut SendRequest<Empty<Bytes>>,
89 uri: &Uri,
90) -> anyhow::Result<u64> {
91 let host = uri.host().ok_or(anyhow::anyhow!("missing host"))?;
92 tracing::debug!("Request Content-Length of resource: {uri}");
93
94 let request = Request::builder()
96 .method(Method::HEAD)
97 .header(header::HOST, host)
99 .uri(uri)
100 .body(Empty::new())?;
101 tracing::debug!("Sending request to server: {request:?}");
102
103 let response = http.send_request(request).await?;
104 tracing::debug!("Received response from server: {response:?}");
105
106 if !response.status().is_success() {
108 return Err(anyhow::anyhow!("HEAD Request failed: {response:?}"));
109 };
110
111 match response.headers().get(header::CONTENT_LENGTH) {
113 Some(header) => {
114 let length: u64 = header.to_str()?.parse()?;
115 tracing::debug!("Content-Length of resource: {length}");
116 Ok(length)
117 }
118 None => Err(anyhow::anyhow!("Missing Content-Length header")),
119 }
120}
121
122async fn get_checksums(
124 http: &mut SendRequest<Empty<Bytes>>,
125 uri: Uri,
126) -> anyhow::Result<HashMap<String, String>> {
127 let host = uri.host().ok_or(anyhow::anyhow!("missing host in uri"))?;
128 tracing::debug!("Fetching checksums from {uri}");
129
130 let request = Request::builder()
131 .method(Method::GET)
132 .header(header::HOST, host)
133 .uri(uri)
134 .body(Empty::new())?;
135
136 let mut response = http.send_request(request).await?;
137
138 if response.status() != StatusCode::OK {
139 return Err(anyhow::anyhow!(
140 "Fetching checksum failed: {}",
141 response.status()
142 ));
143 };
144
145 let mut checksums = HashMap::new();
147 let body = response.body_mut().collect().await?.to_bytes();
148 let content = std::str::from_utf8(&body)?;
149 for line in content.lines() {
150 if let Some((checksum, filename)) = line.split_once(" ") {
151 checksums.insert(filename.trim().to_string(), checksum.trim().to_string());
152 }
153 }
154 tracing::debug!("Fetched {} checksums", checksums.len());
155
156 Ok(checksums)
157}
158
159async fn request_range(
161 mut http: SendRequest<Empty<Bytes>>,
164 uri: Uri,
165 start: u64,
166 end: u64,
167) -> anyhow::Result<Bytes> {
168 let host = uri
169 .host()
170 .ok_or(anyhow::anyhow!("missing host"))?
171 .to_string();
172 tracing::debug!("Requesting range: {} to {}", start, end);
173
174 let request = Request::builder()
176 .method(Method::GET)
177 .uri(uri)
178 .header(header::HOST, host)
179 .header(header::RANGE, format!("bytes={start}-{end}"))
180 .body(Empty::new())?;
181
182 let mut response = http.send_request(request).await?;
183
184 if response.status() != StatusCode::PARTIAL_CONTENT {
186 tracing::debug!("Server did not send chunk");
187 return Err(anyhow::anyhow!(
188 "No chunk from server: {:?}",
189 response.status()
190 ));
191 };
192
193 let body = response.body_mut().collect().await?;
194 Ok(body.to_bytes())
195}
196
197#[tokio::main]
198async fn main() -> anyhow::Result<()> {
199 tracing_subscriber::registry()
200 .with(fmt::layer())
201 .with(EnvFilter::from_default_env())
202 .init();
203 let args = Args::parse();
204 let connections = args.connections.get().into();
205
206 if connections > 8 {
208 tracing::warn!(
209 "The Tor network has limited bandwidth, it is recommended to use less than 8 connections"
210 );
211 };
212
213 let filename = format!("tor-browser-linux-x86_64-{}.tar.xz", args.version);
215
216 if tokio::fs::try_exists(&filename).await? {
218 tracing::info!("File already exists, skipping download");
219 return Err(anyhow::anyhow!("File {filename} already exists"));
220 }
221
222 let url = format!(
223 "https://dist.torproject.org/torbrowser/{}/{}",
224 args.version, filename
225 );
226 let uri = Uri::from_str(url.as_str())?;
227 let checksum_url = format!(
228 "https://dist.torproject.org/torbrowser/{}/sha256sums-signed-build.txt",
229 args.version
230 );
231 let checksum_uri = Uri::from_str(checksum_url.as_str())?;
232
233 let config = TorClientConfig::default();
235
236 tracing::info!("Bootstrapping... (this may take a while)");
237 let client = TorClient::create_bootstrapped(config).await?;
238
239 let mut connection = connect_to_url(&client, &uri).await?;
241 let length = get_content_length(&mut connection, &uri).await?;
242 tracing::info!("Tor Browser Bundle has size: {length} bytes");
243
244 tracing::info!("Fetching checksum");
245 let checksums = get_checksums(&mut connection, checksum_uri).await?;
246 let checksum = checksums
247 .get(filename.as_str())
248 .ok_or(anyhow::anyhow!("Missing checksum in checksum file"))?;
249 tracing::info!("Checksum for resource: {}", &checksum);
250
251 let checksum = hex::decode(checksum).context("Failed to decode checksum")?;
252
253 let connections = std::cmp::min(connections, length);
255
256 let chunk_size = length / connections;
258 let remainder = length % connections;
259
260 let mut ranges = Vec::new();
261 let mut start = 0;
262 for i in 0..connections {
263 let extra = if i < remainder { 1 } else { 0 };
264 let end = start + chunk_size + extra - 1;
265 ranges.push((start, end));
266 start = end + 1;
267 }
268
269 tracing::info!("Creating {connections} connections");
270 let connections = ranges.iter().map(|(start, end)| async {
271 let connection = connect_to_url(&client, &uri).await?;
273 Ok::<_, anyhow::Error>((connection, *start, *end))
274 });
275 let connections = futures::future::try_join_all(connections).await?;
276
277 let mut tasks = Vec::new();
279
280 for (client, start, end) in connections {
281 let task = tokio::spawn(request_range(client, uri.clone(), start, end));
283 tasks.push(task);
284 }
285
286 let mut content = Vec::new();
288
289 let mut hasher: Sha256 = Sha256::new();
291
292 tracing::info!("Streaming download into file");
294 for task in tasks {
295 let data = task.await??;
296 hasher.update(&data);
297 content.extend_from_slice(&data);
298 }
299
300 if checksum != hasher.finalize().as_slice() {
301 return Err(anyhow::anyhow!("Mismatched checksum"));
302 }
303 tracing::info!("Checksum match!");
304
305 let mut file = OpenOptions::new()
307 .create_new(true)
308 .write(true)
309 .open(&filename)
310 .await?;
311
312 file.write_all(&content).await?;
313 tracing::info!("Saved file: {}", &filename);
314
315 Ok(())
316}