use std::time::Duration;
use crate::network::onionreq::builder::{Builder, BuilderError};
use crate::network::onionreq::hop_encryption::EncryptType;
use crate::network::onionreq::response_parser::{
DecryptedResponse, ResponseParser, ResponseParserError,
};
use crate::network::routing::path_manager::{
PathCategory, PathManager, PathManagerError, PATH_HOPS,
};
use crate::network::snode_pool::SnodePool;
use crate::network::transport::{Transport, TransportError, TransportRequest};
use crate::network::types::NetworkDestination;
#[derive(Debug, Clone)]
pub struct OnionRequestRouterConfig {
pub path_strike_threshold: u8,
pub path_build_retry_limit: u8,
pub single_path_mode: bool,
pub disable_pre_build_paths: bool,
pub path_rotation_frequency_secs: u64,
pub request_timeout: Duration,
}
impl Default for OnionRequestRouterConfig {
fn default() -> Self {
Self {
path_strike_threshold: 3,
path_build_retry_limit: 10,
single_path_mode: false,
disable_pre_build_paths: false,
path_rotation_frequency_secs: 600,
request_timeout: Duration::from_secs(30),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum OnionRouteError {
#[error("path: {0}")]
Path(#[from] PathManagerError),
#[error("builder: {0}")]
Builder(#[from] BuilderError),
#[error("transport: {0}")]
Transport(#[from] TransportError),
#[error("guard returned status {0}")]
BadStatus(u16),
#[error("response: {0}")]
Response(#[from] ResponseParserError),
}
pub struct OnionRequestRouter {
pub config: OnionRequestRouterConfig,
}
impl OnionRequestRouter {
pub fn new(config: OnionRequestRouterConfig) -> Self {
Self { config }
}
pub async fn send<T: Transport>(
&self,
transport: &T,
pool: &SnodePool,
paths: &mut PathManager,
category: PathCategory,
destination: &NetworkDestination,
endpoint: &str,
body_json: &[u8],
) -> Result<DecryptedResponse, OnionRouteError> {
let path = match paths.pick_path(category) {
Some(p) => p,
None => paths.build_one(category, pool)?,
};
let mut builder = Builder::make(
destination,
endpoint,
&path.hops,
EncryptType::XChaCha20,
)?;
let blob = builder.generate_onion_blob(Some(body_json))?;
let guard = path.guard();
let url = format!("https://{}/onion_req/v2", guard.to_https_string());
let req = TransportRequest {
url,
method: "POST".to_string(),
body: blob,
headers: Vec::new(),
timeout: self.config.request_timeout,
accept_invalid_certs: true,
};
let resp = match transport.send_request(&req).await {
Ok(r) => r,
Err(e) => {
paths.record_failure(category, &guard.ed25519_pubkey);
return Err(e.into());
}
};
if resp.status_code < 200 || resp.status_code >= 300 {
paths.record_failure(category, &guard.ed25519_pubkey);
return Err(OnionRouteError::BadStatus(resp.status_code));
}
let parser = ResponseParser::from_builder(&builder)?;
let body_str = std::str::from_utf8(&resp.body).map_err(|_| {
OnionRouteError::Response(ResponseParserError::InvalidFormat(
"response body is not UTF-8".into(),
))
})?;
let decrypted = parser.decrypted_response(body_str)?;
Ok(decrypted)
}
}
pub const REQUIRED_PATH_HOPS: usize = PATH_HOPS;
#[cfg(test)]
mod tests {
use super::*;
use crate::network::routing::path_manager::{PathManager, PathManagerConfig};
use crate::network::service_node::ServiceNode;
use crate::network::snode_pool::{SnodePool, SnodePoolConfig};
use crate::network::transport::{
MockRoute, MockTransport, TransportResponse,
};
use crate::network::types::NetworkDestination;
use crate::network::swarm::INVALID_SWARM_ID;
use crate::network::key_types::Ed25519Pubkey;
use crate::crypto::ed25519 as ed;
fn make_node(id: u8) -> ServiceNode {
let mut seed = [0u8; 32];
seed[0] = id;
if id == 0 {
seed[1] = 1;
}
let (pk, _sk) = ed::ed25519_key_pair_from_seed(&seed).unwrap();
ServiceNode {
ed25519_pubkey: Ed25519Pubkey(pk),
ip: [10, 0, 0, id],
https_port: 443,
omq_port: 22000,
storage_server_version: [2, 11, 0],
swarm_id: INVALID_SWARM_ID,
requested_unlock_height: 0,
}
}
fn pool_with(n: usize) -> SnodePool {
let mut pool = SnodePool::new(SnodePoolConfig::default());
let nodes: Vec<ServiceNode> = (1..=n as u8).map(make_node).collect();
pool.add_nodes(nodes);
pool
}
#[tokio::test]
async fn test_non_2xx_status_strikes_guard() {
let pool = pool_with(20);
let mut mgr = PathManager::new(PathManagerConfig {
target_paths_per_category: 1,
path_strike_threshold: 1, });
mgr.build_up_to_target(PathCategory::Standard, &pool).unwrap();
let t = MockTransport::new();
t.route(MockRoute {
url_contains: "onion_req".into(),
body_contains: None,
response: TransportResponse {
status_code: 502,
body: b"bad".to_vec(),
headers: Vec::new(),
},
});
let router = OnionRequestRouter::new(OnionRequestRouterConfig::default());
let dest = NetworkDestination::ServiceNode(make_node(99));
let r = router
.send(
&t,
&pool,
&mut mgr,
PathCategory::Standard,
&dest,
"retrieve",
b"{}",
)
.await;
assert!(matches!(r, Err(OnionRouteError::BadStatus(502))));
assert_eq!(mgr.path_count(PathCategory::Standard), 0); }
#[tokio::test]
async fn test_transport_failure_strikes_guard() {
let pool = pool_with(20);
let mut mgr = PathManager::new(PathManagerConfig {
target_paths_per_category: 1,
path_strike_threshold: 1,
});
mgr.build_up_to_target(PathCategory::Standard, &pool).unwrap();
let t = MockTransport::new();
let router = OnionRequestRouter::new(OnionRequestRouterConfig::default());
let dest = NetworkDestination::ServiceNode(make_node(99));
let r = router
.send(
&t,
&pool,
&mut mgr,
PathCategory::Standard,
&dest,
"retrieve",
b"{}",
)
.await;
assert!(matches!(r, Err(OnionRouteError::Transport(_))));
assert_eq!(mgr.path_count(PathCategory::Standard), 0);
}
}