use tor_llcrypto::pk::rsa::RsaIdentity;
use tor_netdoc::doc::authcert::AuthCertKeyIds;
use tor_netdoc::doc::microdesc::MdDigest;
use tor_netdoc::doc::netstatus::ConsensusFlavor;
#[cfg(feature = "routerdesc")]
use tor_netdoc::doc::routerdesc::RdDigest;
use tor_proto::circuit::ClientCirc;
#[cfg(feature = "hs-client")]
use tor_hscrypto::pk::HsBlindId;
type Result<T> = std::result::Result<T, crate::err::RequestError>;
use base64ct::{Base64Unpadded, Encoding as _};
use std::borrow::Cow;
use std::iter::FromIterator;
use std::time::{Duration, SystemTime};
use itertools::Itertools;
use crate::err::RequestError;
use crate::AnonymizedRequest;
pub(crate) mod sealed {
use super::{AnonymizedRequest, ClientCirc, Result};
pub trait RequestableInner: Send + Sync {
fn make_request(&self) -> Result<http::Request<String>>;
fn partial_response_body_ok(&self) -> bool;
fn max_response_len(&self) -> usize {
(16 * 1024 * 1024) - 1
}
fn check_circuit(&self, circ: &ClientCirc) -> Result<()> {
let _ = circ;
Ok(())
}
fn anonymized(&self) -> AnonymizedRequest;
}
}
pub trait Requestable: sealed::RequestableInner {
fn debug_request(&self) -> DisplayRequestable<'_, Self>
where
Self: Sized,
{
DisplayRequestable(self)
}
}
impl<T: sealed::RequestableInner> Requestable for T {}
pub struct DisplayRequestable<'a, R: Requestable>(&'a R);
impl<'a, R: Requestable> std::fmt::Debug for DisplayRequestable<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0.make_request())
}
}
#[derive(Clone, Debug)]
struct SkewLimit {
max_fast: Duration,
max_slow: Duration,
}
#[derive(Debug, Clone)]
pub struct ConsensusRequest {
flavor: ConsensusFlavor,
authority_ids: Vec<RsaIdentity>,
last_consensus_published: Option<SystemTime>,
last_consensus_sha3_256: Vec<[u8; 32]>,
skew_limit: Option<SkewLimit>,
}
impl ConsensusRequest {
pub fn new(flavor: ConsensusFlavor) -> Self {
ConsensusRequest {
flavor,
authority_ids: Vec::new(),
last_consensus_published: None,
last_consensus_sha3_256: Vec::new(),
skew_limit: None,
}
}
pub fn push_authority_id(&mut self, id: RsaIdentity) {
self.authority_ids.push(id);
}
pub fn push_old_consensus_digest(&mut self, d: [u8; 32]) {
self.last_consensus_sha3_256.push(d);
}
pub fn set_last_consensus_date(&mut self, when: SystemTime) {
self.last_consensus_published = Some(when);
}
pub fn old_consensus_digests(&self) -> impl Iterator<Item = &[u8; 32]> {
self.last_consensus_sha3_256.iter()
}
pub fn authority_ids(&self) -> impl Iterator<Item = &RsaIdentity> {
self.authority_ids.iter()
}
pub fn last_consensus_date(&self) -> Option<SystemTime> {
self.last_consensus_published
}
pub fn set_skew_limit(&mut self, max_fast: Duration, max_slow: Duration) {
self.skew_limit = Some(SkewLimit { max_fast, max_slow });
}
}
fn digest_list_stringify<'d, D, DL, EF>(digests: DL, encode: EF, sep: &str) -> Option<String>
where
DL: IntoIterator<Item = &'d D> + 'd,
D: PartialOrd + Ord + 'd,
EF: Fn(&'d D) -> String,
{
let mut digests = digests.into_iter().collect_vec();
if digests.is_empty() {
return None;
}
digests.sort_unstable();
let ids = digests.into_iter().map(encode).map(Cow::Owned);
let ids = Itertools::intersperse(ids, Cow::Borrowed(sep)).collect::<String>();
Some(ids)
}
impl Default for ConsensusRequest {
fn default() -> Self {
Self::new(ConsensusFlavor::Microdesc)
}
}
impl sealed::RequestableInner for ConsensusRequest {
fn make_request(&self) -> Result<http::Request<String>> {
let mut uri = "/tor/status-vote/current/consensus".to_string();
match self.flavor {
ConsensusFlavor::Ns => {}
flav => {
uri.push('-');
uri.push_str(flav.name());
}
}
let d_encode_hex = |id: &RsaIdentity| hex::encode(id.as_bytes());
if let Some(ids) = digest_list_stringify(&self.authority_ids, d_encode_hex, "+") {
uri.push('/');
uri.push_str(&ids);
}
uri.push_str(".z");
let mut req = http::Request::builder().method("GET").uri(uri);
req = add_common_headers(req, self.anonymized());
if let Some(when) = self.last_consensus_date() {
req = req.header(
http::header::IF_MODIFIED_SINCE,
httpdate::fmt_http_date(when),
);
}
if let Some(ids) = digest_list_stringify(&self.last_consensus_sha3_256, hex::encode, ", ") {
req = req.header("X-Or-Diff-From-Consensus", &ids);
}
Ok(req.body(String::new())?)
}
fn partial_response_body_ok(&self) -> bool {
false
}
fn check_circuit(&self, circ: &ClientCirc) -> Result<()> {
use tor_proto::ClockSkew::*;
let skew = circ.channel().clock_skew();
match (&self.skew_limit, &skew) {
(Some(SkewLimit { max_slow, .. }), Slow(slow)) if slow > max_slow => {
Err(RequestError::TooMuchClockSkew)
}
(Some(SkewLimit { max_fast, .. }), Fast(fast)) if fast > max_fast => {
Err(RequestError::TooMuchClockSkew)
}
(_, _) => Ok(()),
}
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Direct
}
}
#[derive(Debug, Clone, Default)]
pub struct AuthCertRequest {
ids: Vec<AuthCertKeyIds>,
}
impl AuthCertRequest {
pub fn new() -> Self {
AuthCertRequest::default()
}
pub fn push(&mut self, ids: AuthCertKeyIds) {
self.ids.push(ids);
}
pub fn keys(&self) -> impl Iterator<Item = &AuthCertKeyIds> {
self.ids.iter()
}
}
impl sealed::RequestableInner for AuthCertRequest {
fn make_request(&self) -> Result<http::Request<String>> {
if self.ids.is_empty() {
return Err(RequestError::EmptyRequest);
}
let mut ids = self.ids.clone();
ids.sort_unstable();
let ids: Vec<String> = ids
.iter()
.map(|id| {
format!(
"{}-{}",
hex::encode(id.id_fingerprint.as_bytes()),
hex::encode(id.sk_fingerprint.as_bytes())
)
})
.collect();
let uri = format!("/tor/keys/fp-sk/{}.z", &ids.join("+"));
let req = http::Request::builder().method("GET").uri(uri);
let req = add_common_headers(req, self.anonymized());
Ok(req.body(String::new())?)
}
fn partial_response_body_ok(&self) -> bool {
self.ids.len() > 1
}
fn max_response_len(&self) -> usize {
self.ids.len().saturating_mul(16 * 1024)
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Direct
}
}
impl FromIterator<AuthCertKeyIds> for AuthCertRequest {
fn from_iter<I: IntoIterator<Item = AuthCertKeyIds>>(iter: I) -> Self {
let mut req = Self::new();
for i in iter {
req.push(i);
}
req
}
}
#[derive(Debug, Clone, Default)]
pub struct MicrodescRequest {
digests: Vec<MdDigest>,
}
impl MicrodescRequest {
pub fn new() -> Self {
MicrodescRequest::default()
}
pub fn push(&mut self, d: MdDigest) {
self.digests.push(d);
}
pub fn digests(&self) -> impl Iterator<Item = &MdDigest> {
self.digests.iter()
}
}
impl sealed::RequestableInner for MicrodescRequest {
fn make_request(&self) -> Result<http::Request<String>> {
let d_encode_b64 = |d: &[u8; 32]| Base64Unpadded::encode_string(&d[..]);
let ids = digest_list_stringify(&self.digests, d_encode_b64, "-")
.ok_or(RequestError::EmptyRequest)?;
let uri = format!("/tor/micro/d/{}.z", &ids);
let req = http::Request::builder().method("GET").uri(uri);
let req = add_common_headers(req, self.anonymized());
Ok(req.body(String::new())?)
}
fn partial_response_body_ok(&self) -> bool {
self.digests.len() > 1
}
fn max_response_len(&self) -> usize {
self.digests.len().saturating_mul(8 * 1024)
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Direct
}
}
impl FromIterator<MdDigest> for MicrodescRequest {
fn from_iter<I: IntoIterator<Item = MdDigest>>(iter: I) -> Self {
let mut req = Self::new();
for i in iter {
req.push(i);
}
req
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "routerdesc")]
pub struct RouterDescRequest {
requested_descriptors: RequestedDescs,
}
#[derive(Debug, Clone)]
#[cfg(feature = "routerdesc")]
enum RequestedDescs {
AllDescriptors,
Digests(Vec<RdDigest>),
}
#[cfg(feature = "routerdesc")]
impl Default for RouterDescRequest {
fn default() -> Self {
RouterDescRequest {
requested_descriptors: RequestedDescs::Digests(Vec::new()),
}
}
}
#[cfg(feature = "routerdesc")]
impl RouterDescRequest {
pub fn all() -> Self {
RouterDescRequest {
requested_descriptors: RequestedDescs::AllDescriptors,
}
}
pub fn new() -> Self {
RouterDescRequest::default()
}
}
#[cfg(feature = "routerdesc")]
impl sealed::RequestableInner for RouterDescRequest {
fn make_request(&self) -> Result<http::Request<String>> {
let mut uri = "/tor/server/".to_string();
match self.requested_descriptors {
RequestedDescs::Digests(ref digests) => {
uri.push_str("d/");
let ids = digest_list_stringify(digests, hex::encode, "+")
.ok_or(RequestError::EmptyRequest)?;
uri.push_str(&ids);
}
RequestedDescs::AllDescriptors => {
uri.push_str("all");
}
}
uri.push_str(".z");
let req = http::Request::builder().method("GET").uri(uri);
let req = add_common_headers(req, self.anonymized());
Ok(req.body(String::new())?)
}
fn partial_response_body_ok(&self) -> bool {
match self.requested_descriptors {
RequestedDescs::Digests(ref digests) => digests.len() > 1,
RequestedDescs::AllDescriptors => true,
}
}
fn max_response_len(&self) -> usize {
match self.requested_descriptors {
RequestedDescs::Digests(ref digests) => digests.len().saturating_mul(8 * 1024),
RequestedDescs::AllDescriptors => 64 * 1024 * 1024, }
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Direct
}
}
#[cfg(feature = "routerdesc")]
impl FromIterator<RdDigest> for RouterDescRequest {
fn from_iter<I: IntoIterator<Item = RdDigest>>(iter: I) -> Self {
let digests = iter.into_iter().collect();
RouterDescRequest {
requested_descriptors: RequestedDescs::Digests(digests),
}
}
}
#[derive(Debug, Clone, Default)]
#[cfg(feature = "routerdesc")]
#[non_exhaustive]
pub struct RoutersOwnDescRequest {}
#[cfg(feature = "routerdesc")]
impl RoutersOwnDescRequest {
pub fn new() -> Self {
RoutersOwnDescRequest::default()
}
}
#[cfg(feature = "routerdesc")]
impl sealed::RequestableInner for RoutersOwnDescRequest {
fn make_request(&self) -> Result<http::Request<String>> {
let uri = "/tor/server/authority.z";
let req = http::Request::builder().method("GET").uri(uri);
let req = add_common_headers(req, self.anonymized());
Ok(req.body(String::new())?)
}
fn partial_response_body_ok(&self) -> bool {
false
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Direct
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "hs-client")]
pub struct HsDescDownloadRequest {
hsid: HsBlindId,
max_len: usize,
}
#[cfg(feature = "hs-client")]
impl HsDescDownloadRequest {
pub fn new(hsid: HsBlindId) -> Self {
const DEFAULT_HSDESC_MAX_LEN: usize = 50_000;
HsDescDownloadRequest {
hsid,
max_len: DEFAULT_HSDESC_MAX_LEN,
}
}
pub fn set_max_len(&mut self, max_len: usize) {
self.max_len = max_len;
}
}
#[cfg(feature = "hs-client")]
impl sealed::RequestableInner for HsDescDownloadRequest {
fn make_request(&self) -> Result<http::Request<String>> {
let hsid = Base64Unpadded::encode_string(self.hsid.as_ref());
let uri = format!("/tor/hs/3/{}", hsid);
let req = http::Request::builder().method("GET").uri(uri);
let req = add_common_headers(req, self.anonymized());
Ok(req.body(String::new())?)
}
fn partial_response_body_ok(&self) -> bool {
false
}
fn max_response_len(&self) -> usize {
self.max_len
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Anonymized
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "hs-service")]
pub struct HsDescUploadRequest(String);
#[cfg(feature = "hs-service")]
impl HsDescUploadRequest {
pub fn new(hsdesc: String) -> Self {
HsDescUploadRequest(hsdesc)
}
}
#[cfg(feature = "hs-service")]
impl sealed::RequestableInner for HsDescUploadRequest {
fn make_request(&self) -> Result<http::Request<String>> {
const URI: &str = "/tor/hs/3/publish";
let req = http::Request::builder().method("POST").uri(URI);
let req = add_common_headers(req, self.anonymized());
Ok(req.body(self.0.clone())?)
}
fn partial_response_body_ok(&self) -> bool {
false
}
fn max_response_len(&self) -> usize {
1024
}
fn anonymized(&self) -> AnonymizedRequest {
AnonymizedRequest::Anonymized
}
}
const UNIVERSAL_ENCODINGS: &str = "deflate, identity";
fn all_encodings() -> String {
#[allow(unused_mut)]
let mut encodings = UNIVERSAL_ENCODINGS.to_string();
#[cfg(feature = "xz")]
{
encodings += ", x-tor-lzma";
}
#[cfg(feature = "zstd")]
{
encodings += ", x-zstd";
}
encodings
}
fn add_common_headers(
req: http::request::Builder,
anon: AnonymizedRequest,
) -> http::request::Builder {
match anon {
AnonymizedRequest::Anonymized => {
req.header(http::header::ACCEPT_ENCODING, UNIVERSAL_ENCODINGS)
}
AnonymizedRequest::Direct => req.header(http::header::ACCEPT_ENCODING, all_encodings()),
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::sealed::RequestableInner;
use super::*;
#[test]
fn test_md_request() -> Result<()> {
let d1 = b"This is a testing digest. it isn";
let d2 = b"'t actually SHA-256.............";
let mut req = MicrodescRequest::default();
req.push(*d1);
assert!(!req.partial_response_body_ok());
req.push(*d2);
assert!(req.partial_response_body_ok());
assert_eq!(req.max_response_len(), 16 << 10);
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(req,
format!("GET /tor/micro/d/J3QgYWN0dWFsbHkgU0hBLTI1Ni4uLi4uLi4uLi4uLi4-VGhpcyBpcyBhIHRlc3RpbmcgZGlnZXN0LiBpdCBpc24.z HTTP/1.0\r\naccept-encoding: {}\r\n\r\n", all_encodings()));
let req2: MicrodescRequest = vec![*d1, *d2].into_iter().collect();
let ds: Vec<_> = req2.digests().collect();
assert_eq!(ds, vec![d1, d2]);
let req2 = crate::util::encode_request(&req2.make_request()?);
assert_eq!(req, req2);
Ok(())
}
#[test]
fn test_cert_request() -> Result<()> {
let d1 = b"This is a testing dn";
let d2 = b"'t actually SHA-256.";
let key1 = AuthCertKeyIds {
id_fingerprint: (*d1).into(),
sk_fingerprint: (*d2).into(),
};
let d3 = b"blah blah blah 1 2 3";
let d4 = b"I like pizza from Na";
let key2 = AuthCertKeyIds {
id_fingerprint: (*d3).into(),
sk_fingerprint: (*d4).into(),
};
let mut req = AuthCertRequest::default();
req.push(key1);
assert!(!req.partial_response_body_ok());
req.push(key2);
assert!(req.partial_response_body_ok());
assert_eq!(req.max_response_len(), 32 << 10);
let keys: Vec<_> = req.keys().collect();
assert_eq!(keys, vec![&key1, &key2]);
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(req,
format!("GET /tor/keys/fp-sk/5468697320697320612074657374696e6720646e-27742061637475616c6c79205348412d3235362e+626c616820626c616820626c6168203120322033-49206c696b652070697a7a612066726f6d204e61.z HTTP/1.0\r\naccept-encoding: {}\r\n\r\n", all_encodings()));
let req2: AuthCertRequest = vec![key1, key2].into_iter().collect();
let req2 = crate::util::encode_request(&req2.make_request()?);
assert_eq!(req, req2);
Ok(())
}
#[test]
fn test_consensus_request() -> Result<()> {
let d1 = RsaIdentity::from_bytes(
&hex::decode("03479E93EBF3FF2C58C1C9DBF2DE9DE9C2801B3E").unwrap(),
)
.unwrap();
let d2 = b"blah blah blah 12 blah blah blah";
let d3 = SystemTime::now();
let mut req = ConsensusRequest::default();
let when = httpdate::fmt_http_date(d3);
req.push_authority_id(d1);
req.push_old_consensus_digest(*d2);
req.set_last_consensus_date(d3);
assert!(!req.partial_response_body_ok());
assert_eq!(req.max_response_len(), (16 << 20) - 1);
assert_eq!(req.old_consensus_digests().next(), Some(d2));
assert_eq!(req.authority_ids().next(), Some(&d1));
assert_eq!(req.last_consensus_date(), Some(d3));
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(req,
format!("GET /tor/status-vote/current/consensus-microdesc/03479e93ebf3ff2c58c1c9dbf2de9de9c2801b3e.z HTTP/1.0\r\naccept-encoding: {}\r\nif-modified-since: {}\r\nx-or-diff-from-consensus: 626c616820626c616820626c616820313220626c616820626c616820626c6168\r\n\r\n", all_encodings(), when));
let req = ConsensusRequest::default();
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(req,
format!("GET /tor/status-vote/current/consensus-microdesc.z HTTP/1.0\r\naccept-encoding: {}\r\n\r\n", all_encodings()));
Ok(())
}
#[test]
#[cfg(feature = "routerdesc")]
fn test_rd_request_all() -> Result<()> {
let req = RouterDescRequest::all();
assert!(req.partial_response_body_ok());
assert_eq!(req.max_response_len(), 1 << 26);
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(
req,
format!(
"GET /tor/server/all.z HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
all_encodings()
)
);
Ok(())
}
#[test]
#[cfg(feature = "routerdesc")]
fn test_rd_request() -> Result<()> {
let d1 = b"at some point I got ";
let d2 = b"of writing in hex...";
let mut req = RouterDescRequest::default();
if let RequestedDescs::Digests(ref mut digests) = req.requested_descriptors {
digests.push(*d1);
}
assert!(!req.partial_response_body_ok());
if let RequestedDescs::Digests(ref mut digests) = req.requested_descriptors {
digests.push(*d2);
}
assert!(req.partial_response_body_ok());
assert_eq!(req.max_response_len(), 16 << 10);
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(req,
format!("GET /tor/server/d/617420736f6d6520706f696e74204920676f7420+6f662077726974696e6720696e206865782e2e2e.z HTTP/1.0\r\naccept-encoding: {}\r\n\r\n", all_encodings()));
let req2: RouterDescRequest = vec![*d1, *d2].into_iter().collect();
let ds: Vec<_> = match req2.requested_descriptors {
RequestedDescs::Digests(ref digests) => digests.iter().collect(),
RequestedDescs::AllDescriptors => Vec::new(),
};
assert_eq!(ds, vec![d1, d2]);
let req2 = crate::util::encode_request(&req2.make_request()?);
assert_eq!(req, req2);
Ok(())
}
#[test]
#[cfg(feature = "hs-client")]
fn test_hs_desc_download_request() -> Result<()> {
use tor_llcrypto::pk::ed25519::Ed25519Identity;
let hsid = [1, 2, 3, 4].iter().cycle().take(32).cloned().collect_vec();
let hsid = Ed25519Identity::new(hsid[..].try_into().unwrap());
let hsid = HsBlindId::from(hsid);
let req = HsDescDownloadRequest::new(hsid);
assert!(!req.partial_response_body_ok());
assert_eq!(req.max_response_len(), 50 * 1000);
let req = crate::util::encode_request(&req.make_request()?);
assert_eq!(
req,
format!("GET /tor/hs/3/AQIDBAECAwQBAgMEAQIDBAECAwQBAgMEAQIDBAECAwQ HTTP/1.0\r\naccept-encoding: {}\r\n\r\n", UNIVERSAL_ENCODINGS)
);
Ok(())
}
}