onionlink-core 0.1.2

Core Tor v3 onion-service client protocol implementation for onionlink
Documentation
mod circuit;
mod crypto;
mod directory;
mod error;
mod hs;
mod tor;
mod util;

use std::sync::Mutex;

use log::info;

pub use circuit::{
    build_ntor_onionskin, finish_ntor, parse_relay_body, Circuit, NtorState, RelayMessage,
};
pub use crypto::{
    aes_ctr_crypt, ct_equal, hmac_sha256, kdf_tor, random_bytes, sha1, sha256, sha3_256, shake256,
    tor_mac, x25519_public_from_private, x25519_shared, DigestKind, RelayCrypto,
};
pub use directory::{
    blind_onion_key, candidate_rendezvous_relays, current_period_num, decode_http_body,
    default_bootstrap, derive_hs_period_keys, fetch_microdescriptor_doc, hydrate_microdescriptors,
    index_microdescriptors, link_spec_ipv4, onion_subcredential, parse_consensus,
    parse_link_specifiers, parse_microdescriptor_fields, parse_microdescriptor_into,
    parse_onion_address, relay_link_specifiers, relay_usable_dir, relay_usable_hsdir,
    relay_usable_rendezvous, same_relay, select_hsdirs, serialize_link_specifiers,
    split_microdescriptors, Consensus, HsPeriodKeys, LinkSpecifier, MicrodescriptorFields,
    OnionAddress, Relay,
};
pub use error::{ensure, err, Error, Result};
pub use hs::{
    build_introduce1, connect_onion_service, connect_onion_service_with_retries,
    decrypt_descriptor_layer, decrypt_hs_descriptor, fetch_hidden_service_descriptor,
    finish_hs_ntor, parse_ed25519_cert_subject, parse_inner_descriptor, DescriptorFetchResult,
    HiddenServiceDescriptor, HsIntroPayload, HsNtorState, IntroductionPoint, RendezvousStream,
};
pub use tor::{Cell, TorChannel};
pub use util::{
    base32_decode_onion, base64_decode, base64_encode_unpadded, from_string, hex, lower,
    parse_hostport, put_u16, put_u32, put_u64, read_u16, read_u32, split_ws, to_string_lossy,
    Bytes, HostPort,
};

use directory::{http_get_direct, read_file_string};
use hs::connect_onion_service_with_retries as connect_with_retries;

#[derive(Clone, Debug)]
pub struct Options {
    pub onion: String,
    pub port: u16,
    pub bootstrap: HostPort,
    pub consensus_file: String,
    pub verbose: bool,
    pub stdin_mode: bool,
    pub send_text: String,
    pub http_get: String,
    pub timeout_ms: i32,
}

impl Default for Options {
    fn default() -> Self {
        Self {
            onion: String::new(),
            port: 0,
            bootstrap: default_bootstrap(),
            consensus_file: String::new(),
            verbose: false,
            stdin_mode: false,
            send_text: String::new(),
            http_get: String::new(),
            timeout_ms: 30000,
        }
    }
}

impl Options {
    pub fn for_session(
        bootstrap: &str,
        consensus_file: &str,
        timeout_ms: i32,
        verbose: bool,
    ) -> Result<Self> {
        Ok(Self {
            bootstrap: parse_hostport(bootstrap, 0)?,
            consensus_file: consensus_file.to_string(),
            timeout_ms,
            verbose,
            ..Self::default()
        })
    }
}

pub fn load_consensus(opt: &Options) -> Result<Consensus> {
    if !opt.consensus_file.is_empty() {
        info!(
            "loading microdescriptor consensus from {}",
            opt.consensus_file
        );
        let consensus = parse_consensus(&read_file_string(&opt.consensus_file)?)?;
        info!("loaded consensus with {} relays", consensus.relays.len());
        return Ok(consensus);
    }
    info!(
        "fetching microdescriptor consensus from {}:{}",
        opt.bootstrap.host, opt.bootstrap.port
    );
    let doc = http_get_direct(
        &opt.bootstrap,
        "/tor/status-vote/current/consensus-microdesc",
        opt.timeout_ms,
    )?;
    let consensus = parse_consensus(&to_string_lossy(&doc))?;
    info!("loaded consensus with {} relays", consensus.relays.len());
    Ok(consensus)
}

pub fn request_bytes(
    opt: &Options,
    consensus: &Consensus,
    onion: &str,
    port: u16,
    outbound: &[u8],
    response_limit: usize,
) -> Result<Bytes> {
    let mut opt = opt.clone();
    opt.onion = onion.to_string();
    opt.port = port;
    let onion_addr = parse_onion_address(&opt.onion)?;
    let keys = derive_hs_period_keys(consensus, &onion_addr)?;
    let desc = fetch_hidden_service_descriptor(consensus, &keys, opt.timeout_ms, opt.verbose)?;
    let mut stream = connect_with_retries(&opt, consensus, &desc.descriptor, &keys, &[desc.guard])?;
    const STREAM_ID: u16 = 1;
    stream.begin(STREAM_ID, opt.port)?;
    if !outbound.is_empty() {
        stream.send_data(STREAM_ID, outbound)?;
    }
    stream.read_until_end(STREAM_ID, response_limit)
}

pub fn build_simple_http_get(onion: &str, path: &str) -> Bytes {
    let mut normalized_path = if path.is_empty() {
        "/".to_string()
    } else {
        path.to_string()
    };
    if !normalized_path.starts_with('/') {
        normalized_path.insert(0, '/');
    }
    from_string(format!(
        "GET {normalized_path} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n",
        lower(onion)
    ))
}

pub struct Session {
    opt: Options,
    consensus: Mutex<Consensus>,
}

impl Session {
    pub fn new(
        bootstrap: &str,
        consensus_file: &str,
        timeout_ms: i32,
        verbose: bool,
    ) -> Result<Self> {
        let opt = Options::for_session(bootstrap, consensus_file, timeout_ms, verbose)?;
        info!("initializing onionlink session");
        let mut consensus = load_consensus(&opt)?;
        hydrate_microdescriptors(&mut consensus, &opt.bootstrap, opt.timeout_ms, opt.verbose)?;
        Ok(Self {
            opt,
            consensus: Mutex::new(consensus),
        })
    }

    pub fn request(
        &self,
        onion: &str,
        port: u16,
        payload: &[u8],
        response_limit: usize,
    ) -> Result<Bytes> {
        let consensus = self
            .consensus
            .lock()
            .map_err(|_| Error::new("consensus lock poisoned"))?
            .clone();
        request_bytes(&self.opt, &consensus, onion, port, payload, response_limit)
    }

    pub fn http_get(
        &self,
        onion: &str,
        port: u16,
        path: &str,
        response_limit: usize,
    ) -> Result<Bytes> {
        let payload = build_simple_http_get(onion, path);
        self.request(onion, port, &payload, response_limit)
    }
}