diff --git a/src/components/proxy-middleware/middlewares/rules_middleware.js b/src/components/proxy-middleware/middlewares/rules_middleware.js index de76904..4895989 100644 --- a/src/components/proxy-middleware/middlewares/rules_middleware.js +++ b/src/components/proxy-middleware/middlewares/rules_middleware.js @@ -5,6 +5,10 @@ import { } from "../helpers/proxy_ctx_helper"; import RuleProcessorHelper from "../helpers/rule_processor_helper"; import RuleActionProcessor from "../rule_action_processor"; +import { PROXY_HANDLER_TYPE } from "../../../lib/proxy"; +import { RULE_ACTION } from "../constants"; +import process_modify_header_action from "../rule_action_processor/processors/modify_header_processor"; +import * as Sentry from "@sentry/browser"; class RulesMiddleware { constructor(is_active, ctx, rulesHelper) { @@ -79,6 +83,28 @@ class RulesMiddleware { return rule_actions; }; + _applyResponseHeaderRulesForRedirect = (ctx, destUrl) => { + const prevHandler = ctx.currentHandler; + try { + this._init_response_data(ctx); // Reads fetched headers from ctx.serverToProxyResponse + ctx.currentHandler = PROXY_HANDLER_TYPE.ON_RESPONSE; + + const modifyHeaderActions = this._process_rules(true) + .filter((action) => action?.action === RULE_ACTION.MODIFY_HEADERS); + // Process actions and collect their result objects + const actionResults = modifyHeaderActions.map((action) => + process_modify_header_action(action, ctx) + ); + + // Register results so the UI logs both rules as applied + this._update_action_result_objs(actionResults); + } catch (e) { + Sentry.captureException(e); + } finally { + ctx.currentHandler = prevHandler; + } + }; + _update_action_result_objs = (action_result_objs = []) => { if (action_result_objs) { this.action_result_objs = @@ -96,6 +122,9 @@ class RulesMiddleware { this.on_request_actions = this._process_rules(); + ctx.rq.applyResponseHeaderRulesForRedirect = (destUrl) => + this._applyResponseHeaderRulesForRedirect(ctx, destUrl); + const { action_result_objs, continue_request } = await this.rule_action_processor.process_actions( this.on_request_actions, diff --git a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js index 290eb72..1d16f55 100644 --- a/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js +++ b/src/components/proxy-middleware/rule_action_processor/handle_mixed_response.js @@ -1,47 +1,36 @@ const axios = require("axios"); -const parser = require("ua-parser-js"); import fs from "fs"; import * as Sentry from "@sentry/browser"; const mime = require('mime-types'); const handleMixedResponse = async (ctx, destinationUrl) => { - // Handling mixed response from safari - let user_agent_str = null; - user_agent_str = ctx?.clientToProxyRequest?.headers["user-agent"]; - const user_agent = parser(user_agent_str)?.browser?.name; - const LOCAL_DOMAINS = ["localhost", "127.0.0.1"]; - if (ctx.isSSL && destinationUrl.includes("http:")) { - if ( - user_agent === "Safari" || - !LOCAL_DOMAINS.some((domain) => destinationUrl.includes(domain)) - ) { - try { - const resp = await axios.get(destinationUrl, { - headers: { - "Cache-Control": "no-cache", - }, - }); - - return { - status: true, - response_data: { - headers: { "Cache-Control": "no-cache" }, - status_code: 200, - body: resp.data, - }, - }; - } catch (e) { - Sentry.captureException(e); - return { - status: true, - response_data: { - headers: { "Cache-Control": "no-cache" }, - status_code: 502, - body: e.response ? e.response.data : null, - }, - }; - } + try { + const resp = await axios.get(destinationUrl, { + responseType: "arraybuffer", + decompress: false, // 2. CRITICAL: Prevent Axios from unwrapping gzip + validateStatus: () => true, + headers: { "Cache-Control": "no-cache" }, + }); + return { + status: true, + response_data: { + status_code: resp.status, + headers: resp.headers, // real upstream headers (content-encoding already stripped by axios) + body: resp.data, // Buffer + }, + }; + } catch (e) { + // Only transport failures reach here now (DNS, refused, timeout, redirect loop). + Sentry.captureException(e); + return { + status: true, + response_data: { + headers: e.response ? e.response.headers : { "Cache-Control": "no-cache" }, + status_code: e.response ? e.response.status : 502, + body: e.response ? e.response.data : null, + }, + }; } } diff --git a/src/components/proxy-middleware/rule_action_processor/index.js b/src/components/proxy-middleware/rule_action_processor/index.js index f1224e3..d1ec99a 100644 --- a/src/components/proxy-middleware/rule_action_processor/index.js +++ b/src/components/proxy-middleware/rule_action_processor/index.js @@ -41,10 +41,17 @@ class RuleActionProcessor { const status_code = action_result.post_process_data.status_code || 200; const headers = action_result.post_process_data.headers || {}; - let body = action_result.post_process_data.body || null; + let body = action_result.post_process_data.body; + if (body === undefined) body = null; // console.log("Log", ctx.rq.original_request); - if(typeof(body) !== 'string') { + if (body instanceof ArrayBuffer) { + body = Buffer.from(body); + } else if (body && body.buffer instanceof ArrayBuffer && !Buffer.isBuffer(body)) { + body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } + + if (body !== null && !Buffer.isBuffer(body) && typeof body !== "string") { body = JSON.stringify(body); } @@ -75,7 +82,7 @@ class RuleActionProcessor { switch (rule_action.action) { case RULE_ACTION.REDIRECT: - action_result = process_redirect_action(rule_action, ctx); + action_result = await process_redirect_action(rule_action, ctx); break; case RULE_ACTION.MODIFY_HEADERS: action_result = process_modify_header_action(rule_action, ctx); diff --git a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js index adf9d31..702fad7 100644 --- a/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js +++ b/src/components/proxy-middleware/rule_action_processor/processors/redirect_processor.js @@ -8,6 +8,7 @@ import handleMixedResponse from "../handle_mixed_response"; import { build_action_processor_response, build_post_process_data, + stripHopByHopHeaders, } from "../utils"; // adding util to get origin header for handling cors @@ -46,14 +47,37 @@ const process_redirect_action = async (action, ctx) => { ); if (isMixedResponse) { + // Feed the fetched response into the response context, then run the existing + // response Modify Headers processor against the destination URL. + ctx.serverToProxyResponse = { + statusCode: response_data.status_code, + headers: { ...(response_data.headers || {}) }, + }; + if (typeof ctx.rq.applyResponseHeaderRulesForRedirect === "function") { + ctx.rq.applyResponseHeaderRulesForRedirect(new_url); + } + const headers = stripHopByHopHeaders(ctx.serverToProxyResponse.headers); + + // Recalculate content-length for the flattened buffer + let body = response_data.body; + if (body !== null && body !== undefined) { + if (Buffer.isBuffer(body)) { + headers['content-length'] = body.length; + } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { + headers['content-length'] = body.byteLength; + } else if (typeof body === 'string') { + headers['content-length'] = Buffer.byteLength(body); + } else { + headers['content-length'] = Buffer.byteLength(JSON.stringify(body)); + } + } else { + headers["content-length"] = 0; + } + return build_action_processor_response( action, true, - build_post_process_data( - response_data.status_code, - response_data.headers, - response_data.body - ) + build_post_process_data(response_data.status_code, headers, body) ); } diff --git a/src/components/proxy-middleware/rule_action_processor/utils.js b/src/components/proxy-middleware/rule_action_processor/utils.js index d8de508..f8f3171 100644 --- a/src/components/proxy-middleware/rule_action_processor/utils.js +++ b/src/components/proxy-middleware/rule_action_processor/utils.js @@ -59,4 +59,15 @@ export const getHost = (ctx) => { export const get_file_contents = (file_path) => { return fs.readFileSync(file_path, "utf-8"); -} \ No newline at end of file +}; + +export const stripHopByHopHeaders = (headers) => { + const out = { ...(headers || {}) }; + for (const key of Object.keys(out)) { + const k = key.toLowerCase(); + if (k === "content-length" || k === "transfer-encoding") { + delete out[key]; + } + } + return out; +}; \ No newline at end of file