FIX: AutoTLS via RustTLS
This commit is contained in:
@@ -4,14 +4,21 @@ use furumi_common::proto::{
|
||||
PathRequest, ReadRequest,
|
||||
};
|
||||
use moka::future::Cache;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::codegen::InterceptedService;
|
||||
use tonic::metadata::MetadataValue;
|
||||
use tonic::transport::{Channel, Endpoint};
|
||||
use tonic::transport::{Channel, Endpoint, Uri};
|
||||
use tonic::{Request, Status};
|
||||
use tracing::{debug, info};
|
||||
|
||||
// ── Auth interceptor ───────────────────────────────────────────
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthInterceptor {
|
||||
token: String,
|
||||
@@ -31,6 +38,91 @@ impl tonic::service::Interceptor for AuthInterceptor {
|
||||
|
||||
pub type GrpcClient = RemoteFileSystemClient<InterceptedService<Channel, AuthInterceptor>>;
|
||||
|
||||
// ── TLS: accept any certificate (encryption only) ──────────────
|
||||
|
||||
/// A rustls verifier that accepts any server certificate without validation.
|
||||
/// Provides encryption without certificate trust chain verification.
|
||||
#[derive(Debug)]
|
||||
struct NoVerifier;
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for NoVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom tower connector: TCP + TLS with NoVerifier, compatible with tonic's
|
||||
/// `connect_with_connector` API. Bypasses tonic's built-in TLS which doesn't
|
||||
/// support skip-verify in v0.12.
|
||||
#[derive(Clone)]
|
||||
struct InsecureTlsConnector {
|
||||
tls: tokio_rustls::TlsConnector,
|
||||
}
|
||||
|
||||
impl tower::Service<Uri> for InsecureTlsConnector {
|
||||
type Response = hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<TcpStream>>;
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
type Future = Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, uri: Uri) -> Self::Future {
|
||||
let tls = self.tls.clone();
|
||||
Box::pin(async move {
|
||||
let host = uri.host().unwrap_or("127.0.0.1").to_string();
|
||||
let port = uri.port_u16().unwrap_or(50051);
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
let tcp = TcpStream::connect(&addr).await?;
|
||||
|
||||
// Parse the server name — works with both hostnames and IP addresses
|
||||
let server_name = rustls::pki_types::ServerName::try_from(host.as_str())
|
||||
.unwrap_or_else(|_| {
|
||||
rustls::pki_types::ServerName::try_from("furumi-ng")
|
||||
.expect("static string is valid")
|
||||
});
|
||||
|
||||
let tls_stream = tls.connect(server_name.to_owned(), tcp).await?;
|
||||
Ok(hyper_util::rt::TokioIo::new(tls_stream))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Client ──────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FurumiClient {
|
||||
client: GrpcClient,
|
||||
@@ -40,47 +132,55 @@ pub struct FurumiClient {
|
||||
impl FurumiClient {
|
||||
/// Connects to the Furumi-ng server.
|
||||
///
|
||||
/// - `addr`: Server URL. Use `https://` for TLS, `http://` for plaintext.
|
||||
/// - `token`: Bearer token for auth (empty = no auth).
|
||||
/// - `tls_ca_pem`: Optional CA certificate PEM bytes for verifying the server.
|
||||
/// If using TLS without a CA cert, pass `None` (uses system roots).
|
||||
pub async fn connect(addr: &str, token: &str, tls_ca_pem: Option<Vec<u8>>) -> Result<Self> {
|
||||
/// - Use `https://` for encrypted connection (accepts any server certificate)
|
||||
/// - Use `http://` for plaintext (not recommended)
|
||||
///
|
||||
/// No certificate files needed — TLS provides encryption only.
|
||||
pub async fn connect(addr: &str, token: &str) -> Result<Self> {
|
||||
// Install ring as the default crypto provider for rustls
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let mut endpoint = Endpoint::from_shared(addr.to_string())
|
||||
|
||||
let is_https = addr.starts_with("https://");
|
||||
let tonic_addr = if is_https {
|
||||
// Trick tonic into thinking this is a plaintext connection so it doesn't
|
||||
// error out with HttpsUriWithoutTlsSupport. Our custom connector will
|
||||
// handle the actual TLS wrapping.
|
||||
addr.replacen("https://", "http://", 1)
|
||||
} else {
|
||||
addr.to_string()
|
||||
};
|
||||
|
||||
let endpoint = Endpoint::from_shared(tonic_addr)
|
||||
.map_err(|e| ClientError::Internal(format!("Invalid URI: {}", e)))?
|
||||
.timeout(Duration::from_secs(30))
|
||||
.concurrency_limit(256)
|
||||
.tcp_keepalive(Some(Duration::from_secs(60)))
|
||||
.http2_keep_alive_interval(Duration::from_secs(60));
|
||||
|
||||
// Configure TLS if using https://
|
||||
if addr.starts_with("https://") {
|
||||
let mut tls_config = tonic::transport::ClientTlsConfig::new()
|
||||
.domain_name("furumi-ng");
|
||||
|
||||
if let Some(ca_pem) = tls_ca_pem {
|
||||
info!("TLS enabled with server CA certificate");
|
||||
tls_config = tls_config
|
||||
.ca_certificate(tonic::transport::Certificate::from_pem(ca_pem));
|
||||
} else {
|
||||
info!("TLS enabled with system root certificates");
|
||||
}
|
||||
|
||||
endpoint = endpoint.tls_config(tls_config)
|
||||
.map_err(|e| ClientError::Internal(format!("TLS config error: {}", e)))?;
|
||||
}
|
||||
let channel = if is_https {
|
||||
info!("TLS enabled (encryption only, certificate verification disabled)");
|
||||
|
||||
let crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerifier))
|
||||
.with_no_client_auth();
|
||||
|
||||
let connector = InsecureTlsConnector {
|
||||
tls: tokio_rustls::TlsConnector::from(Arc::new(crypto)),
|
||||
};
|
||||
|
||||
endpoint.connect_with_connector(connector).await?
|
||||
} else {
|
||||
info!("Connecting without TLS (plaintext)");
|
||||
endpoint.connect().await?
|
||||
};
|
||||
|
||||
info!("Connecting to {}", addr);
|
||||
let channel = endpoint.connect().await?;
|
||||
|
||||
let interceptor = AuthInterceptor {
|
||||
token: token.to_string(),
|
||||
};
|
||||
|
||||
let client = RemoteFileSystemClient::with_interceptor(channel, interceptor);
|
||||
|
||||
|
||||
let attr_cache = Cache::builder()
|
||||
.max_capacity(100_000)
|
||||
.time_to_live(Duration::from_secs(5))
|
||||
@@ -102,9 +202,7 @@ impl FurumiClient {
|
||||
});
|
||||
|
||||
let response = client.get_attr(req).await?.into_inner();
|
||||
|
||||
self.attr_cache.insert(path.to_string(), response.clone()).await;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -127,7 +225,7 @@ impl FurumiClient {
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Fetches file chunk stream from the server. Returns the streaming receiver.
|
||||
/// Fetches file chunk stream from the server.
|
||||
pub async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
|
||||
Reference in New Issue
Block a user