Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export function isAddressUnreachableError(host: any): Promise<any>;
export function isCertificateError(err: any): boolean;
export function certErrorToken(code: any): "ERR_CERT_DATE_INVALID" | "ERR_CERT_COMMON_NAME_INVALID" | "ERR_CERT_REVOKED" | "ERR_CERT_AUTHORITY_INVALID";
export function dataToServeCertErrorPage(host: any, code: any): {
status: number;
contentType: string;
errorToken: string;
body: string;
};
export function dataToServeUnreachablePage(host: any): {
status: number;
contentType: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isAddressUnreachableError = isAddressUnreachableError;
exports.isCertificateError = isCertificateError;
exports.certErrorToken = certErrorToken;
exports.dataToServeCertErrorPage = dataToServeCertErrorPage;
exports.dataToServeUnreachablePage = dataToServeUnreachablePage;
const dns_1 = __importDefault(require("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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function isAddressUnreachableError(host) {
return new Promise((resolve, reject) => {
dns_1.default.lookup(host, (err, address) => {
Expand All @@ -23,6 +36,119 @@ function isAddressUnreachableError(host) {
});
});
}
// 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',
]);
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.
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';
}
}
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: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${token}</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<style>
body {
background-color: #f2f2f2;
color: #333;
font-family: 'Roboto', sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 100px auto;
padding: 20px;
text-align: center;
}
.sad-face {
font-size: 80px;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
font-weight: normal;
margin-bottom: 10px;
}
p {
font-size: 16px;
color: #666;
}
code {
background: #e8e8e8;
padding: 2px 6px;
border-radius: 4px;
font-size: 14px;
}
@media (max-width: 600px) {
.container {
margin-top: 50px;
padding: 10px;
}
.sad-face {
font-size: 60px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="sad-face">:(</div>
<h1>This site's SSL certificate isn't trusted</h1>
<p>Requestly couldn't verify the TLS certificate of <strong>${safeHost}</strong>${code ? ` (<code>${safeCode}</code>)` : ''}.</p>
<p>If you trust this host, enable <strong>“Allow insecure SSL in proxy interceptor”</strong> in Requestly desktop settings and reload.</p>
<p><strong>${token}</strong></p>
</div>
</body>
</html>
`.trim()
};
}
function dataToServeUnreachablePage(host) {
return {
status: 502,
Expand Down Expand Up @@ -76,7 +202,7 @@ function dataToServeUnreachablePage(host) {
<div class="container">
<div class="sad-face">:(</div>
<h1>This site can’t be reached</h1>
<p>The webpage at <strong>${host}/</strong> might be temporarily down or it may have moved permanently to a new web address.</p>
<p>The webpage at <strong>${escapeHtml(host)}/</strong> might be temporarily down or it may have moved permanently to a new web address.</p>
<p><strong>ERR_NAME_NOT_RESOLVED</strong></p>
</div>
</body>
Expand Down
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 @@ -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;
Expand Down
49 changes: 46 additions & 3 deletions dist/components/proxy-middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand All @@ -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], {
Expand Down
2 changes: 2 additions & 0 deletions dist/rq-proxy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
13 changes: 13 additions & 0 deletions dist/rq-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 //
Expand All @@ -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");
};
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 @@ -4,6 +4,7 @@ export interface ProxyConfig {
certPath: String;
rootCertPath: String;
onCARegenerated?: Function;
allowInsecureCerts?: boolean;
}
export interface Rule {
id: string;
Expand Down
Loading
Loading