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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
|
#![warn(clippy::missing_docs_in_private_items)]
//! # download-manager
//! Use Tor to download the Tor Browser Bundle
//!
//! ### Intro
//! This is a project intended to illustrate how Arti can be used to tunnel an HTTPS
//! based project through Tor and also some of the design choices that go into making that
//! happen, most notably, the usage of isolated clients to create different connections
//! which won't lock each other up or run into some Arti shared state issues.
//!
//! ### Usage
//! Simply run the program:
//! `cargo run`
//!
//! The program will then attempt to create new Tor connections and download the Linux version of
//! the Tor Browser Bundle in chunks using [HTTP Range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
//! in order to overcome the relatively slow connections that the Tor network provides.
//! It is currently capped to six concurrent connections in order to respect the Tor network's bandwidth
//! The Tor Browser Bundle is saved as `download.tar.xz`
//!
//! ### Disclaimer
//! The download manager showcased is not really meant for production. It is simply an example of how Arti
//! can be utilized. Many features, like resumeable downloads, aren't present. Don't use it for any real
//! usage other than academic
use arti_client::{TorClient, TorClientConfig};
use arti_hyper::*;
use futures::future::join_all;
use hyper::{Body, Client, Method, Request, StatusCode, Uri};
use sha2::{Digest, Sha256};
use std::fs::OpenOptions;
use std::io::Write;
use std::str::FromStr;
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
use tls_api_native_tls::TlsConnector;
use tor_rtcompat::PreferredRuntime;
use tracing::{debug, error, info, warn};
/// REQSIZE is just the size of each chunk we get from a particular circuit
const REQSIZE: u64 = 1024 * 1024;
/// This denotes the version of Tor Browser to get
///
/// It also helps us create the URL to get the SHA256 sums for the browser we download
const TOR_VERSION: &str = "12.5.2";
/// Number of simultaneous connections that are made
// TODO: make this user configurable
const MAX_CONNECTIONS: usize = 6;
/// Number of retries to make if a particular request failed
const MAX_RETRIES: usize = 6;
#[derive(thiserror::Error, Debug)]
#[error("Download Manager Error")]
/// Enum storing all the Errors that our program can raise
enum DownloadMgrError {
#[error("Download failed due to unspecified reason")]
/// Blanket download error to catch almost all download errors
DownloadError,
#[error("Got unexpected status code")]
/// Error to represent an unexpected status code from the network
RequestFailed {
/// The status code that we got instead of the intended one
status: StatusCode,
},
/// Error to represent raw bytes properly
#[error("Unable to read body into bytes")]
BodyDownload {
/// Error raised while reading body into bytes, wraps [hyper::Error]`
error: hyper::Error,
},
#[error("Failed to get a connection from pool")]
/// Used to denote a failed .get() request from `Vec<Client>`
ConnectionError,
}
/// Create a single TorClient which will be used to spawn isolated connections
///
/// This Client uses the default config with no other changes
async fn create_tor_client() -> Result<TorClient<PreferredRuntime>, arti_client::Error> {
let config = TorClientConfig::default();
TorClient::create_bootstrapped(config).await
}
/// Creates a `hyper::Client` for sending HTTPS requests over Tor
///
/// Note that it first creates an isolated circuit from the `TorClient`
/// passed into it, this is generally an Arti best practice
async fn build_tor_hyper_client(
baseconn: &TorClient<PreferredRuntime>,
) -> anyhow::Result<Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>>> {
let tor_client = baseconn.isolated_client();
let tls_connector = TlsConnector::builder()?.build()?;
let connector = ArtiHttpConnector::new(tor_client, tls_connector);
Ok(hyper::Client::builder().build::<_, Body>(connector))
}
/// Get the size of file to be downloaded so we can prep main loop
async fn get_content_length(
url: String,
baseconn: &TorClient<PreferredRuntime>,
) -> anyhow::Result<u64> {
let http = build_tor_hyper_client(baseconn).await?;
let uri = Uri::from_str(url.as_str())?;
debug!("Requesting content length of {} via Tor...", url);
// Create a new request
let req = Request::builder()
.method(Method::HEAD)
.uri(uri)
.body(Body::empty())?;
let resp = http.request(req).await?;
// Get Content-Length
match resp.headers().get("Content-Length") {
Some(raw_length) => {
let length = raw_length.to_str()?.parse::<u64>()?;
debug!("Content-Length of resource: {}", length);
// Return it after a suitable typecast
Ok(length)
}
None => Err(DownloadMgrError::DownloadError.into()),
}
}
/// Gets a portion of the file from the server and store it in a Vec if successful
///
/// Note that it returns a Result to denote any network issues that may have arisen from the request
async fn request_range(
url: &String,
start: usize,
end: usize,
http: &Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>>,
) -> anyhow::Result<Vec<u8>> {
warn!("Requesting {} via Tor...", url);
let uri = Uri::from_str(url)?;
let partial_req_value = format!("bytes={}-{}", start, end);
// GET the contents of URL from byte offset "start" to "end"
let req = Request::builder()
.method(Method::GET)
.uri(uri)
.header("Range", partial_req_value)
.body(Body::default())?;
let mut resp = http.request(req).await?;
// Got partial content, this is good
if resp.status() == hyper::StatusCode::PARTIAL_CONTENT {
debug!("Good request, getting partial content...");
// Get the body of the response
return match hyper::body::to_bytes(resp.body_mut()).await {
Ok(bytes) => Ok(bytes.to_vec()),
Err(e) => Err(DownloadMgrError::BodyDownload { error: e }.into()),
};
}
// Got something else, return an Error
warn!("Non 206 Status code: {}", resp.status());
Err(DownloadMgrError::RequestFailed {
status: resp.status(),
}
.into())
}
/// Gets the expected SHA256 sum of the download file from the server
///
/// Note that it returns a Result to denote any network issues that may have arisen from the request
async fn request_sha256_sum(
url: String,
http: &Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>>,
file_name: &str,
) -> anyhow::Result<String> {
let uri = Uri::from_str(url.as_str())?;
// GET the contents of URL from byte offset "start" to "end"
let req = Request::builder()
.method(Method::GET)
.uri(uri)
.body(Body::default())?;
let mut resp = http.request(req).await?;
if resp.status() == hyper::StatusCode::OK {
debug!("Good request, getting content...");
// Get the body of the response
return match hyper::body::to_bytes(resp.body_mut()).await {
Ok(bytes) => {
let bytes_vec = bytes.to_vec();
let str_body = std::str::from_utf8(&bytes_vec)?;
for line in str_body.lines() {
let parts: Vec<&str> = line.splitn(2, " ").collect();
if parts[1] == file_name {
return Ok(parts[0].to_string());
}
}
Err(DownloadMgrError::DownloadError.into())
}
Err(e) => Err(DownloadMgrError::BodyDownload { error: e }.into()),
};
}
// Got something else, return an Error
warn!("Non 200 Status code: {}", resp.status());
Err(DownloadMgrError::RequestFailed {
status: resp.status(),
}
.into())
}
/// Backoff function for determining timeout duration for each repeated download try
fn wait_time_for_iteration(iteration: usize) -> u64 {
1000.min(500 + 100 * iteration as u64)
}
/// Wrapper around [request_range] in order to overcome network issues
///
/// We try a maximum of [MAX_RETRIES] to get the portion of the file we require
///
/// If we are successful, we return the bytes to be later written to disk, else we simply return None
async fn download_segment(
url: String,
start: usize,
end: usize,
newhttp: Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>>,
) -> Result<Vec<u8>, crate::DownloadMgrError> {
for trial in 0..MAX_RETRIES {
if trial != 0 {
tokio::time::sleep(std::time::Duration::from_millis(wait_time_for_iteration(
trial,
)))
.await;
}
// request via new Tor connection
match request_range(&url, start, end, &newhttp).await {
// save to disk
Ok(body) => {
return Ok(body);
}
// retry if we failed
Err(e) => {
warn!(
"Error while trying to get a segment: {}, retrying...",
e.to_string()
);
}
}
}
Err(DownloadMgrError::DownloadError)
}
/// Main method which brings it all together
///
/// Summary:
///
/// 1. Get the SHA256 checksum of the Tor Browser Bundle for later
/// verification of the downloaded data
///
/// 2. Create [MAX_CONNECTIONS] number of connections, these will be all
/// that is used for the main loop of the program
///
/// 3. Get content length of the Tor Browser Bundle so we know how
/// many loops to run
///
/// 4. Create the main loop of the program; it simply cycles through the
/// connections we initialized in step 2 and makes a request with them for the
/// bulk of the payload we request from the network
///
/// 5. Check SHA256 checksum of the file in memory and compare it to the
/// expected value we got from the Tor Project's website
///
/// 6. Write all that data to the disk
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
// generate the URLs and file names from the version number
// and some known conventions
let download_file_name = format!("tor-browser-linux64-{}_ALL.tar.xz", TOR_VERSION);
let url = format!(
"https://dist.torproject.org/torbrowser/{}/{}",
TOR_VERSION, download_file_name
);
let verification_url = format!(
"https://dist.torproject.org/torbrowser/{}/sha256sums-signed-build.txt",
TOR_VERSION
);
let baseconn = create_tor_client().await?;
let length = get_content_length(url.clone(), &baseconn).await?;
let sha_http_client = build_tor_hyper_client(&baseconn).await?;
let expected_sha256sum =
request_sha256_sum(verification_url, &sha_http_client, &download_file_name).await?;
debug!("Expected SHA256 sum of file: {}", expected_sha256sum);
// Initialize the connections we will use for this download
let mut connections: Vec<Client<_>> = Vec::with_capacity(MAX_CONNECTIONS);
for _ in 0..MAX_CONNECTIONS {
let newhttp = build_tor_hyper_client(&baseconn).await?;
connections.push(newhttp);
}
// determine the amount of iterations required
let steps = length / REQSIZE;
let mut downloadtasks = Vec::with_capacity(steps as usize);
let mut start = 0;
let mut taskid = 0;
while start < length as usize {
// the upper bound of what block we need from the server
let end = (start + (REQSIZE as usize) - 1).min(length as usize);
let http = connections
.get(taskid)
.ok_or(DownloadMgrError::ConnectionError)?;
let newhttp = http.clone();
let urlclone = url.clone();
downloadtasks.push(tokio::spawn(async move {
download_segment(urlclone, start, end, newhttp)
.await
.map(|body| (start, body))
}));
start = end + 1;
taskid = (taskid + 1) % MAX_CONNECTIONS;
}
let results_options: Vec<Result<_, _>> = join_all(downloadtasks)
.await
.into_iter()
.flatten()
.collect();
// if we got an Error from network operations, that means we don't have entire file
// thus we delete the partial file and print an error
let has_err = results_options.iter().any(|result_op| result_op.is_err());
if has_err {
error!("Possible missing chunk! Aborting");
return Ok(());
}
let mut results: Vec<_> = results_options
.into_iter()
.filter_map(|result| result.ok())
.collect();
results.sort_by(|a, b| a.0.cmp(&b.0));
let mut file_vec: Vec<u8> = Vec::new();
// write all chunks to memory representation of file, checking along the
// way if the offsets match our expectations
let mut start_check = 0;
for (start, chunk) in results.iter() {
if *start != start_check {
error!("Mismatch in expected and observed offset! Aborting");
return Ok(());
}
let end_check = start_check + (REQSIZE as usize) - 1;
debug!(
"Writing chunk offset {} to memory representation of file...",
start
);
file_vec.extend(chunk);
start_check = end_check + 1;
}
// Verify downloaded content's checksum
let mut sha256 = Sha256::new();
sha256.update(&file_vec);
let hash_result = sha256.finalize();
let observed_hash = format!("{:x}", hash_result);
if observed_hash != expected_sha256sum {
error!("Incorrect SHA 256 sum in download! Aborting");
return Ok(());
}
// Write validated data to disk
info!("Creating download file");
let mut fd = OpenOptions::new()
.write(true)
.create(true)
.open(&download_file_name)?;
debug!("Created file, now writing downloaded content to disk...");
fd.write_all(&file_vec)?;
Ok(())
}
|