diff --git a/Cargo.lock b/Cargo.lock index 6580c4a..1873e41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,11 +530,12 @@ dependencies = [ "hyper", "hyper-util-wasm", "js-sys", - "lazy_static", "parking_lot_core", "pin-project-lite", "ring", + "rustls-pemfile", "rustls-pki-types", + "rustls-webpki", "send_wrapper 0.6.0", "thiserror", "tokio", @@ -1459,6 +1460,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.8.0" diff --git a/client/Cargo.toml b/client/Cargo.toml index a596207..d8003b9 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -21,8 +21,9 @@ http-body-util = "0.1.2" hyper = "1.4.1" hyper-util-wasm = { git = "https://github.com/r58Playz/hyper-util-wasm", branch = "opinionated", version = "0.1.7", features = ["client-legacy", "http1"] } js-sys = "0.3.70" -lazy_static = "1.5.0" pin-project-lite = "0.2.14" +rustls-pemfile = { version = "2.1.3", optional = true } +rustls-webpki = { version = "0.102.7", optional = true } send_wrapper = { version = "0.6.0", features = ["futures"] } thiserror = "1.0.63" tokio = "1.39.3" @@ -51,5 +52,5 @@ features = ["nightly"] [features] default = ["full"] -full = ["fastwebsockets", "async-compression", "hyper-util-wasm/http2"] +full = ["dep:fastwebsockets", "dep:async-compression", "dep:rustls-webpki", "dep:rustls-pemfile", "hyper-util-wasm/http2"] diff --git a/client/src/lib.rs b/client/src/lib.rs index 3d79084..edb05b8 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -73,6 +73,12 @@ pub enum EpoxyError { #[cfg(feature = "full")] #[error("Fastwebsockets: {0:?} ({0})")] FastWebSockets(#[from] fastwebsockets::WebSocketError), + #[cfg(feature = "full")] + #[error("Pemfile: {0:?} ({0})")] + Pemfile(std::io::Error), + #[cfg(feature = "full")] + #[error("Webpki: {0:?} ({0})")] + Webpki(#[from] webpki::Error), #[error("Custom wisp transport: {0}")] WispTransport(String), @@ -188,6 +194,10 @@ pub struct EpoxyClientOptions { pub redirect_limit: usize, #[wasm_bindgen(getter_with_clone)] pub user_agent: String, + pub disable_certificate_validation: bool, + #[cfg(feature = "full")] + #[wasm_bindgen(getter_with_clone)] + pub pem_files: Vec, } #[wasm_bindgen] @@ -206,6 +216,9 @@ impl Default for EpoxyClientOptions { websocket_protocols: Vec::new(), redirect_limit: 10, user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36".to_string(), + disable_certificate_validation: false, + #[cfg(feature = "full")] + pem_files: Vec::new(), } } } @@ -242,6 +255,8 @@ pub struct EpoxyClient { stream_provider: Arc, client: Client, + certs_tampered: bool, + pub redirect_limit: usize, #[wasm_bindgen(getter_with_clone)] pub user_agent: String, @@ -335,6 +350,7 @@ impl EpoxyClient { client, redirect_limit: options.redirect_limit, user_agent: options.user_agent, + certs_tampered: options.disable_certificate_validation || !options.pem_files.is_empty(), }) } @@ -566,10 +582,17 @@ impl EpoxyClient { } } - let (response, response_uri, redirected) = self + let (mut response, response_uri, redirected) = self .send_req(request_builder.body(HttpBody::new(body))?, request_redirect) .await?; + if self.certs_tampered { + response.headers_mut().insert( + HeaderName::from_static("X-Epoxy-CertsTampered"), + HeaderValue::from_static("true"), + ); + } + let response_headers: Array = response .headers() .iter() diff --git a/client/src/stream_provider.rs b/client/src/stream_provider.rs index bde717c..bfd2dde 100644 --- a/client/src/stream_provider.rs +++ b/client/src/stream_provider.rs @@ -1,7 +1,13 @@ -use std::{io::ErrorKind, pin::Pin, sync::Arc, task::Poll}; +use std::{ + io::{BufReader, ErrorKind}, + pin::Pin, + sync::Arc, + task::Poll, +}; +use cfg_if::cfg_if; use futures_rustls::{ - rustls::{ClientConfig, RootCertStore}, + rustls::{crypto::ring::default_provider, ClientConfig, RootCertStore}, TlsConnector, }; use futures_util::{ @@ -10,7 +16,6 @@ use futures_util::{ AsyncRead, AsyncWrite, Future, }; use hyper_util_wasm::client::legacy::connect::{ConnectSvc, Connected, Connection}; -use lazy_static::lazy_static; use pin_project_lite::pin_project; use wasm_bindgen_futures::spawn_local; use webpki_roots::TLS_SERVER_ROOTS; @@ -20,18 +25,11 @@ use wisp_mux::{ ClientMux, MuxStreamAsyncRW, MuxStreamIo, StreamType, }; -use crate::{console_log, utils::IgnoreCloseNotify, EpoxyClientOptions, EpoxyError}; - -lazy_static! { - static ref CLIENT_CONFIG: Arc = { - let certstore = RootCertStore::from_iter(TLS_SERVER_ROOTS.iter().cloned()); - Arc::new( - ClientConfig::builder() - .with_root_certificates(certstore) - .with_no_client_auth(), - ) - }; -} +use crate::{ + console_log, + utils::{IgnoreCloseNotify, NoCertificateVerification}, + EpoxyClientOptions, EpoxyError, +}; pub type ProviderUnencryptedStream = MuxStreamIo; pub type ProviderUnencryptedAsyncRW = MuxStreamAsyncRW; @@ -62,6 +60,8 @@ pub struct StreamProvider { udp_extension: bool, current_client: Arc>>, + + client_config: Arc, } impl StreamProvider { @@ -69,11 +69,44 @@ impl StreamProvider { wisp_generator: ProviderWispTransportGenerator, options: &EpoxyClientOptions, ) -> Result { + cfg_if! { + if #[cfg(feature = "full")] { + let pems: Result, webpki::Error>, std::io::Error> = options + .pem_files + .iter() + .flat_map(|x| { + rustls_pemfile::certs(&mut BufReader::new(x.as_bytes())) + .map(|x| x.map(|x| webpki::anchor_from_trusted_cert(&x).map(|x| x.to_owned()))) + .collect::>() + }) + .collect(); + let pems = pems.map_err(EpoxyError::Pemfile)??; + let certstore = RootCertStore::from_iter(pems.into_iter().chain(TLS_SERVER_ROOTS.iter().cloned())); + } else { + let certstore = RootCertStore::from_iter(TLS_SERVER_ROOTS.iter().cloned()); + } + } + + let client_config = if options.disable_certificate_validation { + ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoCertificateVerification( + default_provider(), + ))) + .with_no_client_auth() + } else { + ClientConfig::builder() + .with_root_certificates(certstore) + .with_no_client_auth() + }; + let client_config = Arc::new(client_config); + Ok(Self { wisp_generator, current_client: Arc::new(Mutex::new(None)), wisp_v2: options.wisp_v2, udp_extension: options.udp_extension_required, + client_config, }) } @@ -149,7 +182,7 @@ impl StreamProvider { let stream = self .get_asyncread(StreamType::Tcp, host.clone(), port) .await?; - let connector = TlsConnector::from(CLIENT_CONFIG.clone()); + let connector = TlsConnector::from(self.client_config.clone()); let ret = connector .connect(host.try_into()?, stream) .into_fallible() diff --git a/client/src/utils.rs b/client/src/utils.rs index 9693944..03ace16 100644 --- a/client/src/utils.rs +++ b/client/src/utils.rs @@ -6,12 +6,21 @@ use std::{ use async_trait::async_trait; use bytes::{buf::UninitSlice, BufMut, Bytes, BytesMut}; -use futures_rustls::TlsStream; +use futures_rustls::{ + rustls::{ + self, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, + DigitallySignedStruct, + }, + TlsStream, +}; use futures_util::{ready, AsyncRead, AsyncWrite, Future, Stream, StreamExt, TryStreamExt}; use http::{HeaderValue, Uri}; use hyper::{body::Body, rt::Executor}; use js_sys::{Array, ArrayBuffer, JsString, Object, Uint8Array}; use pin_project_lite::pin_project; +use rustls_pki_types::{CertificateDer, ServerName, UnixTime}; use send_wrapper::SendWrapper; use wasm_bindgen::{prelude::*, JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; @@ -285,9 +294,7 @@ impl AsyncWrite for IgnoreCloseNotify { cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - self.project() - .inner - .poll_write(cx, buf) + self.project().inner.poll_write(cx, buf) } fn poll_write_vectored( @@ -295,9 +302,7 @@ impl AsyncWrite for IgnoreCloseNotify { cx: &mut Context<'_>, bufs: &[std::io::IoSlice<'_>], ) -> Poll> { - self.project() - .inner - .poll_write_vectored(cx, bufs) + self.project().inner.poll_write_vectored(cx, bufs) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { @@ -309,6 +314,60 @@ impl AsyncWrite for IgnoreCloseNotify { } } +#[derive(Debug)] +pub(crate) struct NoCertificateVerification(pub CryptoProvider); + +impl NoCertificateVerification { + pub fn new(provider: CryptoProvider) -> Self { + Self(provider) + } +} + +impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} + pub fn is_redirect(code: u16) -> bool { [301, 302, 303, 307, 308].contains(&code) }