Skip to content
Open
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
1 change: 1 addition & 0 deletions dist/components/proxy-middleware/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ declare class ProxyMiddlewareManager {
loggerService: any;
sslConfigFetcher: any;
setAllowInsecureCerts: (value: any) => void;
setDevScriptMode: (value: any) => void;
init_config: (config?: {}) => void;
init: (config?: {}) => void;
request_handler_idx: number;
Expand Down
56 changes: 45 additions & 11 deletions dist/components/proxy-middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ class ProxyMiddlewareManager {
this.proxyConfig.allowInsecureCerts = !!value;
}
};
// RQ-2426: switch the script-execution mode on the running proxy. The per-request
// handler reads `this.proxyConfig.devScriptMode`, so mutating it here takes effect
// on the next request — no proxy restart. Only an explicit `true` enables dev mode.
this.setDevScriptMode = (value) => {
if (this.proxyConfig) {
this.proxyConfig.devScriptMode = value === true;
}
if (value === true) {
// eslint-disable-next-line no-console
console.warn("⚠️ Requestly: DEV script-execution mode ENABLED — rule code now runs with FULL system access (no sandbox). Enable only for code you trust.");
}
};
/* NOT USEFUL */
this.init_config = (config = {}) => {
Object.keys(exports.MIDDLEWARE_TYPE).map((middleware_key) => {
Expand Down Expand Up @@ -83,13 +95,25 @@ class ProxyMiddlewareManager {
const is_detachable = true;
const logger_middleware = new logger_middleware_1.default(this.config[exports.MIDDLEWARE_TYPE.LOGGER], this.loggerService);
const idx = this.init_request_handler(async (ctx, callback) => {
var _a;
var _a, _b, _c;
ctx.rq = new ctx_rq_namespace_1.default();
ctx.rq.set_original_request((0, proxy_ctx_helper_1.get_request_options)(ctx));
// RQ-2425: verify upstream TLS certificates by default. Only skip
// verification when the user has explicitly opted in (e.g. to reach a
// self-signed / internal upstream). Defaults to secure when unset.
ctx.proxyToServerRequestOptions.rejectUnauthorized = !((_a = this.proxyConfig) === null || _a === void 0 ? void 0 : _a.allowInsecureCerts);
// RQ-2426: stamp the script-execution mode for THIS request so the code-rule
// processors know whether to use the safe QuickJS sandbox (default) or the
// legacy full-access path. Read from proxyConfig each request → live, no
// restart. Env fallback RQ_SCRIPT_EXECUTION_MODE=dev applies ONLY when the
// config hasn't set it and NEVER overrides an explicit false. Only an explicit
// `true` enables dev mode; everything else fails safe.
const cfgDevScriptMode = (_b = this.proxyConfig) === null || _b === void 0 ? void 0 : _b.devScriptMode;
ctx.rq.devScriptMode =
cfgDevScriptMode === true ||
(cfgDevScriptMode === undefined &&
typeof process !== "undefined" &&
((_c = process === null || process === void 0 ? void 0 : process.env) === null || _c === void 0 ? void 0 : _c.RQ_SCRIPT_EXECUTION_MODE) === "dev");
// Figure out a way to enable/disable middleware dynamically
// instead of re-initing this again
const rules_middleware = new rules_middleware_1.default(this.config[exports.MIDDLEWARE_TYPE.RULES], ctx, this.rulesHelper);
Expand All @@ -103,7 +127,9 @@ class ProxyMiddlewareManager {
if (modifyResponseActionExist) {
const statusCode = ctx.rq_response_status_code || 404;
const responseHeaders = (0, proxy_ctx_helper_1.getResponseHeaders)(ctx) || {};
ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders);
if (!ctx.proxyToClientResponse.headersSent) {
ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders);
}
ctx.proxyToClientResponse.end(ctx.rq_response_body);
ctx.rq.set_final_response({
status_code: statusCode,
Expand All @@ -121,10 +147,12 @@ class ProxyMiddlewareManager {
// Serve a clear SSL error instead of a misleading ERR_NAME_NOT_RESOLVED.
if ((0, handleUnreachableAddress_1.isCertificateError)(err)) {
const { status, contentType, body, errorToken } = (0, handleUnreachableAddress_1.dataToServeCertErrorPage)(host, err === null || err === void 0 ? void 0 : err.code);
ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], {
"Content-Type": contentType,
"x-rq-error": errorToken,
});
if (!ctx.proxyToClientResponse.headersSent) {
ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], {
"Content-Type": contentType,
"x-rq-error": errorToken,
});
}
ctx.proxyToClientResponse.end(body);
ctx.rq.set_final_response({
status_code: status,
Expand All @@ -150,10 +178,12 @@ class ProxyMiddlewareManager {
const isAddressUnreachable = await (0, handleUnreachableAddress_1.isAddressUnreachableError)(hostname);
if (isAddressUnreachable) {
const { status, contentType, body } = (0, handleUnreachableAddress_1.dataToServeUnreachablePage)(host);
ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], {
"Content-Type": contentType,
"x-rq-error": "ERR_NAME_NOT_RESOLVED"
});
if (!ctx.proxyToClientResponse.headersSent) {
ctx.proxyToClientResponse.writeHead(status, http_1.default.STATUS_CODES[status], {
"Content-Type": contentType,
"x-rq-error": "ERR_NAME_NOT_RESOLVED"
});
}
ctx.proxyToClientResponse.end(body);
ctx.rq.set_final_response({
status_code: status,
Expand Down Expand Up @@ -246,7 +276,11 @@ class ProxyMiddlewareManager {
delete responseHeaders['transfer-encoding'];
delete responseHeaders['Transfer-Encoding'];
}
ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders);
// Node 24 commits headers on writeHead and throws on a second call;
// guard so a re-entrant write can't crash the response pipeline.
if (!ctx.proxyToClientResponse.headersSent) {
ctx.proxyToClientResponse.writeHead(statusCode, http_1.default.STATUS_CODES[statusCode], responseHeaders);
}
ctx.proxyToClientResponse.write(ctx.rq_response_body);
ctx.rq.set_final_response({
...(0, proxy_ctx_helper_1.get_response_options)(ctx),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,10 @@ const modify_request = (ctx, new_req) => {
ctx.rq_request_body = new_req;
};
const modify_request_using_code = async (action, ctx) => {
let userFunction = null;
try {
userFunction = (0, utils_2.getFunctionFromString)(action.request);
}
catch (error) {
// User has provided an invalid function
return modify_request(ctx, "Can't parse Requestly function. Please recheck. Error Code 7201. Actual Error: " +
error.message);
}
if (!userFunction || typeof userFunction !== "function") {
var _a, _b;
// RQ-2426: validate the function source parses (compile-only, no execution)
// before running it in the sandboxed worker.
if (!(await (0, utils_2.isValidFunctionString)(action.request))) {
// User has provided an invalid function
return modify_request(ctx, "Can't parse Requestly function. Please recheck. Error Code 944.");
}
Expand All @@ -55,20 +49,40 @@ const modify_request_using_code = async (action, ctx) => {
try {
args.bodyAsJson = JSON.parse(args.request);
}
catch (_a) {
catch (_c) {
/*Do nothing -- could not parse body as JSON */
}
finalRequest = await (0, utils_2.executeUserFunction)(ctx, userFunction, args);
// RQ-2426: run in DEV (legacy full-access) mode only when the proxy config has
// explicitly enabled it (stamped onto ctx.rq per request); otherwise the safe
// QuickJS sandbox. The mode is never taken from the rule itself.
finalRequest = await (0, utils_2.executeUserFunction)(ctx, action.request, args, {
devMode: ((_a = ctx === null || ctx === void 0 ? void 0 : ctx.rq) === null || _a === void 0 ? void 0 : _a.devScriptMode) === true,
});
if (finalRequest && typeof finalRequest === "string") {
return modify_request(ctx, finalRequest);
}
else
throw new Error("Returned value is not a string");
}
catch (error) {
// Function parsed but failed to execute
return modify_request(ctx, "Can't execute Requestly function. Please recheck. Error Code 187. Actual Error: " +
error.message);
// Function parsed but failed to execute. Code 188 = sandbox-internal (our shim
// broke); 187 = the rule author's code. error.message now carries the real
// sandbox error (previously swallowed).
const code = error && error.kind === "prelude" ? 188 : 187;
let message = "Can't execute Requestly function. Please recheck. Error Code " +
code +
". Actual Error: " +
error.message;
// SAFE mode only: if the rule tripped on a host API the sandbox blocks, point
// the user to the doc on enabling Developer Script Mode (full access).
if (code === 187 &&
!(((_b = ctx === null || ctx === void 0 ? void 0 : ctx.rq) === null || _b === void 0 ? void 0 : _b.devScriptMode) === true) &&
(0, utils_2.isSandboxCapabilityError)(error && error.message)) {
message +=
". This rule uses Node.js APIs that the secure sandbox blocks. To run it with full system access, enable Developer Script Mode: " +
utils_2.DEV_MODE_DOC_URL;
}
return modify_request(ctx, message);
}
};
exports.default = process_modify_request_action;
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,10 @@ const modify_response_using_local = (action, ctx) => {
}
};
const modify_response_using_code = async (action, ctx) => {
var _a, _b, _c, _d;
let userFunction = null;
try {
userFunction = (0, utils_2.getFunctionFromString)(action.response);
}
catch (error) {
// User has provided an invalid function
return modify_response(ctx, "Can't parse Requestly function. Please recheck. Error Code 7201. Actual Error: " +
error.message);
}
if (!userFunction || typeof userFunction !== "function") {
var _a, _b, _c, _d, _e, _f;
// RQ-2426: validate the function source parses (compile-only, no execution)
// before running it in the sandboxed worker.
if (!(await (0, utils_2.isValidFunctionString)(action.response))) {
// User has provided an invalid function
return modify_response(ctx, "Can't parse Requestly function. Please recheck. Error Code 944.");
}
Expand All @@ -135,20 +128,40 @@ const modify_response_using_code = async (action, ctx) => {
try {
args.responseJSON = JSON.parse(args.response);
}
catch (_e) {
catch (_g) {
/*Do nothing -- could not parse body as JSON */
}
finalResponse = await (0, utils_2.executeUserFunction)(ctx, action.response, args);
// RQ-2426: run in DEV (legacy full-access) mode only when the proxy config has
// explicitly enabled it (stamped onto ctx.rq per request); otherwise the safe
// QuickJS sandbox. The mode is never taken from the rule itself.
finalResponse = await (0, utils_2.executeUserFunction)(ctx, action.response, args, {
devMode: ((_e = ctx === null || ctx === void 0 ? void 0 : ctx.rq) === null || _e === void 0 ? void 0 : _e.devScriptMode) === true,
});
if (finalResponse && typeof finalResponse === "string") {
return modify_response(ctx, finalResponse, action.statusCode);
}
else
throw new Error("Returned value is not a string");
}
catch (error) {
// Function parsed but failed to execute
return modify_response(ctx, "Can't execute Requestly function. Please recheck. Error Code 187. Actual Error: " +
error.message);
// Function parsed but failed to execute. Code 188 = sandbox-internal (our shim
// broke); 187 = the rule author's code. error.message now carries the real
// sandbox error (previously swallowed).
const code = error && error.kind === "prelude" ? 188 : 187;
let message = "Can't execute Requestly function. Please recheck. Error Code " +
code +
". Actual Error: " +
error.message;
// SAFE mode only: if the rule tripped on a host API the sandbox blocks, point
// the user to the doc on enabling Developer Script Mode (full access).
if (code === 187 &&
!(((_f = ctx === null || ctx === void 0 ? void 0 : ctx.rq) === null || _f === void 0 ? void 0 : _f.devScriptMode) === true) &&
(0, utils_2.isSandboxCapabilityError)(error && error.message)) {
message +=
". This rule uses Node.js APIs that the secure sandbox blocks. To run it with full system access, enable Developer Script Mode: " +
utils_2.DEV_MODE_DOC_URL;
}
return modify_response(ctx, message, action.statusCode);
}
};
exports.default = process_modify_response_action;
2 changes: 2 additions & 0 deletions dist/rq-proxy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ declare class RQProxy {
loggerService: ILoggerService;
globalState: State;
private allowInsecureCerts?;
private devScriptMode?;
constructor(proxyConfig: ProxyConfig, rulesDataSource: IRulesDataSource, loggerService: ILoggerService, initialGlobalState?: IInitialState);
initProxy: (proxyConfig: ProxyConfig) => void;
setAllowInsecureCerts: (value: boolean) => void;
setDevScriptMode: (value: boolean) => void;
doSomething: () => void;
}
export default RQProxy;
13 changes: 12 additions & 1 deletion dist/rq-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class RQProxy {
sslCaDir: proxyConfig.certPath,
host: "0.0.0.0",
}, (err) => {
var _a, _b;
var _a, _b, _c, _d;
console.log("Proxy Listen");
if (err) {
console.log(err);
Expand All @@ -37,6 +37,9 @@ class RQProxy {
if (this.allowInsecureCerts !== undefined) {
(_b = (_a = this.proxyMiddlewareManager).setAllowInsecureCerts) === null || _b === void 0 ? void 0 : _b.call(_a, this.allowInsecureCerts);
}
if (this.devScriptMode !== undefined) {
(_d = (_c = this.proxyMiddlewareManager).setDevScriptMode) === null || _d === void 0 ? void 0 : _d.call(_c, this.devScriptMode);
}
}
});
// For Testing //
Expand All @@ -60,6 +63,14 @@ class RQProxy {
this.allowInsecureCerts = !!value;
(_b = (_a = this.proxyMiddlewareManager) === null || _a === void 0 ? void 0 : _a.setAllowInsecureCerts) === null || _b === void 0 ? void 0 : _b.call(_a, this.allowInsecureCerts);
};
// RQ-2426: live-switch the script-execution mode without restarting the proxy.
// Remembers the value so it survives being set before the middleware manager is
// ready (replayed in initProxy). Only an explicit `true` enables dev mode.
this.setDevScriptMode = (value) => {
var _a, _b;
this.devScriptMode = value === true;
(_b = (_a = this.proxyMiddlewareManager) === null || _a === void 0 ? void 0 : _a.setDevScriptMode) === null || _b === void 0 ? void 0 : _b.call(_a, this.devScriptMode);
};
this.doSomething = () => {
console.log("do something");
};
Expand Down
1 change: 1 addition & 0 deletions dist/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface ProxyConfig {
rootCertPath: String;
onCARegenerated?: Function;
allowInsecureCerts?: boolean;
devScriptMode?: boolean;
}
export interface Rule {
id: string;
Expand Down
47 changes: 45 additions & 2 deletions dist/utils/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,45 @@
export declare const getFunctionFromString: (functionStringEscaped: any) => any;
export declare function executeUserFunction(ctx: any, functionString: string, args: any): Promise<any>;
/**
* Where a sandbox failure originated, so callers + telemetry can tell OUR
* shim/infra bugs (`prelude`) from the rule author's (`user`) and timeouts apart.
*/
export type SandboxErrorKind = "prelude" | "user" | "timeout";
export declare class SandboxError extends Error {
kind: SandboxErrorKind;
constructor(message: string, kind: SandboxErrorKind);
}
/**
* Verify a rule's code string parses WITHOUT executing it. Constructing
* `new Function(body)` compiles/parses the body but never runs it (the function
* is never called), so even an IIFE-shaped string cannot execute here. Avoids the
* `vm` module (unsupported in Electron's renderer); the sandboxed execution
* happens inside QuickJS.
*/
export declare const isValidFunctionString: (functionStringEscaped: string) => Promise<boolean>;
export interface ExecuteUserFunctionOptions {
/**
* When strictly `true`, run in DEV mode (legacy full-access `new Function`).
* Any other value — false/undefined/null/non-boolean — runs the SAFE QuickJS
* sandbox. Fail-safe: dev mode is never entered by accident.
*/
devMode?: boolean;
}
/**
* Entry point for executing a rule's "code" function. Dispatches strictly by mode:
* - dev (options.devMode === true): legacy `new Function` — full host access.
* - safe (anything else, the default): QuickJS-WASM sandbox — no host access.
*
* The mode is supplied by the caller (derived from the proxy config / desktop
* preference and stamped per-request); it is NEVER derived from rule content, so a
* shared/imported rule cannot select its own execution mode.
*/
export declare function executeUserFunction(ctx: any, functionString: string, args: any, options?: ExecuteUserFunctionOptions): Promise<any>;
/** Public docs page explaining how to enable Developer Script Mode. */
export declare const DEV_MODE_DOC_URL = "https://interceptor-docs.requestly.com/interceptor/desktop-app/developer-script-mode";
/**
* True when a code-rule failure was caused by the SAFE sandbox blocking a host
* capability (require of a module, `process`, etc.) — i.e. the kind of error the
* user could resolve by enabling Developer Script Mode. Used to append a helpful
* doc link to the surfaced error. Deliberately narrow so ordinary logic errors in
* the rule don't get the dev-mode hint.
*/
export declare function isSandboxCapabilityError(message: string): boolean;
Loading
Loading