1mod circuit;
2mod crypto;
3mod directory;
4mod error;
5mod hs;
6mod tor;
7mod util;
8
9use std::sync::Mutex;
10
11use log::info;
12
13pub use circuit::{
14 build_ntor_onionskin, finish_ntor, parse_relay_body, Circuit, NtorState, RelayMessage,
15};
16pub use crypto::{
17 aes_ctr_crypt, ct_equal, hmac_sha256, kdf_tor, random_bytes, sha1, sha256, sha3_256, shake256,
18 tor_mac, x25519_public_from_private, x25519_shared, DigestKind, RelayCrypto,
19};
20pub use directory::{
21 blind_onion_key, candidate_rendezvous_relays, current_period_num, decode_http_body,
22 default_bootstrap, derive_hs_period_keys, fetch_microdescriptor_doc, hydrate_microdescriptors,
23 index_microdescriptors, link_spec_ipv4, onion_subcredential, parse_consensus,
24 parse_link_specifiers, parse_microdescriptor_fields, parse_microdescriptor_into,
25 parse_onion_address, relay_link_specifiers, relay_usable_dir, relay_usable_hsdir,
26 relay_usable_rendezvous, same_relay, select_hsdirs, serialize_link_specifiers,
27 split_microdescriptors, Consensus, HsPeriodKeys, LinkSpecifier, MicrodescriptorFields,
28 OnionAddress, Relay,
29};
30pub use error::{ensure, err, Error, Result};
31pub use hs::{
32 build_introduce1, connect_onion_service, connect_onion_service_with_retries,
33 decrypt_descriptor_layer, decrypt_hs_descriptor, fetch_hidden_service_descriptor,
34 finish_hs_ntor, parse_ed25519_cert_subject, parse_inner_descriptor, DescriptorFetchResult,
35 HiddenServiceDescriptor, HsIntroPayload, HsNtorState, IntroductionPoint, RendezvousStream,
36};
37pub use tor::{Cell, TorChannel};
38pub use util::{
39 base32_decode_onion, base64_decode, base64_encode_unpadded, from_string, hex, lower,
40 parse_hostport, put_u16, put_u32, put_u64, read_u16, read_u32, split_ws, to_string_lossy,
41 Bytes, HostPort,
42};
43
44use directory::{http_get_direct, read_file_string};
45use hs::connect_onion_service_with_retries as connect_with_retries;
46
47#[derive(Clone, Debug)]
48pub struct Options {
49 pub onion: String,
50 pub port: u16,
51 pub bootstrap: HostPort,
52 pub consensus_file: String,
53 pub verbose: bool,
54 pub stdin_mode: bool,
55 pub send_text: String,
56 pub http_get: String,
57 pub timeout_ms: i32,
58}
59
60impl Default for Options {
61 fn default() -> Self {
62 Self {
63 onion: String::new(),
64 port: 0,
65 bootstrap: default_bootstrap(),
66 consensus_file: String::new(),
67 verbose: false,
68 stdin_mode: false,
69 send_text: String::new(),
70 http_get: String::new(),
71 timeout_ms: 30000,
72 }
73 }
74}
75
76impl Options {
77 pub fn for_session(
78 bootstrap: &str,
79 consensus_file: &str,
80 timeout_ms: i32,
81 verbose: bool,
82 ) -> Result<Self> {
83 Ok(Self {
84 bootstrap: parse_hostport(bootstrap, 0)?,
85 consensus_file: consensus_file.to_string(),
86 timeout_ms,
87 verbose,
88 ..Self::default()
89 })
90 }
91}
92
93pub fn load_consensus(opt: &Options) -> Result<Consensus> {
94 if !opt.consensus_file.is_empty() {
95 info!(
96 "loading microdescriptor consensus from {}",
97 opt.consensus_file
98 );
99 let consensus = parse_consensus(&read_file_string(&opt.consensus_file)?)?;
100 info!("loaded consensus with {} relays", consensus.relays.len());
101 return Ok(consensus);
102 }
103 info!(
104 "fetching microdescriptor consensus from {}:{}",
105 opt.bootstrap.host, opt.bootstrap.port
106 );
107 let doc = http_get_direct(
108 &opt.bootstrap,
109 "/tor/status-vote/current/consensus-microdesc",
110 opt.timeout_ms,
111 )?;
112 let consensus = parse_consensus(&to_string_lossy(&doc))?;
113 info!("loaded consensus with {} relays", consensus.relays.len());
114 Ok(consensus)
115}
116
117pub fn request_bytes(
118 opt: &Options,
119 consensus: &Consensus,
120 onion: &str,
121 port: u16,
122 outbound: &[u8],
123 response_limit: usize,
124) -> Result<Bytes> {
125 let mut opt = opt.clone();
126 opt.onion = onion.to_string();
127 opt.port = port;
128 let onion_addr = parse_onion_address(&opt.onion)?;
129 let keys = derive_hs_period_keys(consensus, &onion_addr)?;
130 let desc = fetch_hidden_service_descriptor(consensus, &keys, opt.timeout_ms, opt.verbose)?;
131 let mut stream = connect_with_retries(&opt, consensus, &desc.descriptor, &keys, &[desc.guard])?;
132 const STREAM_ID: u16 = 1;
133 stream.begin(STREAM_ID, opt.port)?;
134 if !outbound.is_empty() {
135 stream.send_data(STREAM_ID, outbound)?;
136 }
137 stream.read_until_end(STREAM_ID, response_limit)
138}
139
140pub fn build_simple_http_get(onion: &str, path: &str) -> Bytes {
141 let mut normalized_path = if path.is_empty() {
142 "/".to_string()
143 } else {
144 path.to_string()
145 };
146 if !normalized_path.starts_with('/') {
147 normalized_path.insert(0, '/');
148 }
149 from_string(format!(
150 "GET {normalized_path} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n",
151 lower(onion)
152 ))
153}
154
155pub struct Session {
156 opt: Options,
157 consensus: Mutex<Consensus>,
158}
159
160impl Session {
161 pub fn new(
162 bootstrap: &str,
163 consensus_file: &str,
164 timeout_ms: i32,
165 verbose: bool,
166 ) -> Result<Self> {
167 let opt = Options::for_session(bootstrap, consensus_file, timeout_ms, verbose)?;
168 info!("initializing onionlink session");
169 let mut consensus = load_consensus(&opt)?;
170 hydrate_microdescriptors(&mut consensus, &opt.bootstrap, opt.timeout_ms, opt.verbose)?;
171 Ok(Self {
172 opt,
173 consensus: Mutex::new(consensus),
174 })
175 }
176
177 pub fn request(
178 &self,
179 onion: &str,
180 port: u16,
181 payload: &[u8],
182 response_limit: usize,
183 ) -> Result<Bytes> {
184 let consensus = self
185 .consensus
186 .lock()
187 .map_err(|_| Error::new("consensus lock poisoned"))?
188 .clone();
189 request_bytes(&self.opt, &consensus, onion, port, payload, response_limit)
190 }
191
192 pub fn http_get(
193 &self,
194 onion: &str,
195 port: u16,
196 path: &str,
197 response_limit: usize,
198 ) -> Result<Bytes> {
199 let payload = build_simple_http_get(onion, path);
200 self.request(onion, port, &payload, response_limit)
201 }
202}