diff --git a/dist/components/proxy-middleware/helpers/handleUnreachableAddress.d.ts b/dist/components/proxy-middleware/helpers/handleUnreachableAddress.d.ts
index 0323518..a860f08 100644
--- a/dist/components/proxy-middleware/helpers/handleUnreachableAddress.d.ts
+++ b/dist/components/proxy-middleware/helpers/handleUnreachableAddress.d.ts
@@ -1,4 +1,12 @@
export function isAddressUnreachableError(host: any): Promise Requestly couldn't verify the TLS certificate of ${safeHost}${code ? ` ( If you trust this host, enable “Allow insecure SSL in proxy interceptor” in Requestly desktop settings and reload. ${token} The webpage at ${host}/ might be temporarily down or it may have moved permanently to a new web address. The webpage at ${escapeHtml(host)}/ might be temporarily down or it may have moved permanently to a new web address. ERR_NAME_NOT_RESOLVEDThis site's SSL certificate isn't trusted
+ ${safeCode})` : ''}.This site can’t be reached
-
Requestly couldn't verify the TLS certificate of ${safeHost}${code ? ` (${safeCode})` : ''}.
If you trust this host, enable “Allow insecure SSL in proxy interceptor” in Requestly desktop settings and reload.
+${token}
+diff --git a/dist/components/proxy-middleware/index.d.ts b/dist/components/proxy-middleware/index.d.ts index 45836bb..70eb7a6 100644 --- a/dist/components/proxy-middleware/index.d.ts +++ b/dist/components/proxy-middleware/index.d.ts @@ -14,6 +14,7 @@ declare class ProxyMiddlewareManager { rulesHelper: any; loggerService: any; sslConfigFetcher: any; + setAllowInsecureCerts: (value: any) => void; init_config: (config?: {}) => void; init: (config?: {}) => void; request_handler_idx: number; diff --git a/dist/components/proxy-middleware/index.js b/dist/components/proxy-middleware/index.js index 200892f..f178de8 100644 --- a/dist/components/proxy-middleware/index.js +++ b/dist/components/proxy-middleware/index.js @@ -29,6 +29,14 @@ exports.MIDDLEWARE_TYPE = { }; class ProxyMiddlewareManager { constructor(proxy, proxyConfig, rulesHelper, loggerService, sslConfigFetcher) { + // RQ-2425: update the upstream-TLS-verification policy on the running proxy. + // The per-request handler reads `this.proxyConfig.allowInsecureCerts`, so + // mutating it here takes effect on the next request — no proxy restart. + this.setAllowInsecureCerts = (value) => { + if (this.proxyConfig) { + this.proxyConfig.allowInsecureCerts = !!value; + } + }; /* NOT USEFUL */ this.init_config = (config = {}) => { Object.keys(exports.MIDDLEWARE_TYPE).map((middleware_key) => { @@ -75,13 +83,18 @@ 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; ctx.rq = new ctx_rq_namespace_1.default(); ctx.rq.set_original_request((0, proxy_ctx_helper_1.get_request_options)(ctx)); - ctx.proxyToServerRequestOptions.rejectUnauthorized = false; + // 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); // 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); ctx.onError(async function (ctx, err, kind, callback) { + var _a, _b; // Should only modify response body & headers ctx.rq_response_body = "" + kind + ": " + err, "utf8"; const { action_result_objs, continue_request } = await rules_middleware.on_response(ctx); @@ -102,9 +115,39 @@ class ProxyMiddlewareManager { } } else if (kind === "PROXY_TO_SERVER_REQUEST_ERROR") { + const host = (0, proxy_ctx_helper_1.get_request_url)(ctx); + // RQ-2425: upstream TLS cert verification failed (e.g. "Allow insecure + // SSL" is OFF and the origin has an untrusted/expired/self-signed cert). + // 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, + }); + ctx.proxyToClientResponse.end(body); + ctx.rq.set_final_response({ + status_code: status, + headers: { "Content-Type": contentType }, + body: body, + }); + logger_middleware.send_network_log(ctx, rules_middleware.action_result_objs, requestly_core_1.CONSTANTS.REQUEST_STATE.COMPLETE); + return; + } try { - const host = (0, proxy_ctx_helper_1.get_request_url)(ctx); - const isAddressUnreachable = await (0, handleUnreachableAddress_1.isAddressUnreachableError)(host); + // Resolve against a clean hostname (headers.host minus any port), not + // the full request URL — dns.lookup() can't parse a URL and would + // otherwise report every upstream error as ENOTFOUND. Parse via URL so + // IPv6 literals (e.g. "[::1]:443") are handled correctly, not split on ":". + const rawHost = ((_b = (_a = ctx.clientToProxyRequest) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.host) || ""; + let hostname = rawHost; + try { + hostname = new URL(`http://${rawHost}`).hostname.replace(/^\[|\]$/g, ""); + } + catch (e) { + hostname = rawHost.replace(/:\d+$/, ""); + } + 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], { diff --git a/dist/rq-proxy.d.ts b/dist/rq-proxy.d.ts index 796d56f..bca9653 100644 --- a/dist/rq-proxy.d.ts +++ b/dist/rq-proxy.d.ts @@ -11,8 +11,10 @@ declare class RQProxy { rulesHelper: RulesHelper; loggerService: ILoggerService; globalState: State; + private allowInsecureCerts?; constructor(proxyConfig: ProxyConfig, rulesDataSource: IRulesDataSource, loggerService: ILoggerService, initialGlobalState?: IInitialState); initProxy: (proxyConfig: ProxyConfig) => void; + setAllowInsecureCerts: (value: boolean) => void; doSomething: () => void; } export default RQProxy; diff --git a/dist/rq-proxy.js b/dist/rq-proxy.js index dab0874..08839f8 100644 --- a/dist/rq-proxy.js +++ b/dist/rq-proxy.js @@ -24,6 +24,7 @@ class RQProxy { sslCaDir: proxyConfig.certPath, host: "0.0.0.0", }, (err) => { + var _a, _b; console.log("Proxy Listen"); if (err) { console.log(err); @@ -32,6 +33,10 @@ class RQProxy { console.log("Proxy Started"); this.proxyMiddlewareManager = new proxy_middleware_1.default(this.proxy, proxyConfig, this.rulesHelper, this.loggerService, null); this.proxyMiddlewareManager.init(); + // Replay a toggle that was set before the manager existed. + if (this.allowInsecureCerts !== undefined) { + (_b = (_a = this.proxyMiddlewareManager).setAllowInsecureCerts) === null || _b === void 0 ? void 0 : _b.call(_a, this.allowInsecureCerts); + } } }); // For Testing // @@ -47,6 +52,14 @@ class RQProxy { }); // }; + // RQ-2425: live-update upstream TLS verification without restarting the proxy. + // Remembers the value so it survives even if set before the middleware manager + // is ready (replayed in initProxy once the manager is created). + this.setAllowInsecureCerts = (value) => { + var _a, _b; + 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); + }; this.doSomething = () => { console.log("do something"); }; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index b19b02d..0178822 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -4,6 +4,7 @@ export interface ProxyConfig { certPath: String; rootCertPath: String; onCARegenerated?: Function; + allowInsecureCerts?: boolean; } export interface Rule { id: string; diff --git a/src/components/proxy-middleware/helpers/handleUnreachableAddress.js b/src/components/proxy-middleware/helpers/handleUnreachableAddress.js index 8ea43b4..2a813d7 100644 --- a/src/components/proxy-middleware/helpers/handleUnreachableAddress.js +++ b/src/components/proxy-middleware/helpers/handleUnreachableAddress.js @@ -1,5 +1,16 @@ import dns from 'dns'; +// Escape request-derived values before embedding them into the HTML error pages +// so a crafted host/URL can't inject markup/script (reflected injection). +function escapeHtml(value) { + return String(value === undefined || value === null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export function isAddressUnreachableError(host) { return new Promise((resolve, reject) => { dns.lookup(host, (err, address) => { @@ -7,15 +18,130 @@ export function isAddressUnreachableError(host) { if (err.code === 'ENOTFOUND') { resolve(true); } else { - reject(err); + reject(err); } } else { - resolve(false); + resolve(false); } }); }); } +// RQ-2425: upstream TLS certificate verification failures. These surface when +// "Allow insecure SSL in proxy interceptor" is OFF (the default) and the origin +// presents an untrusted/expired/self-signed cert. Without this, such failures +// were misreported as ERR_NAME_NOT_RESOLVED, which is confusing. +const TLS_CERT_ERROR_CODES = new Set([ + 'CERT_HAS_EXPIRED', + 'CERT_NOT_YET_VALID', + 'DEPTH_ZERO_SELF_SIGNED_CERT', + 'SELF_SIGNED_CERT_IN_CHAIN', + 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', + 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', + 'UNABLE_TO_GET_ISSUER_CERT', + 'CERT_UNTRUSTED', + 'CERT_REVOKED', + 'CERT_REJECTED', + 'HOSTNAME_MISMATCH', + 'ERR_TLS_CERT_ALTNAME_INVALID', +]); + +export function isCertificateError(err) { + if (!err) return false; + if (err.code && TLS_CERT_ERROR_CODES.has(err.code)) return true; + const message = String((err && (err.message || err.reason)) || ''); + return /\bcertificate\b/i.test(message) || /ERR_TLS_CERT/i.test(message); +} + +// Maps a Node/OpenSSL cert error code to a Chrome-style token users recognise. +export function certErrorToken(code) { + switch (code) { + case 'CERT_HAS_EXPIRED': + case 'CERT_NOT_YET_VALID': + return 'ERR_CERT_DATE_INVALID'; + case 'HOSTNAME_MISMATCH': + case 'ERR_TLS_CERT_ALTNAME_INVALID': + return 'ERR_CERT_COMMON_NAME_INVALID'; + case 'CERT_REVOKED': + return 'ERR_CERT_REVOKED'; + default: + return 'ERR_CERT_AUTHORITY_INVALID'; + } +} + +export function dataToServeCertErrorPage(host, code) { + const token = certErrorToken(code); + const safeHost = escapeHtml(host); + const safeCode = escapeHtml(code); + return { + status: 502, + contentType: 'text/html', + errorToken: token, + body: ` + + +
+ +
+ + + +
+