fix(exit-node): preserve raw exit-node responses

Return raw exit-node responses from Apps Script when requested and strip stale exit-node content-encoding headers after server-side decompression.
This commit is contained in:
Captain Mirage
2026-05-16 16:52:45 +03:30
committed by GitHub
parent e36263862e
commit d56ddc692b
2 changed files with 26 additions and 11 deletions
+10 -1
View File
@@ -202,6 +202,15 @@ function _doSingle(req) {
try { try {
var opts = _buildOpts(req); var opts = _buildOpts(req);
var resp = UrlFetchApp.fetch(req.u, opts); var resp = UrlFetchApp.fetch(req.u, opts);
// Raw-return mode for exit-node path.
// r:true = return destination body verbatim so Rust gets {s,h,b} unwrapped.
if (req.r === true) {
return ContentService
.createTextOutput(resp.getContentText())
.setMimeType(ContentService.MimeType.JSON);
}
return _json({ return _json({
s: resp.getResponseCode(), s: resp.getResponseCode(),
h: _respHeaders(resp), h: _respHeaders(resp),
@@ -307,7 +316,7 @@ function _buildOpts(req) {
var opts = { var opts = {
method: (req.m || "GET").toLowerCase(), method: (req.m || "GET").toLowerCase(),
muteHttpExceptions: true, muteHttpExceptions: true,
followRedirects: req.r !== false, followRedirects: true, // ← always true; r flag now has different meaning
validateHttpsCertificates: true, validateHttpsCertificates: true,
escaping: false, escaping: false,
}; };
+15 -9
View File
@@ -2686,9 +2686,8 @@ impl DomainFronter {
.send_prebuilt_payload_through_relay(outer_payload) .send_prebuilt_payload_through_relay(outer_payload)
.await?; .await?;
// exit-node's JSON envelope: {s: u16, h: {...}, b: "<base64>"} on let result = parse_exit_node_response(&app_body);
// success, {e: "..."} on its own internal error. result
parse_exit_node_response(&app_body)
} }
/// Build the inner-layer payload that the exit node will execute. /// Build the inner-layer payload that the exit node will execute.
@@ -3031,7 +3030,7 @@ impl DomainFronter {
let start = text.find('{').ok_or_else(|| { let start = text.find('{').ok_or_else(|| {
FronterError::BadResponse(format!( FronterError::BadResponse(format!(
"no json in tunnel response: {}", "no json in tunnel response: {}",
&text[..text.len().min(200)] &text.chars().take(200).collect::<String>()
)) ))
})?; })?;
let end = text.rfind('}').ok_or_else(|| { let end = text.rfind('}').ok_or_else(|| {
@@ -3199,7 +3198,7 @@ impl DomainFronter {
let start = text.find('{').ok_or_else(|| { let start = text.find('{').ok_or_else(|| {
FronterError::BadResponse(format!( FronterError::BadResponse(format!(
"no json in batch response: {}", "no json in batch response: {}",
&text[..text.len().min(200)] &text.chars().take(200).collect::<String>()
)) ))
})?; })?;
let end = text.rfind('}').ok_or_else(|| { let end = text.rfind('}').ok_or_else(|| {
@@ -3961,11 +3960,17 @@ fn unix_to_ymd_utc(secs: u64) -> (i64, u32, u32) {
/// MITM TLS write-back path sees the same shape it gets from the regular /// MITM TLS write-back path sees the same shape it gets from the regular
/// Apps Script relay (status line + headers + body). /// Apps Script relay (status line + headers + body).
fn parse_exit_node_response(body: &[u8]) -> Result<Vec<u8>, FronterError> { fn parse_exit_node_response(body: &[u8]) -> Result<Vec<u8>, FronterError> {
let v: Value = serde_json::from_slice(body).map_err(|e| { let json_start = body
.windows(4)
.position(|w| w == b"\r\n\r\n")
.map(|i| i + 4)
.unwrap_or(0);
let json_bytes = &body[json_start..];
let v: Value = serde_json::from_slice(json_bytes).map_err(|e| {
FronterError::Relay(format!( FronterError::Relay(format!(
"exit-node response not valid JSON ({}): {}", "exit-node response not valid JSON ({}): {}",
e, e,
String::from_utf8_lossy(&body[..body.len().min(200)]) String::from_utf8_lossy(&json_bytes[..json_bytes.len().min(200)])
)) ))
})?; })?;
@@ -4001,6 +4006,7 @@ fn parse_exit_node_response(body: &[u8]) -> Result<Vec<u8>, FronterError> {
"transfer-encoding", "transfer-encoding",
"connection", "connection",
"keep-alive", "keep-alive",
"content-encoding", // exit node's fetch() auto-decompresses; header is stale
]; ];
let mut out = Vec::with_capacity(body_bytes.len() + 256); let mut out = Vec::with_capacity(body_bytes.len() + 256);
@@ -4565,13 +4571,13 @@ fn parse_relay_json(body: &[u8]) -> Result<Vec<u8>, FronterError> {
let start = text.find('{').ok_or_else(|| { let start = text.find('{').ok_or_else(|| {
FronterError::BadResponse(format!( FronterError::BadResponse(format!(
"no json in: {}", "no json in: {}",
&text[..text.len().min(200)] &text.chars().take(200).collect::<String>()
)) ))
})?; })?;
let end = text.rfind('}').ok_or_else(|| { let end = text.rfind('}').ok_or_else(|| {
FronterError::BadResponse(format!( FronterError::BadResponse(format!(
"no json end in: {}", "no json end in: {}",
&text[..text.len().min(200)] &text.chars().take(200).collect::<String>()
)) ))
})?; })?;
serde_json::from_str(&text[start..=end])? serde_json::from_str(&text[start..=end])?