Some checks failed
Main Confidence / confidence (push) Failing after 45s
## Summary - introduce surface-aware compressed governance outcomes and reuse the shared truth/explanation seams for operator-first summaries - apply the compressed outcome hierarchy across baseline, evidence, review, review-pack, canonical review/evidence, and artifact-oriented operation-run surfaces - expand spec 214 fixtures and Pest coverage, and fix tenant-panel route assertions by generating explicit tenant-panel URLs in the affected Filament tests ## Validation - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - focused governance compression suite from `specs/214-governance-outcome-compression/quickstart.md` passed (`68` tests, `445` assertions) - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryItemResourceTest.php tests/Feature/Filament/BackupSetUiEnforcementTest.php tests/Feature/Filament/RestoreRunUiEnforcementTest.php` passed (`18` tests, `81` assertions) Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #253
384 lines
15 KiB
Plaintext
384 lines
15 KiB
Plaintext
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
var socksClientCertificatesInterceptor_exports = {};
|
|
__export(socksClientCertificatesInterceptor_exports, {
|
|
ClientCertificatesProxy: () => ClientCertificatesProxy,
|
|
getMatchingTLSOptionsForOrigin: () => getMatchingTLSOptionsForOrigin,
|
|
rewriteOpenSSLErrorIfNeeded: () => rewriteOpenSSLErrorIfNeeded
|
|
});
|
|
module.exports = __toCommonJS(socksClientCertificatesInterceptor_exports);
|
|
var import_events = require("events");
|
|
var import_http2 = __toESM(require("http2"));
|
|
var import_net = __toESM(require("net"));
|
|
var import_stream = __toESM(require("stream"));
|
|
var import_tls = __toESM(require("tls"));
|
|
var import_socksProxy = require("./utils/socksProxy");
|
|
var import_utils = require("../utils");
|
|
var import_browserContext = require("./browserContext");
|
|
var import_network = require("./utils/network");
|
|
var import_debugLogger = require("./utils/debugLogger");
|
|
var import_happyEyeballs = require("./utils/happyEyeballs");
|
|
var import_utilsBundle = require("../utilsBundle");
|
|
let dummyServerTlsOptions = void 0;
|
|
function loadDummyServerCertsIfNeeded() {
|
|
if (dummyServerTlsOptions)
|
|
return;
|
|
const { cert, key } = (0, import_utils.generateSelfSignedCertificate)();
|
|
dummyServerTlsOptions = { key, cert };
|
|
}
|
|
class SocksProxyConnection {
|
|
constructor(socksProxy, uid, host, port) {
|
|
this._firstPackageReceived = false;
|
|
this._closed = false;
|
|
this.socksProxy = socksProxy;
|
|
this.uid = uid;
|
|
this.host = host;
|
|
this.port = port;
|
|
this._serverCloseEventListener = () => {
|
|
this._browserEncrypted.destroy();
|
|
};
|
|
this._browserEncrypted = new import_stream.default.Duplex({
|
|
read: () => {
|
|
},
|
|
write: (data, encoding, callback) => {
|
|
this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data });
|
|
callback();
|
|
},
|
|
destroy: (error, callback) => {
|
|
if (error)
|
|
socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message });
|
|
else
|
|
socksProxy._socksProxy.sendSocketEnd({ uid: this.uid });
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
async connect() {
|
|
const proxyAgent = this.socksProxy.getProxyAgent(this.host, this.port);
|
|
if (proxyAgent)
|
|
this._serverEncrypted = await proxyAgent.connect(new import_events.EventEmitter(), { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
|
|
else
|
|
this._serverEncrypted = await (0, import_happyEyeballs.createSocket)(rewriteToLocalhostIfNeeded(this.host), this.port);
|
|
this._serverEncrypted.once("close", this._serverCloseEventListener);
|
|
this._serverEncrypted.once("error", (error) => this._browserEncrypted.destroy(error));
|
|
if (this._closed) {
|
|
this._serverEncrypted.destroy();
|
|
return;
|
|
}
|
|
this.socksProxy._socksProxy.socketConnected({
|
|
uid: this.uid,
|
|
host: this._serverEncrypted.localAddress,
|
|
port: this._serverEncrypted.localPort
|
|
});
|
|
}
|
|
onClose() {
|
|
this._serverEncrypted.destroy();
|
|
this._browserEncrypted.destroy();
|
|
this._closed = true;
|
|
}
|
|
onData(data) {
|
|
if (!this._firstPackageReceived) {
|
|
this._firstPackageReceived = true;
|
|
if (data[0] === 22)
|
|
this._establishTlsTunnel(this._browserEncrypted, data);
|
|
else
|
|
this._establishPlaintextTunnel(this._browserEncrypted);
|
|
}
|
|
this._browserEncrypted.push(data);
|
|
}
|
|
_establishPlaintextTunnel(browserEncrypted) {
|
|
browserEncrypted.pipe(this._serverEncrypted);
|
|
this._serverEncrypted.pipe(browserEncrypted);
|
|
}
|
|
_establishTlsTunnel(browserEncrypted, clientHello) {
|
|
const browserALPNProtocols = parseALPNFromClientHello(clientHello) || ["http/1.1"];
|
|
import_debugLogger.debugLogger.log("client-certificates", `Browser->Proxy ${this.host}:${this.port} offers ALPN ${browserALPNProtocols}`);
|
|
const serverDecrypted = import_tls.default.connect({
|
|
socket: this._serverEncrypted,
|
|
host: this.host,
|
|
port: this.port,
|
|
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
|
ALPNProtocols: browserALPNProtocols,
|
|
servername: !import_net.default.isIP(this.host) ? this.host : void 0,
|
|
secureContext: this.socksProxy.secureContextMap.get(new URL(`https://${this.host}:${this.port}`).origin)
|
|
}, async () => {
|
|
const browserDecrypted = await this._upgradeToTLSIfNeeded(browserEncrypted, serverDecrypted.alpnProtocol);
|
|
import_debugLogger.debugLogger.log("client-certificates", `Proxy->Server ${this.host}:${this.port} chooses ALPN ${browserDecrypted.alpnProtocol}`);
|
|
browserDecrypted.pipe(serverDecrypted);
|
|
serverDecrypted.pipe(browserDecrypted);
|
|
const cleanup = (error) => this._serverEncrypted.destroy(error);
|
|
browserDecrypted.once("error", cleanup);
|
|
serverDecrypted.once("error", cleanup);
|
|
browserDecrypted.once("close", cleanup);
|
|
serverDecrypted.once("close", cleanup);
|
|
if (this._closed)
|
|
serverDecrypted.destroy();
|
|
});
|
|
serverDecrypted.once("error", async (error) => {
|
|
import_debugLogger.debugLogger.log("client-certificates", `error when connecting to server: ${error.message.replaceAll("\n", " ")}`);
|
|
this._serverEncrypted.removeListener("close", this._serverCloseEventListener);
|
|
this._serverEncrypted.destroy();
|
|
const browserDecrypted = await this._upgradeToTLSIfNeeded(this._browserEncrypted, serverDecrypted.alpnProtocol);
|
|
const responseBody = (0, import_utils.escapeHTML)("Playwright client-certificate error: " + error.message).replaceAll("\n", " <br>");
|
|
if (browserDecrypted.alpnProtocol === "h2") {
|
|
if ("performServerHandshake" in import_http2.default) {
|
|
const session = import_http2.default.performServerHandshake(browserDecrypted);
|
|
session.on("error", (error2) => {
|
|
this._browserEncrypted.destroy(error2);
|
|
});
|
|
session.once("stream", (stream2) => {
|
|
const cleanup = (error2) => {
|
|
session.close();
|
|
this._browserEncrypted.destroy(error2);
|
|
};
|
|
stream2.once("end", cleanup);
|
|
stream2.once("error", cleanup);
|
|
stream2.respond({
|
|
[import_http2.default.constants.HTTP2_HEADER_CONTENT_TYPE]: "text/html",
|
|
[import_http2.default.constants.HTTP2_HEADER_STATUS]: 503
|
|
});
|
|
stream2.end(responseBody);
|
|
});
|
|
} else {
|
|
this._browserEncrypted.destroy(error);
|
|
}
|
|
} else {
|
|
browserDecrypted.end([
|
|
"HTTP/1.1 503 Internal Server Error",
|
|
"Content-Type: text/html; charset=utf-8",
|
|
"Content-Length: " + Buffer.byteLength(responseBody),
|
|
"",
|
|
responseBody
|
|
].join("\r\n"));
|
|
}
|
|
});
|
|
}
|
|
async _upgradeToTLSIfNeeded(socket, alpnProtocol) {
|
|
this._brorwserDecrypted ??= new Promise((resolve, reject) => {
|
|
const dummyServer = import_tls.default.createServer({
|
|
...dummyServerTlsOptions,
|
|
ALPNProtocols: [alpnProtocol || "http/1.1"]
|
|
});
|
|
dummyServer.emit("connection", socket);
|
|
dummyServer.once("secureConnection", (tlsSocket) => {
|
|
dummyServer.close();
|
|
resolve(tlsSocket);
|
|
});
|
|
dummyServer.once("error", (error) => {
|
|
dummyServer.close();
|
|
reject(error);
|
|
});
|
|
});
|
|
return this._brorwserDecrypted;
|
|
}
|
|
}
|
|
class ClientCertificatesProxy {
|
|
constructor(contextOptions) {
|
|
this._connections = /* @__PURE__ */ new Map();
|
|
this.secureContextMap = /* @__PURE__ */ new Map();
|
|
(0, import_browserContext.verifyClientCertificates)(contextOptions.clientCertificates);
|
|
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
|
this._proxy = contextOptions.proxy;
|
|
this._initSecureContexts(contextOptions.clientCertificates);
|
|
this._socksProxy = new import_socksProxy.SocksProxy();
|
|
this._socksProxy.setPattern("*");
|
|
this._socksProxy.addListener(import_socksProxy.SocksProxy.Events.SocksRequested, async (payload) => {
|
|
try {
|
|
const connection = new SocksProxyConnection(this, payload.uid, payload.host, payload.port);
|
|
await connection.connect();
|
|
this._connections.set(payload.uid, connection);
|
|
} catch (error) {
|
|
import_debugLogger.debugLogger.log("client-certificates", `Failed to connect to ${payload.host}:${payload.port}: ${error.message}`);
|
|
this._socksProxy.socketFailed({ uid: payload.uid, errorCode: error.code });
|
|
}
|
|
});
|
|
this._socksProxy.addListener(import_socksProxy.SocksProxy.Events.SocksData, (payload) => {
|
|
this._connections.get(payload.uid)?.onData(payload.data);
|
|
});
|
|
this._socksProxy.addListener(import_socksProxy.SocksProxy.Events.SocksClosed, (payload) => {
|
|
this._connections.get(payload.uid)?.onClose();
|
|
this._connections.delete(payload.uid);
|
|
});
|
|
loadDummyServerCertsIfNeeded();
|
|
}
|
|
getProxyAgent(host, port) {
|
|
const proxyFromOptions = (0, import_network.createProxyAgent)(this._proxy);
|
|
if (proxyFromOptions)
|
|
return proxyFromOptions;
|
|
const proxyFromEnv = (0, import_utilsBundle.getProxyForUrl)(`https://${host}:${port}`);
|
|
if (proxyFromEnv)
|
|
return (0, import_network.createProxyAgent)({ server: proxyFromEnv });
|
|
}
|
|
_initSecureContexts(clientCertificates) {
|
|
const origin2certs = /* @__PURE__ */ new Map();
|
|
for (const cert of clientCertificates || []) {
|
|
const origin = normalizeOrigin(cert.origin);
|
|
const certs = origin2certs.get(origin) || [];
|
|
certs.push(cert);
|
|
origin2certs.set(origin, certs);
|
|
}
|
|
for (const [origin, certs] of origin2certs) {
|
|
try {
|
|
this.secureContextMap.set(origin, import_tls.default.createSecureContext(convertClientCertificatesToTLSOptions(certs)));
|
|
} catch (error) {
|
|
error = rewriteOpenSSLErrorIfNeeded(error);
|
|
throw (0, import_utils.rewriteErrorMessage)(error, `Failed to load client certificate: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
static async create(progress, contextOptions) {
|
|
const proxy = new ClientCertificatesProxy(contextOptions);
|
|
try {
|
|
await progress.race(proxy._socksProxy.listen(0, "127.0.0.1"));
|
|
return proxy;
|
|
} catch (error) {
|
|
await proxy.close();
|
|
throw error;
|
|
}
|
|
}
|
|
proxySettings() {
|
|
return { server: `socks5://127.0.0.1:${this._socksProxy.port()}` };
|
|
}
|
|
async close() {
|
|
await this._socksProxy.close();
|
|
}
|
|
}
|
|
function normalizeOrigin(origin) {
|
|
try {
|
|
return new URL(origin).origin;
|
|
} catch (error) {
|
|
return origin;
|
|
}
|
|
}
|
|
function convertClientCertificatesToTLSOptions(clientCertificates) {
|
|
if (!clientCertificates || !clientCertificates.length)
|
|
return;
|
|
const tlsOptions = {
|
|
pfx: [],
|
|
key: [],
|
|
cert: []
|
|
};
|
|
for (const cert of clientCertificates) {
|
|
if (cert.cert)
|
|
tlsOptions.cert.push(cert.cert);
|
|
if (cert.key)
|
|
tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
|
|
if (cert.pfx)
|
|
tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
|
|
}
|
|
return tlsOptions;
|
|
}
|
|
function getMatchingTLSOptionsForOrigin(clientCertificates, origin) {
|
|
const matchingCerts = clientCertificates?.filter(
|
|
(c) => normalizeOrigin(c.origin) === origin
|
|
);
|
|
return convertClientCertificatesToTLSOptions(matchingCerts);
|
|
}
|
|
function rewriteToLocalhostIfNeeded(host) {
|
|
return host === "local.playwright" ? "localhost" : host;
|
|
}
|
|
function rewriteOpenSSLErrorIfNeeded(error) {
|
|
if (error.message !== "unsupported" && error.code !== "ERR_CRYPTO_UNSUPPORTED_OPERATION")
|
|
return error;
|
|
return (0, import_utils.rewriteErrorMessage)(error, [
|
|
"Unsupported TLS certificate.",
|
|
"Most likely, the security algorithm of the given certificate was deprecated by OpenSSL.",
|
|
"For more details, see https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md#the-legacy-provider",
|
|
"You could probably modernize the certificate by following the steps at https://github.com/nodejs/node/issues/40672#issuecomment-1243648223"
|
|
].join("\n"));
|
|
}
|
|
function parseALPNFromClientHello(buffer) {
|
|
if (buffer.length < 6)
|
|
return null;
|
|
if (buffer[0] !== 22)
|
|
return null;
|
|
let offset = 5;
|
|
if (buffer[offset] !== 1)
|
|
return null;
|
|
offset += 4;
|
|
offset += 2;
|
|
offset += 32;
|
|
if (offset >= buffer.length)
|
|
return null;
|
|
const sessionIdLength = buffer[offset];
|
|
offset += 1 + sessionIdLength;
|
|
if (offset + 2 > buffer.length)
|
|
return null;
|
|
const cipherSuitesLength = buffer.readUInt16BE(offset);
|
|
offset += 2 + cipherSuitesLength;
|
|
if (offset >= buffer.length)
|
|
return null;
|
|
const compressionMethodsLength = buffer[offset];
|
|
offset += 1 + compressionMethodsLength;
|
|
if (offset + 2 > buffer.length)
|
|
return null;
|
|
const extensionsLength = buffer.readUInt16BE(offset);
|
|
offset += 2;
|
|
const extensionsEnd = offset + extensionsLength;
|
|
if (extensionsEnd > buffer.length)
|
|
return null;
|
|
while (offset + 4 <= extensionsEnd) {
|
|
const extensionType = buffer.readUInt16BE(offset);
|
|
offset += 2;
|
|
const extensionLength = buffer.readUInt16BE(offset);
|
|
offset += 2;
|
|
if (offset + extensionLength > extensionsEnd)
|
|
return null;
|
|
if (extensionType === 16)
|
|
return parseALPNExtension(buffer.subarray(offset, offset + extensionLength));
|
|
offset += extensionLength;
|
|
}
|
|
return null;
|
|
}
|
|
function parseALPNExtension(buffer) {
|
|
if (buffer.length < 2)
|
|
return null;
|
|
const listLength = buffer.readUInt16BE(0);
|
|
if (listLength !== buffer.length - 2)
|
|
return null;
|
|
const protocols = [];
|
|
let offset = 2;
|
|
while (offset < buffer.length) {
|
|
const protocolLength = buffer[offset];
|
|
offset += 1;
|
|
if (offset + protocolLength > buffer.length)
|
|
break;
|
|
const protocol = buffer.subarray(offset, offset + protocolLength).toString("utf8");
|
|
protocols.push(protocol);
|
|
offset += protocolLength;
|
|
}
|
|
return protocols.length > 0 ? protocols : null;
|
|
}
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
ClientCertificatesProxy,
|
|
getMatchingTLSOptionsForOrigin,
|
|
rewriteOpenSSLErrorIfNeeded
|
|
});
|