Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ updates:
patterns:
- "worker"
- "worker-macros"
wasm-bindgen:
patterns:
- "wasm-bindgen-futures"
- "wasm-bindgen"
# Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
Expand Down
28 changes: 2 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 0 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,14 @@ crate-type = ["cdylib"]

[dependencies]
worker = { version = "0.8.3", features = ["http"] }
worker-macros = { version = "0.8.1" }
futures = "0.3.32"
wasm-bindgen-futures = "0.4.71"
wasm-bindgen = "0.2.121"
js-sys = "0.3.91"
hickory-proto = { version = "0.26.1", default-features = false }
tokio = { version = "1.52.3", features = ["io-util", "sync"], default-features = false }
regex = "1.12.3"
getrandom = { version = "0.4", features = ["wasm_js"], default-features = false }
sha2 = "0.11.0"
pin-project-lite = "0.2.17"

prefix-trie = "0.8.3"
ipnet = "2.12.0"
serde = { version = "1.0.195", features = ["derive"], default-features = false }

[profile.release]
opt-level = "z"
debug = false
Expand All @@ -37,6 +29,5 @@ strip = true
debug-assertions = false
codegen-units = 1


[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Oz", "--enable-bulk-memory", "--all-features"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## ✨ 特性

🔒 Trojan 代理 - 基于 WebSocket 的 Trojan 协议代理,注意属于 Cloudflare IP 范围地址会 block,因
🔒 Trojan 代理 - 基于 WebSocket 的 Trojan 协议代理,注意目标地址在 Cloudflare 范围内会 block

🌐 通用网站镜像 - 支持绝大多数网址的镜像,访问失败时建议通过代理方式

Expand Down
154 changes: 86 additions & 68 deletions src/proxy/api.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,58 @@
use super::*;
use regex::Regex;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use tokio::sync::OnceCell;

static REGISTRY: &str = "registry-1.docker.io";

static HOP_HEADERS: OnceCell<HashSet<&'static str>> = OnceCell::const_new();

async fn get_hop_headers() -> &'static HashSet<&'static str> {
HOP_HEADERS
.get_or_init(|| async {
HashSet::from([
// RFC 2616
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
// proxy generated
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
"x-real-ip",
"via",
"x-forwarded-port",
"x-forwarded-server",
])
})
.await
}

fn rewrite_location(value: &str, uri: &Url, my_host: &str) -> String {
if value.starts_with('/') {
return format!("/{}{}", uri.host().unwrap(), value);
}

if value.starts_with("https://") {
if let Ok(url) = Url::parse(value) {
if url
.host_str()
.is_some_and(|h| h.contains("cloudflarestorage"))
{
return value.to_string();
}
}
return value.replace("https://", &format!("https://{}/", my_host));
}

value.to_string()
}

fn replace_host(content: &mut str, src: &str, dest: &str) -> Result<String> {
let re = Regex::new(r#"(?P<attr>src|href)(?P<eq>=)(?P<quote>['"]?)(?P<url>(//|https://))"#)
.map_err(|_e| worker::Error::BadEncoding)?;
Expand Down Expand Up @@ -52,114 +101,83 @@ pub async fn handler(
dst_host: &str,
query: Option<HashMap<String, String>>,
) -> Result<Response> {
let hops = HOP_HEADERS
.get_or_init(|| async { get_hop_headers().await })
.await;
let my_host = req.headers().get("host")?.ok_or("Host header not found")?;

let hops = get_hop_headers().await;
// build request
let req_headers = Headers::new();
for (key, value) in req.headers().entries() {
if hops.contains(&key) {
if hops.contains(key.as_str()) {
continue;
}
req_headers.set(&key, &value)?;
}
req_headers.set("host", dst_host)?;
req_headers.set("referer", "")?;

let mut req_init = RequestInit {
let body = req.bytes().await?;
let body = (!body.is_empty()).then(|| worker::wasm_bindgen::JsValue::from(body));

let req_init = RequestInit {
method: req.method(),
headers: req_headers,
body: None,
body,
cf: CfProperties::default(),
redirect: RequestRedirect::Manual,
cache: None, // CacheMode::Default,
};
// request body
if let Ok(body) = req.bytes().await {
if !body.is_empty() {
req_init.body = Some(wasm_bindgen::JsValue::from(body));
}
}
let new_req = Request::new_with_init(uri.as_ref(), &req_init)?;

// send request
let new_req = Request::new_with_init(uri.as_ref(), &req_init)?;
let mut response = Fetch::Request(new_req).send().await?;

// update response
let resp_header = Headers::new();
let status = response.status_code();

for (key, value) in response.headers().entries() {
if hops.contains(&key) {
continue;
}
let new_value = match (status, key.as_str()) {
(301..=308, "location") => {
if value.starts_with('/') {
format!("/{}{}", uri.host().unwrap(), value)
} else if value.starts_with("https://") {
if let Ok(url) = Url::parse(&value) {
if url
.host_str()
.is_some_and(|host| host.contains("cloudflarestorage"))
{
value
} else {
value.replace("https://", &format!("https://{}/", my_host))
}
} else {
value.replace("https://", &format!("https://{}/", my_host))
}
} else {
value
}
}
(301..=308, "location") => rewrite_location(&value, &uri, &my_host),
(401, "www-authenticate") => {
value.replace("https://", &format!("https://{}/", my_host))
}
_ => value,
};
resp_header.set(&key, &new_value)?;
}
let _ = resp_header.delete("content-security-policy");
let _ = resp_header.set("access-control-allow-origin", "*");
if let Some(s) = resp_header.get("content-type")? {
if s.contains("text/html") {
let _ = resp_header.delete("content-encoding");
let _ = resp_header.set(
"set-cookie",
format!("{}={}; Path=/; Max-Age=3600", COOKIE_HOST_KEY, dst_host).as_str(),
);
let mut body = response.text().await?;
let should_replace = query
.as_ref()
.and_then(|map| map.get("tul_rh"))
.map(|value| value != "n")
.unwrap_or(true);
let newbody = if should_replace {
replace_host(&mut body, dst_host, &my_host)?
} else {
body
};
let resp = Response::builder()
.with_headers(resp_header)
.with_status(status)
.body(ResponseBody::Body(newbody.into_bytes()));
return Ok(resp);
resp_header.delete("content-security-policy")?;
resp_header.set("access-control-allow-origin", "*")?;

if resp_header
.get("content-type")?
.is_some_and(|ct| ct.contains("text/html"))
{
resp_header.delete("content-encoding")?;
resp_header.set(
"set-cookie",
format!("{}={}; Path=/; Max-Age=3600", COOKIE_HOST_KEY, dst_host).as_str(),
)?;

let mut body = response.text().await?;
let should_replace = query.as_ref().and_then(|q| q.get("tul_rh")) != Some(&"n".to_string());

if should_replace {
body = replace_host(&mut body, dst_host, &my_host)?;
}

return Ok(Response::builder()
.with_headers(resp_header)
.with_status(status)
.body(ResponseBody::Body(body.into_bytes())));
}

let resp = match response.stream() {
Err(_) => Response::builder()
.with_status(status)
.with_headers(resp_header)
.empty(),
Ok(stream) => Response::builder()
.with_status(status)
.with_headers(resp_header)
.from_stream(stream)?,
Err(_) => Response::builder()
.with_status(status)
.with_headers(resp_header)
.empty(),
};

Ok(resp)
Expand Down
Loading
Loading