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