From 14c82687c9c20227f3f4612c728fa535f0e23a34 Mon Sep 17 00:00:00 2001 From: yylt Date: Fri, 15 May 2026 11:28:42 +0800 Subject: [PATCH] update trie and some func --- .github/dependabot.yml | 4 -- Cargo.lock | 28 +------- Cargo.toml | 9 --- README.md | 2 +- src/proxy/api.rs | 154 +++++++++++++++++++++------------------ src/proxy/dns.rs | 159 ++++++++++++++++++++--------------------- src/proxy/mod.rs | 92 ++++++------------------ wrangler.toml | 1 - 8 files changed, 186 insertions(+), 263 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 068a878..5f964db 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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: "/" diff --git a/Cargo.lock b/Cargo.lock index cdcca14..e1d1e46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,12 +157,6 @@ dependencies = [ "syn", ] -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "equivalent" version = "1.0.2" @@ -595,9 +589,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -650,17 +644,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "prefix-trie" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" -dependencies = [ - "either", - "ipnet", - "num-traits", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -1008,18 +991,11 @@ dependencies = [ "futures", "getrandom", "hickory-proto", - "ipnet", - "js-sys", "pin-project-lite", - "prefix-trie", "regex", - "serde", "sha2", "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", "worker", - "worker-macros", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f1f5728..9814e98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,7 @@ 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" @@ -25,10 +21,6 @@ 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 @@ -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"] diff --git a/README.md b/README.md index 55518dc..000cd2f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## ✨ 特性 -🔒 Trojan 代理 - 基于 WebSocket 的 Trojan 协议代理,注意属于 Cloudflare IP 范围地址会 block,因 +🔒 Trojan 代理 - 基于 WebSocket 的 Trojan 协议代理,注意目标地址在 Cloudflare 范围内会 block 🌐 通用网站镜像 - 支持绝大多数网址的镜像,访问失败时建议通过代理方式 diff --git a/src/proxy/api.rs b/src/proxy/api.rs index ed5f784..ffc662c 100644 --- a/src/proxy/api.rs +++ b/src/proxy/api.rs @@ -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> = 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 { let re = Regex::new(r#"(?Psrc|href)(?P=)(?P['"]?)(?P(//|https://))"#) .map_err(|_e| worker::Error::BadEncoding)?; @@ -52,15 +101,12 @@ pub async fn handler( dst_host: &str, query: Option>, ) -> Result { - 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)?; @@ -68,23 +114,19 @@ pub async fn handler( 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 @@ -92,30 +134,8 @@ pub async fn handler( 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)) } @@ -123,43 +143,41 @@ pub async fn handler( }; 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) diff --git a/src/proxy/dns.rs b/src/proxy/dns.rs index 5292774..3615e5a 100644 --- a/src/proxy/dns.rs +++ b/src/proxy/dns.rs @@ -1,47 +1,48 @@ use super::*; use js_sys::Uint8Array; use std::net::Ipv4Addr; -use tokio::sync::OnceCell; -use wasm_bindgen::JsValue; +use worker::wasm_bindgen::JsValue; use hickory_proto::op::Message; use hickory_proto::rr::rdata::svcb::SvcParamValue; use hickory_proto::rr::{RData, RecordType}; -use ipnet::Ipv4Net; -use prefix_trie::set::PrefixSet; - const DNS_HEADER_SIZE: usize = 12; const QTYPE_A: u16 = 1; const QCLASS_IN: u16 = 1; -static CF_TRIE: OnceCell> = OnceCell::const_new(); +const CF_NETWORKS: [(u32, u32); 14] = [ + (ip_to_u32(103, 22, 200, 0), 22), + (ip_to_u32(103, 31, 4, 0), 22), + (ip_to_u32(104, 16, 0, 0), 13), + (ip_to_u32(104, 24, 0, 0), 14), + (ip_to_u32(108, 162, 192, 0), 18), + (ip_to_u32(131, 0, 72, 0), 22), + (ip_to_u32(141, 101, 64, 0), 18), + (ip_to_u32(162, 158, 0, 0), 15), + (ip_to_u32(172, 64, 0, 0), 13), + (ip_to_u32(173, 245, 48, 0), 20), + (ip_to_u32(188, 114, 96, 0), 20), + (ip_to_u32(190, 93, 240, 0), 20), + (ip_to_u32(197, 234, 240, 0), 22), + (ip_to_u32(198, 41, 128, 0), 17), +]; + +const fn ip_to_u32(a: u8, b: u8, c: u8, d: u8) -> u32 { + (a as u32) << 24 | (b as u32) << 16 | (c as u32) << 8 | d as u32 +} -// ref: https://www.cloudflare.com/ips -async fn get_cf_trie() -> PrefixSet { - // TODO fetch from cloudflare - let ipv4s = vec![ - "103.22.200.0/22", - "103.31.4.0/22", - "104.16.0.0/13", - "104.24.0.0/14", - "108.162.192.0/18", - "131.0.72.0/22", - "141.101.64.0/18", - "162.158.0.0/15", - "172.64.0.0/13", - "173.245.48.0/20", - "188.114.96.0/20", - "190.93.240.0/20", - "197.234.240.0/22", - "198.41.128.0/17", - ]; +fn is_cloudflare_ip(ip: Ipv4Addr) -> bool { + let ip_int = u32::from(ip); - let mut pm: PrefixSet = PrefixSet::new(); - for ip in ipv4s { - pm.insert(ip.parse().unwrap()); - } - pm + CF_NETWORKS.iter().any(|&(net_start, mask_bits)| { + let mask = if mask_bits == 0 { + 0 + } else { + (!0u32) << (32 - mask_bits) + }; + (ip_int & mask) == net_start + }) } // DNS 报文构建 @@ -163,24 +164,16 @@ pub async fn resolve_a(domain: &str, resolver: &str) -> Result { extract_ipv4_from_response(&resp_bytes) } -pub async fn is_cf_address, K: AsRef>( - resolve: K, - addr: &Address, +pub async fn is_cf_address( + resolve: impl AsRef, + addr: &Address>, ) -> Result<(bool, Ipv4Addr)> { - let trie = CF_TRIE.get_or_init(|| async { get_cf_trie().await }).await; - let v4fn = |ip: Ipv4Addr| -> Result<(bool, Ipv4Addr)> { - let ipnet = - Ipv4Net::new(ip, 32).map_err(|e| Error::RustError(format!("Invalid IPv4: {}", e)))?; - Ok((trie.get_lpm(&ipnet).is_some(), ip)) + let ip = match addr { + Address::Ipv4(ip) => *ip, + Address::Domain(domain) => resolve_a(domain.as_ref(), resolve.as_ref()).await?, }; - match addr { - Address::Ipv4(ip) => v4fn(*ip), - Address::Domain(domain) => { - let ip = resolve_a(domain.as_ref(), resolve.as_ref()).await?; - v4fn(ip) - } - } + Ok((is_cloudflare_ip(ip), ip)) } pub async fn resolve_handler>( @@ -237,15 +230,12 @@ pub async fn process_response( } }; - // 2. 查找 HTTPS 类型的记录 + // 2. 查找 HTTPS 类型记录,如果没有 HTTPS 记录,直接返回原始响应 let record = message .answers .iter_mut() .find(|r| r.record_type() == RecordType::HTTPS); - let mut ipv4_hint = None; - - // 3. 如果没有 HTTPS 记录,直接返回原始响应 let record = match record { None => { console_debug!("[process_response] Return original: No HTTPS record found in answers"); @@ -254,7 +244,7 @@ pub async fn process_response( Some(rc) => rc, }; - // 4. 从 HTTPS 记录中提取 IPv4 提示(Ipv4Hint)和 ECH 配置 + // 3. 从 HTTPS 记录中提取 ECH 配置和 IPv4 提示(Ipv4Hint),有 ECHO 或 IPv4 不属于 CF 直接返回 if let RData::HTTPS(ref hs) = record.data { for (_key, value) in hs.0.svc_params.iter() { match value { @@ -262,7 +252,18 @@ pub async fn process_response( console_debug!("[process_response] Return original: ECH already configured"); return Ok(response_bytes.to_vec()); } - SvcParamValue::Ipv4Hint(v4hint) => ipv4_hint = v4hint.0.first().copied(), + SvcParamValue::Ipv4Hint(hint) => { + if let Some(first_a) = hint.0.first() { + let ip = first_a.0; // 取出 Ipv4Addr + if !(is_cloudflare_ip(ip)) { + console_debug!( + "[process_response] Return original: IP {} is not in Cloudflare range", + ip + ); + return Ok(response_bytes.to_vec()); + } + } + } _ => {} } } @@ -271,37 +272,7 @@ pub async fn process_response( return Ok(response_bytes.to_vec()); } - // 5. 检查 IP 地址是否属于 Cloudflare - let addr = match ipv4_hint { - None => { - console_debug!("[process_response] Return original: No Ipv4Hint found in HTTPS record"); - return Ok(response_bytes.to_vec()); - } - Some(addr) => addr, - }; - - let trie = CF_TRIE.get_or_init(|| async { get_cf_trie().await }).await; - let ipnet = match Ipv4Net::new(addr.0, 32) { - Ok(net) => net, - Err(e) => { - console_debug!( - "[process_response] Return original: Invalid IPv4 {}: {}", - addr, - e - ); - return Ok(response_bytes.to_vec()); - } - }; - - if trie.get_lpm(&ipnet).is_none() { - console_debug!( - "[process_response] Return original: IP {} is not in Cloudflare range", - addr - ); - return Ok(response_bytes.to_vec()); - } - - // 6. 查询 ech_domain 的 HTTPS 记录 + // 4. 查询 ech_domain 的 HTTPS let ech_response = match doh_query(ech_domain, RecordType::HTTPS.into(), resolver).await { Ok(resp) => resp, Err(e) => { @@ -325,7 +296,7 @@ pub async fn process_response( } }; - // 7. 替换原响应中的 HTTPS 记录数据 + // 5. 替换原响应中的 HTTPS 记录数据 if let Some(https_record) = ech_message .answers .iter_mut() @@ -352,3 +323,25 @@ pub async fn process_response( Ok(response_bytes.to_vec()) } } + +#[test] +fn test_boundary_ips() { + let test_cases = vec![ + ("104.16.0.0", true), // 网络地址 + ("104.23.255.255", true), // 广播地址 + ("104.15.255.255", false), // /13之外 + ("104.24.0.0", true), // /14的网络地址 + ("104.27.255.255", true), // /14的广播地址 + ("104.28.0.0", false), // /14之外 + ]; + + for (ip_str, expected) in test_cases { + let ip = ip_str.parse::().unwrap(); + assert_eq!( + is_cloudflare_ip(ip), + expected, + "Boundary test failed for {}", + ip_str + ); + } +} diff --git a/src/proxy/mod.rs b/src/proxy/mod.rs index f4c77fe..bb9930a 100644 --- a/src/proxy/mod.rs +++ b/src/proxy/mod.rs @@ -4,16 +4,14 @@ pub mod tj; pub mod websocket; use sha2::{Digest, Sha224}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::net::Ipv4Addr; use tokio::sync::OnceCell; use worker::*; +// Cloudfalre ECH domain static ECH_DOMAIN: OnceCell = OnceCell::const_new(); -// one hop header, which should remove when forward. -static HOP_HEADERS: OnceCell> = OnceCell::const_new(); - // trojan password hash static TJ_PASSWORD: OnceCell> = OnceCell::const_new(); @@ -22,6 +20,7 @@ static TJ_PATH: OnceCell = OnceCell::const_new(); // suport DoH domain, like 1.1.1.1, doh.pub, dns.google static DOH_HOST: OnceCell = OnceCell::const_new(); + static COOKIE_HOST_KEY: &str = "tul_host"; #[derive(Debug, Clone)] @@ -30,41 +29,19 @@ pub enum Address> { Domain(T), } -async fn get_ech_domain(cx: &RouteContext<()>) -> String { - cx.env - .var("ECH_DOMAIN") - .map_or("linux.do".to_string(), |x| x.to_string()) -} - -async fn get_hop_headers() -> HashSet { - let mut headers = HashSet::new(); - - // RFC 2616 - headers.insert("connection".to_string()); - headers.insert("keep-alive".to_string()); - headers.insert("proxy-authenticate".to_string()); - headers.insert("proxy-authorization".to_string()); - headers.insert("te".to_string()); - headers.insert("trailer".to_string()); - headers.insert("transfer-encoding".to_string()); - headers.insert("upgrade".to_string()); - - // generated by proxy - headers.insert("x-forwarded-for".to_string()); - headers.insert("x-forwarded-host".to_string()); - headers.insert("x-forwarded-proto".to_string()); - headers.insert("x-real-ip".to_string()); - headers.insert("via".to_string()); - headers.insert("x-forwarded-port".to_string()); - headers.insert("x-forwarded-server".to_string()); - - // Cloudflare headers - //headers.insert("cf-connecting-ip".to_string()); // use this otherwise visit cf-cdn blocked. - headers.insert("cf-ray".to_string()); - headers.insert("cf-ipcountry".to_string()); - headers.insert("cf-request-id".to_string()); - - headers +async fn get_or_init_env<'a>( + cell: &'a OnceCell, + cx: &RouteContext<()>, + key: &str, + default: &str, +) -> &'a String { + cell.get_or_init(|| async { + cx.env + .var(key) + .map(|secret| secret.to_string()) // Secret → String + .unwrap_or_else(|_| default.to_string()) + }) + .await } async fn get_trojan_path(cx: &RouteContext<()>) -> String { @@ -91,12 +68,6 @@ async fn get_trojan_password(cx: &RouteContext<()>) -> Vec { .to_vec() } -async fn get_doh_host(cx: &RouteContext<()>) -> String { - cx.env - .var("DOH_HOST") - .map_or("dns.google".to_string(), |x| x.to_string()) -} - // parse path:[{scheme}://]{domain}:{port}{path} fn parse_path(url: &str) -> (&str, Option<&str>, Option<&str>, Option<&str>) { if !url.starts_with('/') || url.len() == 1 { @@ -178,12 +149,9 @@ pub async fn handler(req: Request, cx: RouteContext<()>) -> Result { let tj_path = TJ_PATH .get_or_init(|| async { get_trojan_path(&cx).await }) .await; - let dns_host = DOH_HOST - .get_or_init(|| async { get_doh_host(&cx).await }) - .await; - let ech_domain: &String = ECH_DOMAIN - .get_or_init(|| async { get_ech_domain(&cx).await }) - .await; + let dns_host = get_or_init_env(&DOH_HOST, &cx, "DOH_HOST", "dns.google").await; + let ech_domain = get_or_init_env(&ECH_DOMAIN, &cx, "ECH_DOMAIN", "linux.do").await; + let query = req .query() .map_or(None, |q: HashMap| Some(q)); @@ -199,7 +167,6 @@ pub async fn handler(req: Request, cx: RouteContext<()>) -> Result { .body(ResponseBody::Body(bytes)); Ok(new_resp) } - path if path.starts_with(tj_path.as_str()) => tj(req, cx).await, path if path.starts_with("/v2") => api::image_handler(req, query).await, "/tul_search" => { @@ -270,15 +237,12 @@ pub async fn tj(_req: Request, cx: RouteContext<()>) -> Result { let expected_hash = TJ_PASSWORD .get_or_init(|| async { get_trojan_password(&cx).await }) .await; - let dns_host = DOH_HOST - .get_or_init(|| async { get_doh_host(&cx).await }) - .await; - + let dns_host = get_or_init_env(&DOH_HOST, &cx, "DOH_HOST", "dns.google").await; let WebSocketPair { server, client } = WebSocketPair::new()?; let response = Response::from_websocket(client)?; // cloudflare not support early data! server.accept()?; - wasm_bindgen_futures::spawn_local(async move { + worker::wasm_bindgen_futures::spawn_local(async move { let events = server.events().expect("Failed to get event stream"); let mut wsstream = websocket::WsStream::new(&server, events, None); @@ -287,7 +251,6 @@ pub async fn tj(_req: Request, cx: RouteContext<()>) -> Result { let addr = match dns::is_cf_address(dns_host, &hostname).await { Ok((true, _)) => { console_debug!("DNS query success, behind cloudflare for {:?}", &hostname); - //server.close(Some(1000u16), Some("use DoH then connect directly")).ok(); None } Ok((false, ip)) => Some(ip), @@ -410,16 +373,3 @@ fn test_parse_path() { ); } } - -#[test] -fn test_build_search_url_requires_q() { - let err = build_search_url(&Some(HashMap::new())).unwrap_err(); - assert!(err.to_string().contains("missing query parameter: q")); -} - -#[test] -fn test_build_search_url_rejects_empty_q() { - let query = HashMap::from([(String::from("q"), String::from(" "))]); - let err = build_search_url(&Some(query)).unwrap_err(); - assert!(err.to_string().contains("missing query parameter: q")); -} diff --git a/wrangler.toml b/wrangler.toml index 698f50a..fc92948 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -4,4 +4,3 @@ compatibility_date = "2025-01-10" [build] command = "cargo install -q worker-build && worker-build --release" -