diff --git a/Cargo.lock b/Cargo.lock index ccc85af..0d4f49c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -483,6 +498,7 @@ dependencies = [ "base64 0.21.0", "boltapi", "boringtun", + "brotli", "byteorder", "bytes", "chrono", @@ -492,6 +508,7 @@ dependencies = [ "dashmap", "fast-socks5", "fastrand", + "flate2", "flume", "futures", "http", @@ -568,6 +585,27 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -1220,9 +1258,9 @@ checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", diff --git a/boltconn/Cargo.toml b/boltconn/Cargo.toml index da9c437..f43bed2 100644 --- a/boltconn/Cargo.toml +++ b/boltconn/Cargo.toml @@ -19,6 +19,7 @@ axum = { version = "0.6.18", features = ["ws"] } base64 = "0.21.0" boltapi = { path = "../boltapi" } boringtun = "0.6.0" +brotli = "3.4.0" byteorder = "1.4.3" bytes = "1.2.1" chrono = { version = "0.4.31", default-features = false, features = ["clock", "std"] } @@ -27,6 +28,7 @@ colored = "2.0.0" dashmap = "5.5.3" fast-socks5 = "0.9.1" fastrand = "2.0.0" +flate2 = "1.0.28" flume = "0.11.0" futures = "0.3.25" http = "0.2.8" diff --git a/boltconn/src/cli/mod.rs b/boltconn/src/cli/mod.rs index c06b65d..8da13f8 100644 --- a/boltconn/src/cli/mod.rs +++ b/boltconn/src/cli/mod.rs @@ -137,7 +137,7 @@ pub(crate) async fn controller_main(args: ProgramArgs) -> ! { match create(init) { Ok(_) => exit(0), Err(err) => { - eprintln!("{}", err); + eprintln!("Error occurred: {}", err); exit(-1) } } diff --git a/boltconn/src/intercept/intercept_modifier.rs b/boltconn/src/intercept/intercept_modifier.rs index 8cb4167..e58b0dc 100644 --- a/boltconn/src/intercept/intercept_modifier.rs +++ b/boltconn/src/intercept/intercept_modifier.rs @@ -282,6 +282,7 @@ impl Modifier for InterceptModifier { .ok_or_else(|| anyhow!("no id"))? .1; + // For large body, we skip the manipulation let mut whole_data = if self.result.capture_response || self.result.contains_script { Self::read_at_most(body, self.size_limit).await? } else { @@ -326,7 +327,6 @@ impl Modifier for InterceptModifier { NetworkAddr::Raw(addr) => addr.ip().to_string(), NetworkAddr::DomainName { domain_name, .. } => domain_name.clone(), }; - // For large body, we skip the manipulation self.contents.push( ( req, diff --git a/boltconn/src/intercept/script_engine.rs b/boltconn/src/intercept/script_engine.rs index e03f38b..4ac3ac9 100644 --- a/boltconn/src/intercept/script_engine.rs +++ b/boltconn/src/intercept/script_engine.rs @@ -5,6 +5,7 @@ use rquickjs::class::Trace; use rquickjs::{Class, Context, Ctx, FromJs, IntoJs, Object, Runtime, Value}; use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::io::Read; #[derive(Debug, Clone)] struct HttpData { @@ -99,29 +100,57 @@ impl ScriptEngine { status: Option, headers: &HeaderMap, field: &str, - ) -> Option<(Option, HeaderMap, Option)> { + ) -> Option<(Option, HeaderMap, Option)> { let runtime = Runtime::new().ok()?; let ctx = Context::full(&runtime).ok()?; + let header = { + let mut header = HashMap::new(); + for (k, v) in headers.iter() { + let key = k.to_string(); + match header.entry(key) { + Entry::Vacant(e) => { + e.insert(v.to_str().ok()?.to_string()); + } + Entry::Occupied(mut e) => e + .get_mut() + .push_str(format!(", {}", v.to_str().ok()?).as_str()), + }; + } + header + }; + // decompress + let body = if let Some(compress) = header.get("content-encoding") { + data.and_then(|d| { + let mut v = vec![]; + match compress.as_str() { + "gzip" => { + flate2::read::GzDecoder::new(d.as_ref()) + .read_to_end(&mut v) + .ok()?; + } + "deflate" => { + flate2::read::DeflateDecoder::new(d.as_ref()) + .read_to_end(&mut v) + .ok()?; + } + "br" => { + brotli::Decompressor::new(d.as_ref(), d.len()) + .read_to_end(&mut v) + .ok()?; + } + _ => None?, + } + String::from_utf8(v).ok() + }) + } else { + data.and_then(|d| String::from_utf8(d.to_vec()).ok()) + }; let js_data = HttpData { url: url.to_string(), method, - header: { - let mut header = HashMap::new(); - for (k, v) in headers.iter() { - let key = k.to_string(); - match header.entry(key) { - Entry::Vacant(e) => { - e.insert(v.to_str().ok()?.to_string()); - } - Entry::Occupied(mut e) => e - .get_mut() - .push_str(format!(", {}", v.to_str().ok()?).as_str()), - }; - } - header - }, + header, status, - body: data.and_then(|d| String::from_utf8(d.to_vec()).ok()), + body, }; let r = match ctx.with(|ctx| -> Result { // init console @@ -159,7 +188,7 @@ impl ScriptEngine { } }) { Err(e) => { - tracing::trace!( + tracing::warn!( "Failed to run script {} for {}: {}", self.name .clone() @@ -176,7 +205,38 @@ impl ScriptEngine { for (k, v) in r.header { header.insert(HeaderName::from_bytes(k.as_bytes()).ok()?, v.parse().ok()?); } - Some((r.status, header, r.body)) + // recompress + let body = if let Some(compress) = header.get("content-encoding") { + r.body.and_then(|d| { + let mut buf = vec![]; + match compress.to_str().ok()? { + "gzip" => { + let r = + flate2::read::GzEncoder::new(d.as_bytes(), flate2::Compression::none()) + .read_to_end(&mut buf); + r.ok()?; + } + "deflate" => { + flate2::read::DeflateEncoder::new( + d.as_bytes(), + flate2::Compression::none(), + ) + .read_to_end(&mut buf) + .ok()?; + } + "br" => { + brotli::CompressorReader::new(d.as_bytes(), d.as_bytes().len(), 0, 22) + .read_to_end(&mut buf) + .ok()?; + } + _ => None?, + }; + Some(Bytes::from(buf)) + }) + } else { + r.body.map(Bytes::from) + }; + Some((r.status, header, body)) } pub fn try_rewrite_req(