use crate::server::Server;
use crate::server::conn::{NO_PEER_ADDR as I2P_PEER_ADDR, serve_connection};
use crate::server::http::hyper_handler;
use std::path::PathBuf;
use std::sync::Arc;
use tachyon_i2p::{CryptoType, I2pRouter, SigType};
#[cfg(feature = "tls")]
use tokio_rustls::TlsAcceptor;
#[derive(Clone)]
enum I2pTls {
None,
#[cfg(feature = "cert-gen")]
SelfSigned,
#[cfg(feature = "tls")]
Custom(Arc<rustls::ServerConfig>),
}
impl std::fmt::Debug for I2pTls {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => f.write_str("None"),
#[cfg(feature = "cert-gen")]
Self::SelfSigned => f.write_str("SelfSigned"),
#[cfg(feature = "tls")]
Self::Custom(_) => f.write_str("Custom(..)"),
}
}
}
type OnReadyHook = Box<dyn FnOnce(&str) + Send>;
pub struct I2pConfig {
nickname: String,
data_dir: Option<PathBuf>,
sig_type: SigType,
encryption_types: Vec<CryptoType>,
tls: I2pTls,
on_ready: Option<OnReadyHook>,
}
impl std::fmt::Debug for I2pConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("I2pConfig")
.field("nickname", &self.nickname)
.field("data_dir", &self.data_dir)
.field("sig_type", &self.sig_type)
.field("encryption_types", &self.encryption_types)
.field("tls", &self.tls)
.finish_non_exhaustive()
}
}
impl I2pConfig {
#[must_use]
pub fn new(nickname: impl Into<String>) -> Self {
Self {
nickname: nickname.into(),
data_dir: None,
sig_type: SigType::default(),
encryption_types: Vec::new(),
tls: I2pTls::None,
on_ready: None,
}
}
#[must_use]
pub fn data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.data_dir = Some(dir.into());
self
}
#[must_use]
pub const fn signature_type(mut self, sig: SigType) -> Self {
self.sig_type = sig;
self
}
#[must_use]
pub fn crypto_type(mut self, crypto: CryptoType) -> Self {
self.encryption_types = vec![crypto];
self
}
#[must_use]
pub fn encryption_types(mut self, types: &[CryptoType]) -> Self {
self.encryption_types = types.to_vec();
self
}
#[cfg(feature = "tls")]
#[must_use]
pub fn tls_config(mut self, config: rustls::ServerConfig) -> Self {
self.tls = I2pTls::Custom(Arc::new(config));
self
}
#[cfg(feature = "cert-gen")]
#[must_use]
pub fn self_signed_tls(mut self) -> Self {
self.tls = I2pTls::SelfSigned;
self
}
#[cfg_attr(not(feature = "tls"), allow(clippy::missing_const_for_fn))]
#[must_use]
pub fn no_tls(mut self) -> Self {
self.tls = I2pTls::None;
self
}
#[must_use]
pub fn on_ready(mut self, f: impl FnOnce(&str) + Send + 'static) -> Self {
self.on_ready = Some(Box::new(f));
self
}
#[must_use]
pub fn nickname(&self) -> &str {
&self.nickname
}
#[must_use]
pub const fn tls_enabled(&self) -> bool {
!matches!(self.tls, I2pTls::None)
}
fn keys_path(&self) -> PathBuf {
self.data_dir
.clone()
.unwrap_or_else(|| PathBuf::from(".tachyon-i2p"))
.join(format!("{}.keys", self.nickname))
}
}
impl<S> Server<S>
where
S: Clone + Send + Sync + 'static,
{
pub async fn serve_i2p(
self,
nickname: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.serve_i2p_config(I2pConfig::new(nickname)).await
}
pub async fn serve_i2p_config(
self,
config: I2pConfig,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let router = I2pRouter::start(config.nickname.clone()).await?;
self.serve_i2p_config_with_router(&router, config).await
}
pub async fn serve_i2p_config_with_router(
self,
router: &I2pRouter,
config: I2pConfig,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
crate::server::enforce_fips_compliance()?;
validate_nickname(&config.nickname)?;
let keys_path = config.keys_path();
let is_public = true;
let mut destination = router
.destination_from_keys_file(
keys_path,
is_public,
config.sig_type,
&config.encryption_types,
)
.await?;
let address = destination.b32_address().to_string();
tracing::info!("[i2p] eepsite published at {address}");
if let Some(on_ready) = config.on_ready {
on_ready(&address);
}
#[cfg(feature = "tls")]
{
let tls_acceptor = match &config.tls {
I2pTls::None => None,
#[cfg(feature = "cert-gen")]
I2pTls::SelfSigned => {
let cert = crate::tls::generate_self_signed_cert(vec![address.clone()])?;
let server_config = self.effective_tls_policy().server_config_from_pem(
cert.cert_pem.as_bytes(),
cert.key_pem.as_bytes(),
)?;
Some(TlsAcceptor::from(Arc::new(server_config)))
}
I2pTls::Custom(server_config) => Some(TlsAcceptor::from(server_config.clone())),
};
let state = Arc::new(self);
loop {
let stream = match destination.accept().await {
Ok(s) => s,
Err(e) => {
tracing::debug!("[i2p] accept error: {e}");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
continue;
}
};
let state = state.clone();
let tls_acceptor = tls_acceptor.clone();
drop(tokio::spawn(async move {
if let Err(e) = handle_i2p_stream(state, stream, tls_acceptor).await {
tracing::debug!("[i2p] connection error: {e}");
}
}));
}
}
#[cfg(not(feature = "tls"))]
{
let state = Arc::new(self);
loop {
let stream = match destination.accept().await {
Ok(s) => s,
Err(e) => {
tracing::debug!("[i2p] accept error: {e}");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
continue;
}
};
let state = state.clone();
drop(tokio::spawn(async move {
if let Err(e) = handle_i2p_stream_plaintext(state, stream).await {
tracing::debug!("[i2p] connection error: {e}");
}
}));
}
}
}
}
fn validate_nickname(nickname: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if nickname.is_empty() || nickname.contains(['/', '\\']) || nickname == "." || nickname == ".."
{
return Err(format!("invalid I2P eepsite nickname {nickname:?}").into());
}
Ok(())
}
#[cfg(not(feature = "tls"))]
async fn handle_i2p_stream_plaintext<S>(
state: Arc<Server<S>>,
stream: tachyon_i2p::I2pStream,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
S: Clone + Send + Sync + 'static,
{
let svc =
hyper::service::service_fn(move |req| hyper_handler(state.clone(), req, I2P_PEER_ADDR));
serve_connection(stream, svc).await
}
#[cfg(feature = "tls")]
async fn handle_i2p_stream<S>(
state: Arc<Server<S>>,
stream: tachyon_i2p::I2pStream,
tls_acceptor: Option<TlsAcceptor>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
S: Clone + Send + Sync + 'static,
{
match tls_acceptor {
None => {
let svc = hyper::service::service_fn(move |req| {
hyper_handler(state.clone(), req, I2P_PEER_ADDR)
});
serve_connection(stream, svc).await
}
Some(acceptor) => {
let tls_stream = tokio::time::timeout(
crate::server::TLS_HANDSHAKE_TIMEOUT,
acceptor.accept(stream),
)
.await
.map_err(|_| "TLS handshake timed out")??;
let svc = hyper::service::service_fn(move |req| {
hyper_handler(state.clone(), req, I2P_PEER_ADDR)
});
serve_connection(tls_stream, svc).await
}
}
}
#[cfg(test)]
mod tests {
use super::{I2pConfig, validate_nickname};
#[test]
fn validate_nickname_accepts_a_normal_name() {
assert!(validate_nickname("my-eepsite").is_ok());
}
#[test]
fn validate_nickname_rejects_path_traversal() {
assert!(validate_nickname("..").is_err());
assert!(validate_nickname(".").is_err());
assert!(validate_nickname("").is_err());
assert!(validate_nickname("../../etc/passwd").is_err());
assert!(validate_nickname("a/b").is_err());
assert!(validate_nickname("a\\b").is_err());
}
#[test]
fn i2p_config_defaults_are_sensible() {
let config = I2pConfig::new("test-nickname");
assert_eq!(config.nickname(), "test-nickname");
assert!(!config.tls_enabled());
assert_eq!(
config.keys_path(),
std::path::Path::new(".tachyon-i2p/test-nickname.keys")
);
}
#[cfg(feature = "cert-gen")]
#[test]
fn i2p_config_builder_methods_are_chainable() {
let config = I2pConfig::new("nick")
.data_dir("/tmp/i2p-data")
.self_signed_tls();
assert_eq!(
config.keys_path(),
std::path::Path::new("/tmp/i2p-data/nick.keys")
);
assert!(config.tls_enabled());
let config = config.no_tls();
assert!(!config.tls_enabled());
}
#[cfg(not(feature = "cert-gen"))]
#[test]
fn i2p_config_data_dir_is_chainable_without_cert_gen() {
let config = I2pConfig::new("nick").data_dir("/tmp/i2p-data");
assert_eq!(
config.keys_path(),
std::path::Path::new("/tmp/i2p-data/nick.keys")
);
assert!(!config.tls_enabled());
}
#[test]
fn i2p_config_signature_and_crypto_type_defaults_and_overrides() {
let config = I2pConfig::new("nick");
assert_eq!(config.sig_type, tachyon_i2p::SigType::default());
assert!(
config.encryption_types.is_empty(),
"no explicit crypto_type() call should mean \"use libi2pd's automatic hybrid set\""
);
let config = config
.signature_type(tachyon_i2p::SigType::EcdsaP521)
.crypto_type(tachyon_i2p::CryptoType::EciesMlkem768X25519);
assert_eq!(config.sig_type, tachyon_i2p::SigType::EcdsaP521);
assert_eq!(
config.encryption_types,
vec![tachyon_i2p::CryptoType::EciesMlkem768X25519]
);
}
#[test]
fn i2p_config_encryption_types_preserves_preference_order() {
let config = I2pConfig::new("nick").encryption_types(&[
tachyon_i2p::CryptoType::EciesMlkem1024X25519,
tachyon_i2p::CryptoType::EciesX25519,
]);
assert_eq!(
config.encryption_types,
vec![
tachyon_i2p::CryptoType::EciesMlkem1024X25519,
tachyon_i2p::CryptoType::EciesX25519,
],
"the preferred type must stay first -- it's what libi2pd publishes as preferred"
);
let config = config.crypto_type(tachyon_i2p::CryptoType::EciesX25519);
assert_eq!(
config.encryption_types,
vec![tachyon_i2p::CryptoType::EciesX25519]
);
}
#[test]
fn i2p_config_debug_does_not_panic() {
let debug = format!("{:?}", I2pConfig::new("nick"));
assert!(debug.contains("I2pConfig"));
assert!(debug.contains("nick"));
}
#[cfg(all(feature = "tls", feature = "cert-gen"))]
#[test]
fn tls_config_switches_to_a_custom_server_config() {
let policy = crate::tls::TlsPolicy::hardened();
let cert = crate::tls::generate_self_signed_cert(vec!["nick.b32.i2p".to_string()])
.expect("generate self-signed cert");
let server_config = policy
.server_config_from_pem(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes())
.expect("build server config");
let config = I2pConfig::new("nick").tls_config(server_config);
assert!(matches!(config.tls, super::I2pTls::Custom(_)));
assert!(config.tls_enabled());
}
#[test]
fn on_ready_stores_the_callback() {
let config = I2pConfig::new("nick").on_ready(|_addr| {});
assert!(config.on_ready.is_some());
}
}