UNPKG

@openziti/ziti-browzer-sw-workbox-strategies

Version:

Custom Workbox Strategies used as part of the OpenZiti browZer stack

1,486 lines 80.1 kB
import { WorkboxError } from 'workbox-core/_private/WorkboxError.js';
import { CacheFirst } from 'workbox-strategies/CacheFirst.js';
import { Mutex } from 'async-mutex';
import { isUndefined, isEqual } from 'lodash-es';
import * as cheerio from 'cheerio';
import { ZitiBrowzerCore, ZITI_CONSTANTS } from '@openziti/ziti-browzer-core';
import pjson from '../package.json';
var regexZBR = new RegExp(/ziti-browzer-runtime-\w{8}\.js/, 'g');
var regexZBRnaked = new RegExp(/ziti-browzer-runtime\.js/, 'gi');
var regexZBRLogo = new RegExp(/ziti-browzer-logo/, 'g');
var regexZBRcss = new RegExp(/ziti-browzer-css-\w{8}\.css/, 'g');
var regexZBRCORS = new RegExp(/ziti-cors-proxy/, 'g');
var regexEdgeClt = new RegExp(/\/edge\/client\/v1/, 'g');
var regexZBWASM = new RegExp(/libcrypto.*.wasm/, 'g');
var regexPolipop = new RegExp(/polipop/, 'g');
var regexCannySetup = new RegExp(/canny-setup/, 'g');
var regexOAUTHTOKEN = new RegExp(/\/oauth\/token/, 'g');
var regexFavicon = new RegExp(/\/favicon\.ico/, 'g');
var regexJSDelivr = new RegExp(/jsdelivr.net/, 'g');
var regexZentaoRefreshRandom = new RegExp(/\/index\.php\?m\=user\&f\=refreshRandom/, 'g');
var regexZentaoLogin = new RegExp(/\/index\.php\?m\=user\&f\=login/, 'g');
var regexZentaoLoginReferer = new RegExp(/\/index\.php\?m\=user\&f\=login\&referer\=/, 'g');
var regexLibRdpHtml = new RegExp(/librdphtml/, 'g');
var regexAtImport = new RegExp(/\@import/, 'gi');
var regexSlash = new RegExp(/^\/$/, 'g');
var regexDotSlash = new RegExp(/^\.\//, 'g');
var regexTextHtml = new RegExp(/text\/html/, 'i');
var regexTextXml = new RegExp(/text\/xml/, 'i');
var regexMstsXml = new RegExp(/application\/x\-msts\-radc\+xml/, 'i');
var regexAppJS = new RegExp(/application\/javascript/, 'i');
var regexAppJSON = new RegExp(/application\/json/, 'i');
var regexVideo = new RegExp(/video/, 'i');
var regexMpeg = new RegExp(/mpeg/, 'i');
var regexImage = new RegExp(/image\//, 'i');
var regexCSS = new RegExp(/^.*\.css$/, 'i');
var regexJS = new RegExp(/^.*\.js$/, 'i');
var regexPNG = new RegExp(/^.*\.png$/, 'i');
var regexJPG = new RegExp(/^.*\.jpg$/, 'i');
var regexSVG = new RegExp(/^.*\.svg$/, 'i');
var regexControllerAPI;
const keycloakJs = `https://cdn.jsdelivr.net/npm/keycloak-js@23.0.1/dist/keycloak.min.js`;
const erudaJs = `https://cdn.jsdelivr.net/npm/eruda@3.0.1/eruda.min.js`;
/**
 * An implementation of a Ziti network request strategy.
 *
 * If the network request fails, and there is no cache match, this will throw
 * a `WorkboxError` exception.
 *
 */
class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ {
    /**
     * @param {Object} [options]
     * @param {string} [options._zitiBrowzerServiceWorkerGlobalScope] config dsts
     * @param {string} [options._logLevel] Which level to log at
     * @param {string} [options._controllerApi] Location of Ziti Controller
     * @param {number} [options.zitiNetworkTimeoutSeconds] If set, any network requests
     * that fail to respond within the timeout will fallback to the cache.
     *
     */
    constructor(options = {}) {
        super(options);
        this._zitiBrowzerServiceWorkerGlobalScope = options.zitiBrowzerServiceWorkerGlobalScope || 0;
        this._zitiNetworkTimeoutSeconds = options.zitiNetworkTimeoutSeconds || 0;
        this._logLevel = options.logLevel || 'Silent';
        this._controllerApi = options.controllerApi || '<controllerApi-not-configured>';
        this._initialized = false;
        this._targetServiceHost = '';
        var controllerAPIURL = new URL(this._controllerApi);
        regexControllerAPI = new RegExp(controllerAPIURL.host, 'g');
        this._initializationMutex = new Mutex();
        this._uuid = options.uuid;
        this._rootPaths = [];
        this._core = new ZitiBrowzerCore({});
        this.logger = this._core.createZitiLogger({
            logLevel: this._logLevel,
            suffix: 'ZBSW',
            useSWPostMessage: options.eruda,
            zitiBrowzerServiceWorkerGlobalScope: this._zitiBrowzerServiceWorkerGlobalScope,
        });
        this.logger.trace(`ZitiFirstStrategy ctor completed`);
    }
    parseCSP(policy) {
        const result = {};
        policy.split(";").forEach((directive) => {
            const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
            if (directiveKey &&
                !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
                result[directiveKey] = directiveValue;
            }
        });
        return result;
    }
    ;
    buildCSP({ directives }) {
        const namesSeen = new Set();
        const result = [];
        Object.keys(directives).forEach((originalName) => {
            const name = originalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
            if (namesSeen.has(name)) {
                throw new Error(`${originalName} is specified more than once`);
            }
            namesSeen.add(name);
            let value = directives[originalName];
            if (Array.isArray(value)) {
                value = value.join(" ");
            }
            else if (value === true) {
                value = "";
            }
            if (value) {
                result.push(`${name} ${value}`);
            }
            else if (value !== false) {
                result.push(name);
            }
        });
        return result.join("; ");
    }
    ;
    generateNewCSP(val) {
        let origCSP = this.parseCSP(val);
        this.logger.trace(`generateNewCSP() origCSP: `, origCSP);
        let idpURL = new URL(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.idp.host);
        let idpHost = idpURL.host;
        let controllerURL = new URL(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.controller.api);
        let controllerHost = controllerURL.host;
        if (origCSP['default-src']) {
            origCSP['default-src'].push(`https://*.netfoundry.io:*`);
            origCSP['default-src'].push(`https://*.cloudziti.io`);
            origCSP['default-src'].push(`wss://*.netfoundry.io:*`);
            origCSP['default-src'].push("data:");
            origCSP['default-src'].push("https://opencollective.com");
        }
        if (origCSP['script-src']) {
            origCSP['script-src'].push(`${this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.idp.host}`.replace('https://', ''));
            origCSP['script-src'].push(`canny.io`);
            if (!origCSP['script-src'].includes("'unsafe-eval'")) {
                origCSP['script-src'].push("'unsafe-eval'");
            }
        }
        if (origCSP['connect-src']) {
            origCSP['connect-src'].push(`${idpHost}`);
            origCSP['connect-src'].push(`${this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host}`);
            origCSP['connect-src'].push(`${controllerHost}`);
            origCSP['connect-src'].push(`https://*.netfoundry.io:*`);
            origCSP['connect-src'].push(`https://*.cloudziti.io`);
            origCSP['connect-src'].push(`wss://*.netfoundry.io:*`);
            origCSP['connect-src'].push(`wss://localhost:*`);
            if (!origCSP['connect-src'].includes("data:")) {
                origCSP['connect-src'].push("data:");
            }
        }
        if (origCSP['img-src']) {
            origCSP['img-src'].push(`data:`);
            origCSP['img-src'].push(`${this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host}`);
            origCSP['img-src'].push(`*`);
        }
        if (origCSP['font-src']) {
            if (!origCSP['font-src'].includes("data:")) {
                origCSP['font-src'].push("data:");
            }
        }
        let directives = {};
        if (!isUndefined(origCSP['child-src'])) {
            directives.childSrc = origCSP['child-src'];
        }
        if (!isUndefined(origCSP['connect-src'])) {
            directives.connectSrc = origCSP['connect-src'];
        }
        if (!isUndefined(origCSP['default-src'])) {
            directives.defaultSrc = origCSP['default-src'];
        }
        if (!isUndefined(origCSP['font-src'])) {
            directives.fontSrc = origCSP['font-src'];
        }
        if (!isUndefined(origCSP['frame-ancestors'])) {
            directives.frameAncestors = origCSP['frame-ancestors'];
        }
        if (!isUndefined(origCSP['frame-src'])) {
            directives.frameSrc = origCSP['frame-src'];
        }
        if (!isUndefined(origCSP['img-src'])) {
            directives.imgSrc = origCSP['img-src'];
        }
        if (!isUndefined(origCSP['media-src'])) {
            directives.mediaSrc = origCSP['media-src'];
        }
        if (!isUndefined(origCSP['object-src'])) {
            directives.objectSrc = origCSP['object-src'];
        }
        if (!isUndefined(origCSP['script-src'])) {
            directives.scriptSrc = origCSP['script-src'];
        }
        if (!isUndefined(origCSP['style-src'])) {
            directives.styleSrc = origCSP['style-src'];
        }
        if (!isUndefined(origCSP['worker-src'])) {
            directives.workerSrc = origCSP['worker-src'];
        }
        let newCSP = this.buildCSP({ directives });
        let newCSParray = this.parseCSP(newCSP);
        this.logger.trace(`generateNewCSP() newCSP: `, newCSParray);
        return newCSP;
    }
    /**
     * Remain in lazy-sleepy loop until z-b-runtime sends us the _zitiConfig
     *
     */
    async await_zitiConfig(requestUrl, _handler) {
        let self = this;
        let ctr = 0;
        let waitTime = 100;
        return new Promise((resolve, _reject) => {
            (async function waitFor_zitiConfig() {
                if (isUndefined(self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig) || isUndefined(self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.access_token)) {
                    ctr++;
                    self.logger.trace(`await_zitiConfig: ...waiting [${ctr}] for [${requestUrl}]`);
                    if (ctr == 5) { // kick the ZBR, and ask for the config
                        self.logger.trace('await_zitiConfig: sending ZITI_CONFIG_NEEDED msg to ZBR');
                        self._zitiBrowzerServiceWorkerGlobalScope._sendMessageToClients({ type: 'ZITI_CONFIG_NEEDED' });
                        setTimeout(waitFor_zitiConfig, waitTime);
                    }
                    else if (ctr == 10) { // only do the unregister once
                        self.logger.trace(`await_zitiConfig: initiating unregister`);
                        // Let's try and 'reboot' the ZBR/SW pair
                        await self._zitiBrowzerServiceWorkerGlobalScope._unregister();
                        return resolve(-1);
                    }
                    else {
                        setTimeout(waitFor_zitiConfig, waitTime);
                    }
                }
                else {
                    self.logger.trace(`await_zitiConfig: config acquired for [${requestUrl}]`);
                    return resolve(0);
                }
            })();
        });
    }
    /**
     * Remain in lazy-sleepy loop until z-b-runtime notifies us that it has completed initialization
     *
     */
    async await_zbrInitialized(request) {
        let self = this;
        let ctr = 0;
        return new Promise((resolve, reject) => {
            (function waitFor_zbrInitialized() {
                if (self._zitiBrowzerServiceWorkerGlobalScope._zbrReloadPending) { // this gets reset when ZBR sends the SW the 
                    self.logger.trace(`await_zbrInitialized: ...waiting for [${request.url}]`);
                    ctr++;
                    if (ctr > 40) {
                        return reject();
                    }
                    setTimeout(waitFor_zbrInitialized, 250);
                }
                else {
                    self.logger.trace(`await_zbrInitialized: ...acquired for [${request.url}]`);
                    self.logger.trace(`await_zbrInitialized: ...setting logLevel to [${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.sw.logLevel}]`);
                    self.logger.logLevel = self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.sw.logLevel;
                    return resolve();
                }
            })();
        });
    }
    accessTokenRefreshedEventHandler(accessTokenRefreshedEvent) {
        this.logger.trace(`accessTokenRefreshedEventHandler() ${accessTokenRefreshedEvent}`);
        this._zitiBrowzerServiceWorkerGlobalScope._currentAPISession = this._zitiBrowzerServiceWorkerGlobalScope._zitiContext.getCurrentAPISession();
        this._zitiBrowzerServiceWorkerGlobalScope._accessTokenRefreshed(); // This will cause ZBR to ask us for the new data
    }
    idpAuthHealthEventHandler(idpAuthHealthEvent) {
        this.logger.trace(`idpAuthHealthEventHandler() ${idpAuthHealthEvent}`);
        if (idpAuthHealthEvent.expired) {
            this.logger.trace(`idpAuthHealthEventHandler: authToken has expired and will be torn down`);
            setTimeout(function (_zitiBrowzerServiceWorkerGlobalScope) {
                _zitiBrowzerServiceWorkerGlobalScope._accessTokenExpired(); // This will cause a logout with the IdP
            }, 10, this._zitiBrowzerServiceWorkerGlobalScope);
            setTimeout(function (_zitiBrowzerServiceWorkerGlobalScope) {
                _zitiBrowzerServiceWorkerGlobalScope._unregister(); // Let's try and 'reboot' the ZBR/SW pair
            }, 500, this._zitiBrowzerServiceWorkerGlobalScope);
        }
    }
    async noConfigForServiceEventHandler(noConfigForServiceEvent) {
        this.logger.trace(`noConfigForServiceEventHandler() `, noConfigForServiceEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._noConfigForService(noConfigForServiceEvent);
    }
    async noConfigProtocolForServiceEventHandler(noConfigProtocolForServiceEvent) {
        this.logger.trace(`noConfigProtocolForServiceEventHandler() `, noConfigProtocolForServiceEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._noConfigProtocolForService(noConfigProtocolForServiceEvent);
    }
    async WSSEnabledEdgeRouterConnectionErrorEventHandler(wssERConnectionErrorEvent) {
        this.logger.trace(`WSSEnabledEdgeRouterConnectionErrorEventHandler() `, wssERConnectionErrorEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._wssERConnectionError(wssERConnectionErrorEvent);
    }
    async ControllerConnectionErrorEventHandler(controllerConnectionErrorEvent) {
        this.logger.trace(`ControllerConnectionErrorEventHandler() `, controllerConnectionErrorEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._controllerConnectionError(controllerConnectionErrorEvent);
    }
    async sessionCreationErrorEventHandler(sessionCreationErrorEvent) {
        this.logger.trace(`sessionCreationErrorEventHandler() `, sessionCreationErrorEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._sessionCreationError(sessionCreationErrorEvent);
        this._zitiBrowzerServiceWorkerGlobalScope._unregisterNoReload();
    }
    async noServiceEventHandler(noServiceEvent) {
        this.logger.trace(`noServiceEventHandler() `, noServiceEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._noService(noServiceEvent);
        this._zitiBrowzerServiceWorkerGlobalScope._unregisterNoReload();
    }
    async invalidAuthEventHandler(invalidAuthEvent) {
        this.logger.trace(`invalidAuthEventHandler() `, invalidAuthEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._invalidAuth(invalidAuthEvent);
    }
    async noWSSRoutersEventHandler(noWSSRoutersEvent) {
        this.logger.trace(`noWSSRoutersEventHandler() `, noWSSRoutersEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._noWSSRouters(noWSSRoutersEvent);
    }
    async channelConnectFailEventHandler(channelConnectFailEvent) {
        this.logger.trace(`channelConnectFailEventHandler() `, channelConnectFailEvent);
        await this._zitiBrowzerServiceWorkerGlobalScope._channelConnectFail(channelConnectFailEvent);
    }
    async xgressEventHandler(xgressEvent) {
        this._zitiBrowzerServiceWorkerGlobalScope._xgressEvent(xgressEvent);
    }
    async nestedTLSHandshakeTimeoutEventHandler(nestedTLSHandshakeTimeoutEvent) {
        this._zitiBrowzerServiceWorkerGlobalScope._nestedTLSHandshakeTimeout(nestedTLSHandshakeTimeoutEvent);
    }
    /**
     * Do all work necessary to initialize the ZitiFirstStrategy instance.
     *
     */
    async _initialize() {
        // Run the init sequence within a critical-section
        await this._initializationMutex.runExclusive(async () => {
            return new Promise(async (resolve, _) => {
                if (!this._initialized) {
                    this.logger.trace(`_initialize: entered`);
                    if (isUndefined(this._zitiContext)) {
                        this._zitiContext = this._core.createZitiContext({
                            logger: this.logger,
                            controllerApi: this._controllerApi,
                            sdkType: pjson.name,
                            sdkVersion: pjson.version,
                            sdkBranch: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.sdkBranch,
                            sdkRevision: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.sdkRevision,
                            token_type: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.token_type,
                            id_token: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.id_token,
                            access_token: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.access_token,
                            bootstrapperTargetService: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service,
                            bootstrapperHost: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host,
                        });
                        this.logger.trace(`_initialize: ZitiContext created`);
                        this._zitiBrowzerServiceWorkerGlobalScope._zitiContext = this._zitiContext;
                        // Make SW scope available to idpAuthHealthEventHandler
                        this._zitiContext._zitiBrowzerServiceWorkerGlobalScope = this._zitiBrowzerServiceWorkerGlobalScope;
                        this._zitiContext.setKeyTypeEC();
                        await this._zitiContext.initialize({
                            loadWASM: true, // unlike the ZBR, here in the ZBSW, we always instantiate the internal WebAssembly
                            doAuthenticate: true, // unlike the ZBR, here in the ZBSW, we always auth with Controller
                            jspi: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.jspi,
                            target: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target,
                            bootstrapperHost: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host
                        });
                        await this._zitiContext.listControllerVersion();
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_IDP_AUTH_HEALTH, this.idpAuthHealthEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_CONFIG_FOR_SERVICE, this.noConfigForServiceEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_SERVICE, this.noServiceEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_SESSION_CREATION_ERROR, this.sessionCreationErrorEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_INVALID_AUTH, this.invalidAuthEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_CHANNEL_CONNECT_FAIL, this.channelConnectFailEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_WSS_ROUTERS, this.noWSSRoutersEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_XGRESS, this.xgressEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NESTED_TLS_HANDSHAKE_TIMEOUT, this.nestedTLSHandshakeTimeoutEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_CONFIG_PROTOCOL_FOR_SERVICE, this.noConfigProtocolForServiceEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_WSS_ROUTER_CONNECTION_ERROR, this.WSSEnabledEdgeRouterConnectionErrorEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_CONTROLLER_CONNECTION_ERROR, this.ControllerConnectionErrorEventHandler);
                        this._zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_ACCESS_TOKEN_REFRESHED, this.accessTokenRefreshedEventHandler);
                        this.logger.trace(`_initialize: ZitiContext '${this._uuid}' initialized`);
                    }
                    else {
                        this.logger.trace(`_initialize: initiating unregister`);
                        await this._zitiBrowzerServiceWorkerGlobalScope._unregister(); // Let's try and 'reboot' the ZBR/SW pair
                        this.logger.trace(`_initialize: terminated`);
                    }
                    setTimeout(async function (self, resolve) {
                        let result = await self._zitiContext.enroll(); // this acquires an ephemeral Cert
                        if (!result) {
                            self.logger.trace(`_initialize: ephemeral Cert acquisition failed`);
                            // If we couldn't acquire a cert, it most likely means that the JWT from the IdP needs a refresh
                            self.logger.trace(`_initialize: initiating unregister`);
                            await self._zitiBrowzerServiceWorkerGlobalScope._unregister(); // Let's try and 'reboot' the ZBR/SW pair
                            self.logger.trace(`_initialize: terminated`);
                        }
                        else {
                            self.logger.trace(`_initialize: ephemeral Cert acquisition succeeded`);
                            self._rootPaths.push(self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.path);
                            self._initialized = true;
                            self._zitiBrowzerServiceWorkerGlobalScope._currentAPISession = self._zitiContext.getCurrentAPISession();
                            self.logger.trace(`_initialize: ZitiContext '${self._uuid}' initialize complete`);
                        }
                        return resolve(null);
                    }, 500, this, resolve);
                }
                else {
                    return resolve(null);
                }
            });
        })
            .catch((err) => {
            this.logger.error(err);
            return new Promise(async (_, reject) => {
                reject(err);
            });
        });
    }
    _sendServiceUnavailable(_zitiBrowzerServiceWorkerGlobalScope, newUrl) {
        _zitiBrowzerServiceWorkerGlobalScope._sendMessageToClients({
            type: 'SERVICE_UNAVAILABLE_TO_IDENTITY',
            payload: {
                message: `Ziti Service ${newUrl.hostname} is unavailable to your identity; Notify your administrator.`
            }
        });
    }
    /**
     * Determine if this request should be routed over Ziti, or over raw internet.
     *
     * @private
     * @param {Request} request The request from the fetch event.
     * @return {ZitiShouldRouteResult} If request should go over Ziti we return a (possibly adjusted) URL
  
     */
    async _shouldRouteOverZiti(request) {
        let result = {};
        this.logger.trace(`_shouldRouteOverZiti starting`);
        let url = new URL(request.url);
        result.url = url.toString();
        let targetHost = url.hostname;
        let targetPort = url.port;
        if (isEqual(targetHost, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host) && isEqual(targetPort, '')) {
            targetPort = '443';
        }
        let targetPath = url.pathname;
        this.logger.trace(`_shouldRouteOverZiti targetHost:port path is: ${targetHost}:${targetPort} ${targetPath}`);
        try {
            // We want to intercept fetch requests that target the Ziti BrowZer Bootstrapper... that is...
            // ...we want to intercept any request from the web app that targets the server from 
            // which the app was loaded.
            let targetserviceHost = await this._zitiContext.getConfigHostByServiceName(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service);
            let connectAppData = await this._zitiContext.getConnectAppDataByServiceName(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.scheme);
            var targetServiceRegex = new RegExp(targetserviceHost, 'g');
            var browzerLoadBalancerRegex = new RegExp(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.loadbalancer.host, 'g');
            if ((isEqual(targetHost, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host) && (isEqual(targetPort, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.port))) // yes, the request is targeting the Ziti BrowZer Bootstrapper
                ||
                    (isEqual(targetHost, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host) && (isEqual(targetPort, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.loadbalancer.port))) // yes, the request is targeting the Ziti BrowZer LB
                ||
                    (this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.loadbalancer.host && request.url.match(browzerLoadBalancerRegex))) { // yes, the request is targeting the Ziti BrowZer LoadBalancer
                var newUrl = new URL(request.url);
                if (isEqual(targetPath, '/')) {
                    result.routeOverZiti = true;
                    result.serviceName = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service;
                    result.serviceScheme = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.scheme;
                    result.serviceConnectAppData = connectAppData;
                }
                else if ((request.url.match(regexZBR)) || (request.url.match(regexZBRnaked)) || (request.url.match(regexZBWASM)) || (request.url.match(regexZBRLogo)) || (request.url.match(regexZBRcss)) || (request.url.match(regexZBRCORS))) { // the request seeks z-b-r/wasm/logo/css/cors-proxy
                    this.logger.trace(`_shouldRouteOverZiti: z-b-r/css/wasm/logo, bypassing intercept of [${request.url}]`);
                    result.routeOverZiti = false;
                }
                else {
                    // Don't muck with URL only because top-level domain of target matches the top-level domain of the load-balancer. 
                    // Only do that if the entire hostname matches, or else sub-domains represented by different Services will be routed 
                    // to the wrong place.
                    if (isEqual(newUrl.hostname, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host)) {
                        newUrl.hostname = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service;
                        newUrl.port = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.port;
                        if (isEqual(newUrl.port, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.port) ||
                            isEqual(newUrl.port, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.loadbalancer.port)) {
                            newUrl.port = '';
                        }
                    }
                    var pathnameArray = newUrl.pathname.split('/');
                    var targetpathnameArray = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.path.split('/');
                    if (!isEqual(pathnameArray[1], targetpathnameArray[1])) {
                        newUrl.pathname = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.path + newUrl.pathname;
                        newUrl.pathname = newUrl.pathname.replace('//', '/');
                    }
                    this.logger.trace('_shouldRouteOverZiti: transformed URL: ', newUrl.toString());
                    result.serviceName = await this._zitiContext.shouldRouteOverZiti(newUrl);
                    this.logger.trace(`_shouldRouteOverZiti result.serviceName[${result.serviceName}]`);
                    if (isUndefined(result.serviceName) || isEqual(result.serviceName, '')) { // If we have no config associated with the hostname:port, do not intercept
                        this.logger.warn(`_shouldRouteOverZiti: no associated Ziti config, bypassing intercept of [${request.url}]`);
                        setTimeout(this._sendServiceUnavailable, 250, this._zitiBrowzerServiceWorkerGlobalScope, newUrl);
                    }
                    else {
                        result.routeOverZiti = true;
                        result.url = newUrl.toString();
                        result.serviceScheme = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.scheme;
                        result.serviceConnectAppData = connectAppData;
                    }
                }
            }
            // If no routing determination has been made yet
            if (isUndefined(result.routeOverZiti)) {
                result.serviceName = await this._zitiContext.shouldRouteOverZiti(url);
                this.logger.trace(`_shouldRouteOverZiti result.serviceName[${result.serviceName}]`);
                if (isUndefined(result.serviceName) || isEqual(result.serviceName, '')) { // If we have no config associated with the hostname:port, do not intercept
                    this.logger.warn(`_shouldRouteOverZiti: no associated Ziti config, bypassing intercept of [${request.url}]`);
                    result.routeOverZiti = false;
                }
                else {
                    result.routeOverZiti = true;
                    result.url = url.toString();
                    result.serviceScheme = url.protocol;
                    let connectAppData = await this._zitiContext.getConnectAppDataByServiceName(result.serviceName, url.protocol);
                    result.serviceConnectAppData = connectAppData;
                }
            }
        }
        catch (e) {
            this.logger.error(e);
        }
        this.logger.trace(`_shouldRouteOverZiti result[${result.routeOverZiti}]`);
        return result;
    }
    /**
     * Determine if this request can used previously cached response, or be routed over Ziti/internet.
     *
     * @private
     * @param {Request} request The request from the fetch event.
     * @return {boolean} If request can used previously cached response
  
     */
    _shouldUseCache(request) {
        if (request.method !== 'GET') { // Only cache GET responses
            this.logger.trace(`_shouldUseCache: handling ${request.method} method; NOT using cache`);
            return false;
        }
        if (request.url.match(regexEdgeClt)) { // Never cache responses from Ziti Controller
            this.logger.trace(`_shouldUseCache: handling request to Ziti Controller; NOT using cache`);
            return false;
        }
        if (request.url.match(regexControllerAPI)) { // Never cache responses from Ziti Controller
            this.logger.trace(`_shouldUseCache: handling request to Ziti Controller; NOT using cache`);
            return false;
        }
        // We will allow the SW to cache teh ZBR/WASM files ...for the moment
        //
        // if ( (request.url.match( regexZBR )) || ((request.url.match( regexZBWASM ))) ) { // Do not cache the ZBR/WASM
        //   this.logger.trace(`_shouldUseCache: handling request for ZBR|WASM; NOT using cache`);
        //   return false;
        // }
        if (request.url.match(regexSlash)) { // Never cache responses for root path
            this.logger.trace(`_shouldUseCache: handling request for '/'; NOT using cache`);
            return false;
        }
        let url = new URL(request.url);
        if (url.pathname === '/') { // Do not cache the web app's root path
            this.logger.trace(`_shouldUseCache: handling request for ROOT path; NOT using cache`);
            return false;
        }
        let isRootPath = this._rootPaths.find((element) => element === `${url.pathname}`);
        if (isRootPath) { // Do not cache the web app's root path
            this.logger.trace(`_shouldUseCache: handling request for ROOT path; NOT using cache`);
            return false;
        }
        if (url.search !== '') { // Do not cache requests with search parms
            this.logger.trace(`_shouldUseCache: handling request with search parms; NOT using cache`);
            return false;
        }
        // if (request.url.match( regexLibRdpHtml )) {
        //   this.logger.trace(`_shouldUseCache: handling request with librdphtml; NOT using cache`);
        //   return false;
        // }
        // Cache everything else
        this.logger.trace(`_shouldUseCache: we WILL cache response for ${request.url}`);
        return true;
    }
    _isRootPATH(request) {
        if (request.url.match(regexSlash)) {
            return true;
        }
        let url = new URL(request.url);
        if (url.pathname === '/') {
            return true;
        }
        let isRootPath = this._rootPaths.find((element) => element === `${url.pathname}`);
        if (isRootPath) {
            return true;
        }
        return false;
    }
    /**
     * @private
     * @param {Request|string} request A request to run this strategy for.
     * @param {workbox-strategies.StrategyHandler} handler The event that triggered the request.
     * @return {Promise<Response>}
     */
    async _handle(request, handler) {
        let tryZiti;
        this.logger.trace(`_handle entered for: [${request.url}]`);
        const requestURL = new URL(request.url);
        // If hitting the Controller, or seeking z-b-runtime|WASM, then
        // we never go over Ziti, and we let the browser route the request 
        // to the Controller or browZer Bootstrapper.  
        if ((request.url.match(regexControllerAPI)) || //    "     "      "
            (request.url.match(regexZBR)) || // seeking Ziti BrowZer Runtime
            (request.url.match(regexZBRnaked)) || // seeking Ziti BrowZer Runtime
            (request.url.match(regexZBRLogo)) || // seeking Ziti BrowZer Logo
            (request.url.match(regexZBRCORS)) || // seeking Ziti BrowZer CORS proxy
            (request.url.match(regexZBRcss)) || // seeking Ziti BrowZer CSS
            (request.url.match(regexPolipop)) || // seeking Ziti Polipop
            (request.url.match(regexCannySetup)) || // seeking Canny setup
            (request.url.match(regexOAUTHTOKEN)) || // seeking IdP token 
            (request.url.match(regexFavicon)) || // seeking favicon
            (request.url.match(regexZBWASM)) || // seeking Ziti BrowZer WASM
            (request.url.match(regexJSDelivr)) // seeking CDN content
        ) {
            tryZiti = false;
        }
        else {
            tryZiti = true;
        }
        // if (request.url.match( regexLibRdpHtml )) {
        //   await this._zitiBrowzerServiceWorkerGlobalScope._sendMessageToClients( { type: 'REAPPLY_WEBSOCKET_INTERCEPT'} );
        // }
        if (this._isRootPATH(request)) {
            const url = new URL(request.url);
            const urlSearchParams = new URLSearchParams(url.search);
            const codeParm = urlSearchParams.get('code');
            const stateParm = urlSearchParams.get('state');
            if (codeParm && stateParm) { // possible IdP-related URL
                if (this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig && requestURL.hostname === this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host) { // ..but if hitting the protected web app itself
                    tryZiti = true; // ..then let it go over Ziti
                }
                else {
                    tryZiti = false; // ..otherwise, route over raw internet since it's IdP-related
                }
            }
        }
        if (tryZiti && this._zitiBrowzerServiceWorkerGlobalScope._zbrReloadPending) {
            if (request.url.match(regexZBWASM)) { // the ZBR loads the WASM during init, so we need to process that request; all others wait
                /* NOP */
            }
            else if (request.url.match(regexControllerAPI)) { // the ZBR hits the Ziti Controller during init, so we need to process that request; all others wait
                /* NOP */
            }
            else {
                await this.await_zbrInitialized(request).catch(async (_err) => {
                    this.logger.debug(`ZBR init not responding`);
                    await this._zitiBrowzerServiceWorkerGlobalScope._unregister();
                    throw new WorkboxError('no-response', { url: request.url });
                });
            }
        }
        if (tryZiti && (!this._isRootPATH(request)) && (!request.url.match(regexZBR))) { // if NOT in the process of bootstrapping from HTTP Agent
            if (isUndefined(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig)) { // ...and we don't yet have the zitiConfig from ZBR
                if (!request.url.match(regexEdgeClt)) { // ...and NOT hitting the controller
                    let result = await this.await_zitiConfig(request.url, handler); // ...then wait for ZBR to send zitiConfig to us
                    if (result < 0) {
                        let redirectResponse = new Response('', {
                            status: 302,
                            statusText: 'Found',
                            headers: {
                                Location: '/'
                            }
                        });
                        return redirectResponse;
                    }
                    ;
                }
            }
        }
        let self = this;
        let skipInject = false;
        let useCache = this._shouldUseCache(request);
        if (useCache) {
            let cachResponse = await handler.cacheMatch(request);
            if (cachResponse) {
                return cachResponse;
            }
        }
        const promises = [];
        let timeoutId;
        let bootstrappingZBRFromSW;
        let bootstrappingZBRFromSWConfigNeeded;
        let response;
        let shouldRoute = { routeOverZiti: false };
        if (tryZiti) {
            if (this._isRootPATH(request)) { // seeking root path
                bootstrappingZBRFromSW = true;
                if (isUndefined(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig)) { // ...but we don't yet have the zitiConfig from ZBR
                    tryZiti = false; // ...then we're bootstrapping, so load ZBR from HTTP Agent
                    bootstrappingZBRFromSWConfigNeeded = true;
                }
            }
        }
        if (tryZiti) {
            if (isUndefined(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig)) {
                let result = await this.await_zitiConfig(request.url, handler);
                if (result < 0) {
                    let redirectResponse = new Response('', {
                        status: 302,
                        statusText: 'Found',
                        headers: {
                            Location: '/'
                        }
                    });
                    return redirectResponse;
                }
                ;
            }
            // If going over Ziti, we must first complete the work to ensure WASM is instantiated, we have a cert, etc
            await this.await_zitiConfig('null', handler); // wait for ZBR to send zitiConfig to us
            await this._initialize();
            this._targetServiceHost = await this._zitiContext.getConfigHostByServiceName(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service);
            // Now determine if we're going over Ziti or not
            shouldRoute = await this._shouldRouteOverZiti(request);
        }
        if (this._zitiNetworkTimeoutSeconds) {
            const { id, promise } = this._getZitiTimeoutPromise({ request, handler });
            timeoutId = id;
            promises.push(promise);
        }
        let networkPromise;
        let zitiNetworkPromise;
        if (!shouldRoute.routeOverZiti) {
            this.logger.trace(`_handle: ------- routing over raw internet ----------`);
            networkPromise = this._getNetworkPromise({
                timeoutId,
                request,
                handler,
                useCache,
            });
            promises.push(networkPromise);
        }
        else {
            this.logger.trace(`_handle: ------- routing over Ziti ----------`);
            zitiNetworkPromise = this._getZitiNetworkPromise({
                timeoutId,
                shouldRoute,
                request,
                handler,
                useCache,
            });
            promises.push(zitiNetworkPromise);
        }
        response = await handler.waitUntil((async () => {
            let netPromise = networkPromise || zitiNetworkPromise;
            // Promise.race() will resolve as soon as the first promise resolves.
            return ((await handler.waitUntil(Promise.race(promises))) ||
                // If Promise.race() resolved with null, it might be due to a network
                // timeout + a cache miss. If that were to happen, we'd rather wait until
                // the netPromise (which is either over Ziti or raw internet) resolves 
                // instead of returning null.
                //
                // Note that it's fine to await an already-resolved promise, so we don't
                // have to check to see if it's still "in flight".
                (await netPromise));
        })()).catch((err) => {
            this.logger.error(err);
            return new Promise(async (_, reject) => {
                reject(err);
            });
        });
        if (!response) {
            this.logger.error(`no-response when trying to reach URL [${request.url}]`);
            await this._zitiBrowzerServiceWorkerGlobalScope._requestFailedWithNoResponse({
                url: request.url
            });
            let errResponse = new Response('', {
                status: 500,
                statusText: 'ServerError',
            });
            return errResponse;
        }
        const location = response.headers.get('Location');
        const contentType = response.headers.get('Content-Type');
        if (request.url.match(regexZentaoRefreshRandom)) {
            skipInject = true;
        }
        if (request.url.match(regexZentaoLogin) && !request.url.match(regexZentaoLoginReferer)) {
            skipInject = true;
        }
        if (location && response.status >= 300 && response.status < 400) {
            if (!this._rootPaths.find((element) => element === `${location}`)) {
                this._rootPaths.push(location);
            }
            skipInject = true;
        }
        if (response.status === 403) {
            skipInject = true;
        }
        if (!contentType || !contentType.match(regexTextHtml)) {
            skipInject = true;
        }
        if (contentType && contentType.match(regexTextHtml)) {
            /**
             * Jenkins thing
             */
            useCache = false;
        }
        if (contentType && contentType.match(regexTextXml)) {
            /**
             * MSFT RDP thing
             */
            // useCache = false;
        }
        if (contentType && contentType.match(regexMstsXml)) {
            /**
             * MSFT RDP thing
             */
            // useCache = false;
        }
        if (contentType && contentType.match(regexAppJSON)) {
            useCache = false;
        }
        if ((contentType && contentType.match(regexVideo)) || (contentType && contentType.match(regexMpeg))) {
            /**
             * streaming media server thing
             */
            useCache = false;
        }
        function streamingAttrReplace($, elementType, attrType) {
            $(elementType).each(async (_, e) => {
                let attr = $(e).attr(attrType);
                if (attr) {
                    try {
                        let url = new URL(attr);
                        if (isEqual(url.host, self._targetServiceHost)) {
                            attr = attr.replace('http:', 'https:');
                            attr = attr.replace(self._targetServiceHost, self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host);
                            $(e).attr(attrType, attr);
                        }
                    }
                    catch (e) {
                        let newUrl = new URL(`${requestURL.origin}`);
                        if (!attr.startsWith('/')) {
                            newUrl.pathname = `${requestURL.pathname}/${attr}`;
                        }
                        else {
                            newUrl.pathname = `${requestURL.pathname}${attr}`;
                        }
                        newUrl.pathname = newUrl.pathname.replace('//', '/');
                        attr = newUrl.toString();
                        self.logger.trace(`streamingAttrReplace: transformed attr[${attr}] `);
                        $(e).attr(attrType, attr);
                    }
                }
            });
        }
        function streamingImportReplace($, elementType) {
            $(elementType).each(async (_, e) => {
                let text = $(e).text();
                if (text.match(regexAtImport)) {
                    let importUrl = text.slice(text.indexOf('@') + 7);
                    importUrl = importUrl.replaceAll('"', '');
                    importUrl = importUrl.replace(';', '');
                    let url = new URL(importUrl);
                    if (isEqual(url.host, self._targetServiceHost)) {
                        text = text.replace('http:', 'https:');
                        text = text.replace(self._targetServiceHost, self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host);
                        $(e).text(text);
                    }
                }
            });
        }
        if (shouldRoute.routeOverZiti && !skipInject) {
            var ignore = false;
            if (!ignore) {
                if (response.body) {
                    let fromBootstrapper = false;
                    let fullDocument = true;
                    function _obtainBootStrapperURL() {
                        let url;
                        if (self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.loadbalancer.host) {
                            url = `https://${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host}:${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.loadbalancer.port}`;
                        }
                        else {
                            url = `${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.scheme}://${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host}:${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.port}`;
                        }
                        return url;
                    }
                    function streamingHEADReplace() {
                        let buffer = '';
                        return new TransformStream({
                            transform(chunk, _controller) {
                                try {
                                    if (fullDocument && !fromBootstrapper) {
                                        let zbrLocation = self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.runtime.src;
                                        // Parse the HTML
                                        const $ = cheerio.load(chunk, {
                                            decodeEntities: false // ensure we don't evaporate Angular-specific attributes like ng-app, ng-model, etc.
                                        });
                                        // Ensure any links in the HTML that point to the target service are using the correct scheme
                                        streamingAttrReplace($, 'link', 'href');
                                        streamingAttrReplace($, 'a', 'href');
                                        streamingAttrReplace($, 'area', 'href');
                                        streamingAttrReplace($, 'base', 'href');
                                        streamingAttrReplace($, 'img', 'src');
                                        streamingAttrReplace($, 'audio', 'src');
                                        streamingAttrReplace($, 'embed', 'src');
                                        streamingAttrReplace($, 'iframe', 'src');
                                        streamingAttrReplace($, 'input', 'src');
                                        streamingAttrReplace($, 'script', 'src');
                                        streamingAttrReplace($, 'source', 'src');
                                        streamingAttrReplace($, 'track', 'src');
                                        streamingAttrReplace($, 'video', 'src');
                                        // Ensure any @import's in the HTML that point to the target service are using the correct scheme
                                        streamingImportReplace($, 'style');
                                        self.logger.trace(`streamingHEADReplace: HTML before modifications is: ${$.html()}`);
                                        let zbrElement = $('<script></script> ').attr('id', 'from-ziti-browzer-sw').attr('type', 'text/javascript').attr('src', `${_obtainBootStrapperURL()}/${zbrLocation}`); //.attr('defer', `defer`);
                                        let cannyElement = $('<script></script> ')
                                            .attr('id', 'ziti-browzer-canny-setup')
                                            .attr('type', 'text/javascript')
                                            .attr('src', `${_obtainBootStrapperURL()}/canny-setup.js`);
                                        let ppElement = $('<script></script> ')
                                            .attr('id', 'ziti-browzer-pp')
                                            .attr('type', 'text/javascript')
                                            .attr('src', `${_obtainBootStrapperURL()}/polipop.min.js`);
                                        let ppCss1Element = $('<link> ')
                                            .attr('id', 'ziti-browzer-ppcss')
                                            .attr('rel', 'stylesheet')
                                            .attr('href', `${_obtainBootStrapperURL()}/polipop.core.min.css`);
                                        let ppCss2Element = $('<link> ')
                                            .attr('rel', 'stylesheet')
                                            .attr('href', `${_obtainBootStrapperURL()}/polipop.compact.min.css`);
                                        // let otElement = $('<meta></meta> ')
                                        //     .attr('id', 'ziti-browzer-origin-trial')
                                        //     .attr('http-equiv', 'origin-trial')
                                        //     .attr('content', `${self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.runtime.originTrialToken}`);
                                        let kcElement = $('<meta name="author" content="OpenZiti BrowZer" />');
                                        // Locate the CSP
                                        let cspElement = $('meta[http-equiv="content-security-policy"]');
                                        // If we found a CSP
                                        if (cspElement.length > 0) {
                                            // self.logger.trace('streamingHEADReplace: CSP found in html with content: ', cspElement.attr('content'));
                                            // then augment it to enable WASM load/xeq
                                            let cspContent = cspElement.attr('content');
                                            let newCspContent = self.generateNewCSP(cspContent);
                                            cspElement.attr('content', newCspContent);
                                            // self.logger.trace('streamingHEADReplace: CSP is now enhanced with content: ', cspElement.attr('content'));
                                            // Inject the PP immediately after the CSP
                                            cspElement.after(ppCss1Element);
                                            cspElement.after(ppCss2Element);
                                            cspElement.after(ppElement);
                                            // cspElement.after(otElement);
                                            // let otEl = $('meta[id="ziti-browzer-origin-trial"]');
                                            // Inject the ZBR immediately after the origin trial meta
                                            cspElement.after(zbrElement);
                                            cspElement.after(cannyElement);
                                            buffer += $.html();
                                        }
                                        // If we did NOT find a CSP
                                        else {
                                            // Locate the HEAD
                                            let headElement = $('head');
                                            headElement.prepend(cannyElement);
                                            // Inject the Ziti browZer Runtime at the front of <head> element so we are prepared to intercept as soon as possible over on the browser
                                            headElement.prepend(zbrElement);
                                            headElement.prepend(ppElement);
                                            headElement.prepend(ppCss2Element);
                                            headElement.prepend(ppCss1Element);
                                            // headElement.prepend(otElement);
                                            buffer += $.html();
                                        }
                                    }
                                    else {
                                        buffer += chunk;
                                    }
                                }
                                catch (e) {
                                    self.logger.error(e);
                                }
                            },
                            flush(controller) {
                                if (buffer) {
                                    buffer = buffer.replace('document.domain', 'document.zitidomain');
                                    self.logger.trace(`streamingHEADReplace: HTML after modifications is: ${buffer}`);
                                    controller.enqueue(buffer);
                                }
                            }
                        });
                    }
                    const bodyStream = response.body
                        .pipeThrough(new TextDecoderStream())
                        .pipeThrough(streamingHEADReplace())
                        .pipeThrough(new TextEncoderStream());
                    const newHeaders = new Headers(response.headers);
                    const newResponse = new Response(bodyStream, {
                        status: response.status,
                        statusText: response.statusText,
                        headers: newHeaders
                    });
                    response = newResponse;
                }
            }
        }
        else {
            if (shouldRoute.routeOverZiti && (contentType && contentType.match(regexTextXml))) {
                if (response.body) {
                    function streamingXMLReplace() {
                        let buffer = '';
                        return new TransformStream({
                            transform(chunk, _controller) {
                                try {
                                    // Parse the XML
                                    const $ = cheerio.load(chunk, {
                                        xmlMode: true,
                                    });
                                    self.logger.trace('streamingXMLReplace: XML before modifications is: ', $.html());
                                    streamingAttrReplace($, 'RDWAPage', 'baseurl');
                                    buffer += $.html();
                                }
                                catch (e) {
                                    self.logger.error(e);
                                }
                            },
                            flush(controller) {
                                if (buffer) {
                                    buffer = buffer.replace(self._targetServiceHost, self._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host);
                                    self.logger.trace('streamingXMLReplace: XML after modifications is: ', buffer);
                                    controller.enqueue(buffer);
                                }
                            }
                        });
                    }
                    const bodyStream = response.body
                        .pipeThrough(new TextDecoderStream())
                        .pipeThrough(streamingXMLReplace())
                        .pipeThrough(new TextEncoderStream());
                    const newHeaders = new Headers(response.headers);
                    const newResponse = new Response(bodyStream, {
                        status: response.status,
                        statusText: response.statusText,
                        headers: newHeaders
                    });
                    response = newResponse;
                }
            }
        }
        if (useCache) {
            await handler.waitUntil(handler.cachePut(request, response.clone()));
        }
        return response;
    }
    /**
     * @param {Object} options
     * @param {Request} options.request
     * @param {Event} options.event
     * @return {Promise<Response>}
     *
     * @private
     */
    _getZitiTimeoutPromise({ request, handler, }) {
        let timeoutId;
        let self = this;
        const timeoutPromise = new Promise((resolve) => {
            const onNetworkTimeout = async () => {
                self.logger.debug(`Timing out the network response at ${self._zitiNetworkTimeoutSeconds} seconds`);
                resolve(await handler.cacheMatch(request));
            };
            timeoutId = setTimeout(onNetworkTimeout, this._zitiNetworkTimeoutSeconds * 1000);
        });
        return {
            promise: timeoutPromise,
            id: timeoutId,
        };
    }
    /**
     *
     * @param zitiRequest
     * @returns body|undefined
     */
    async getRequestBody(zitiRequest) {
        var requestBlob = await zitiRequest.blob();
        if (requestBlob.size > 0) {
            return (requestBlob);
        }
        return (undefined);
    }
    /**
     *
     * @param headersObject
     */
    dumpHeaders(headersObject) {
        for (var pair of headersObject.entries()) {
            this.logger.trace('dumpHeaders: ', pair[0], pair[1]);
        }
    }
    /**
     *
     * @param headersObject
     */
    generateCSP(headersObject) {
        for (var pair of headersObject.entries()) {
            this.logger.trace('dumpHeaders: ', pair[0], pair[1]);
        }
    }
    /**
     * @param {Object} options
     * @param {number|undefined} options.timeoutId
     * @param {string} options.zitiURL
     * @param {Request} options.request
     * @param {Event} options.event
     * @return {Promise<Response>}
     *
     * @private
     */
    async _getZitiNetworkPromise({ timeoutId, shouldRoute, request, handler, useCache, }) {
        let error = null;
        let response;
        try {
            this.logger.debug(`doing Ziti fetch for: ${request.url}`);
            /**
             * Instantiate a fresh HTTP Request object that we will push through the ziti-browzer-core which will:
             *
             * 1) contain re-routed host
             * 2) have any headers we need to pile on
             * 3) prepare to stream out any body data associated with the intercepted request
             */
            const zitiRequest = new Request(request, {});
            var newHeaders = new Headers();
            zitiRequest.headers.forEach(function (header, key) {
                if (!isEqual(key, 'origin')) {
                    newHeaders.append(key, header);
                }
            });
            if (!isEqual(request.referrer, '')) {
                newHeaders.append('referer', request.referrer);
            }
            try {
                newHeaders.append('Remote-User', await this._zitiContext.getAccessTokenEmail());
            }
            catch (e) { }
            if (isEqual(request.mode, 'navigate')) {
                this.logger.trace(`Navigation event detected: [${request.url}]`);
                newHeaders.append('Sec-Fetch-Mode', 'navigate');
            }
            // Propagate any Cookie values we have accumulated
            let cookieHeaderValue = '';
            for (const cookie in this._zitiBrowzerServiceWorkerGlobalScope._cookieObject) {
                if (cookie !== '' && !isEqual(cookie, '__ziti-browzer-config')) { // don't send the ZBR config cookie
                    if (this._zitiBrowzerServiceWorkerGlobalScope._cookieObject.hasOwnProperty(cookie)) {
                        cookieHeaderValue += cookie + '=' + this._zitiBrowzerServiceWorkerGlobalScope._cookieObject[cookie];
                        // this.logger.debug(`cookieHeaderValue: [${cookieHeaderValue}]`);
                        newHeaders.append('Cookie', cookieHeaderValue);
                        cookieHeaderValue = '';
                    }
                }
            }
            var blob = await this.getRequestBody(zitiRequest);
            let referrerUrlPathname = '/';
            if (request.referrer) {
                let referrerUrl = new URL(request.referrer);
                referrerUrlPathname = referrerUrl.pathname;
            }
            var zitiResponse = await this._zitiContext.httpFetch(shouldRoute.url, {
                serviceName: shouldRoute.serviceName,
                serviceScheme: shouldRoute.serviceScheme,
                servicePath: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.path,
                serviceConnectAppData: shouldRoute.serviceConnectAppData,
                bootstrapperHost: this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host,
                method: zitiRequest.method,
                headers: newHeaders,
                mode: zitiRequest.mode,
                cache: zitiRequest.cache,
                credentials: zitiRequest.credentials,
                redirect: zitiRequest.redirect,
                referrerPolicy: zitiRequest.referrerPolicy,
                body: blob
            });
            this.logger.debug(`Got zitiResponse from [${zitiResponse.url}]`);
            /**
             * Now that ziti-browzer-core has returned us a ZitiResponse, instantiate a fresh native Response object that we
             * will return to the Browser. This requires us to:
             *
             * 1) propagate the HTTP headers, status, etc
             * 2) pipe the HTTP response body
             */
            if (isUndefined(zitiResponse.headers.raw)) {
                return zitiResponse;
            }
            var zitiHeaders = zitiResponse.headers.raw();
            const contentType = zitiHeaders['content-type'];
            let isTextHtml = false;
            let isTextXml = false;
            if (contentType && contentType[0] && contentType[0].match(regexTextHtml)) {
                isTextHtml = true;
            }
            if (contentType && contentType[0] && contentType[0].match(regexTextXml)) {
                isTextXml = true;
            }
            var headers = new Headers();
            const keys = Object.keys(zitiHeaders);
            for (let i = 0; i < keys.length; i++) {
                let key = keys[i];
                let val = zitiHeaders[key][0];
                this.logger.trace(`ZitiFirstStrategy: zitiResponse.headers: [${key}] [${val}]`);
                if (key.toLowerCase() === 'set-cookie') {
                    if (Array.isArray(val)) {
                        for (var ndx = 0; ndx < val.length; ndx++) {
                            this.logger.trace('ZitiFirstStrategy: sending SET_COOKIE cmd');
                            this._zitiBrowzerServiceWorkerGlobalScope._sendMessageToClients({
                                type: 'SET_COOKIE',
                                payload: val[ndx]
                            });
                            let parts = val[ndx].split('=');
                            function extractCookieVal(str) {
                                const match = str.match(/=(.*?);/);
                                return match ? match[1] : null;
                            }
                            const result = extractCookieVal(val[ndx]);
                            this._zitiBrowzerServiceWorkerGlobalScope._cookieObject[parts[0]] = result;
                        }
                    }
                    else {
                        headers.append('x-ziti-browzer-set-cookie', val);
                        this.logger.trace('ZitiFirstStrategy: sending SET_COOKIE cmd');
                        let resp = await this._zitiBrowzerServiceWorkerGlobalScope._sendMessageToClients({
                            type: 'SET_COOKIE',
                            payload: val
                        });
                        this.logger.trace('ZitiFirstStrategy: SET_COOKIE response: ', resp);
                        let parts = val.split('=');
                        this._zitiBrowzerServiceWorkerGlobalScope._cookieObject[parts[0]] = parts[1];
                    }
                }
                else if (key.toLowerCase() === 'location') {
                    this.logger.trace(`location header transform needed for: ${val}`);
                    function updateSignInWithGoogleRedirectUri(url, newValue, logger) {
                        let paramName = 'redirect_uri';
                        try {
                            let urlObj = new URL(url);
                            let searchParams = urlObj.searchParams;
                            // If url is doing a "Sign in with Google"
                            if (isEqual(urlObj.host, 'accounts.google.com') && isEqual(urlObj.pathname, '/o/oauth2/auth') && searchParams.has(paramName)) {
                                // Transform the redirect_uri host to be the browZer bootstrapper
                                let paramValue = searchParams.get(paramName);
                                if (paramValue) {
                                    let redirect_uriObj = new URL(paramValue);
                                    redirect_uriObj.host = newValue;
                                    redirect_uriObj.port = '443';
                                    redirect_uriObj.protocol = 'https';
                                    searchParams.set(paramName, redirect_uriObj.toString());
                                }
                            }
                            logger.trace(`updateSignInWithGoogleRedirectUri(): location header transformed to: ${urlObj.toString()}`);
                            return urlObj.toString();
                        }
                        catch (e) {
                            return url;
                        }
                    }
                    // NOT YET
                    // val = updateSignInWithGoogleRedirectUri(val, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host, this.logger);
                    let pathname;
                    let skipTransform = false;
                    if (val.startsWith('/')) {
                        pathname = val;
                    }
                    else {
                        function isValidUrl(string) {
                            try {
                                let url = new URL(string);
                                return url;
                            }
                            catch (err) {
                                return undefined;
                            }
                        }
                        let locationUrl = isValidUrl(val);
                        if (isUndefined(locationUrl)) { // i.e. it's a relative path (no slashes)
                            pathname = `${referrerUrlPathname}${val}`;
                        }
                        else {
                            if (isEqual(locationUrl.hostname, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host)) {
                                pathname = locationUrl.pathname + locationUrl.search;
                            }
                            else {
                                let serviceConnectAppData = await this._zitiBrowzerServiceWorkerGlobalScope._zitiContext.getConnectAppDataByServiceName(this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service, 'https');
                                if (!isUndefined(serviceConnectAppData)) {
                                    if (isEqual(locationUrl.hostname, serviceConnectAppData.dst_hostname)) {
                                        pathname = locationUrl.pathname + locationUrl.search;
                                    }
                                    else if (isEqual(locationUrl.hostname, serviceConnectAppData.dst_ip)) {
                                        pathname = locationUrl.pathname + locationUrl.search;
                                    }
                                }
                                else {
                                    skipTransform = true;
                                }
                            }
                        }
                    }
                    if (!isUndefined(pathname) && !skipTransform) {
                        let serviceScheme = shouldRoute.serviceScheme;
                        if (serviceScheme && !serviceScheme.endsWith(':')) {
                            serviceScheme = serviceScheme + ":";
                        }
                        let serviceName = shouldRoute.serviceName;
                        if (isEqual(serviceName, this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.target.service)) {
                            serviceName = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.bootstrapper.self.host;
                        }
                        let newLocationUrl = new URL(`${serviceScheme}//${serviceName}${pathname}`);
                        val = newLocationUrl.toString();
                        this.logger.trace(`location header transformed to: ${val}`);
                    }
                    else {
                        this.logger.trace(`location header transform bypassed for: ${val}`);
                    }
                }
                else if (key.toLowerCase() === 'content-security-policy') {
                    val = this.generateNewCSP(val);
                }
                headers.append(key, val);
            }
            headers.append('x-ziti-browzer-sw-workbox-strategies-version', pjson.version);
            if ((!isEqual(zitiResponse.status, 204)) && (zitiResponse.status < 300 || zitiResponse.status > 399)) {
                if (isTextHtml) {
                    var responseBlob = await zitiResponse.blob();
                    var responseBlobStream = responseBlob.stream();
                    const responseStream = new ReadableStream({
                        start(controller) {
                            function push() {
                                var chunk = responseBlobStream.read();
                                if (chunk) {
                                    controller.enqueue(chunk);
                                    push();
                                }
                                else {
                                    controller.close();
                                    return;
                                }
                            }
                            ;
                            push();
                        }
                    });
                    response = new Response(responseStream, { "status": zitiResponse.status, "headers": headers });
                }
                else if (isTextXml) {
                    var responseBlob = await zitiResponse.blob();
                    var responseBlobStream = responseBlob.stream();
                    const responseStream = new ReadableStream({
                        start(controller) {
                            function push() {
                                var chunk = responseBlobStream.read();
                                if (chunk) {
                                    controller.enqueue(chunk);
                                    push();
                                }
                                else {
                                    controller.close();
                                    return;
                                }
                            }
                            ;
                            push();
                        }
                    });
                    response = new Response(responseStream, { "status": zitiResponse.status, "headers": headers });
                }
                else {
                    const responseStream = new ReadableStream({
                        start(controller) {
                            function push(chunk) {
                                try {
                                    if (chunk) {
                                        controller.enqueue(chunk);
                                    }
                                    else {
                                        // if (!isClosed) {
                                        // isClosed = true;
                                        controller.close();
                                        // }
                                        return;
                                    }
                                }
                                catch (e) { }
                            }
                            ;
                            zitiResponse.body.on('data', (chunk) => {
                                push(chunk);
                            });
                            zitiResponse.body.on('end', () => {
                                push(null);
                            });
                        }
                    });
                    response = new Response(responseStream, { "status": zitiResponse.status, "headers": headers });
                }
            }
            else {
                response = new Response(null, { "status": zitiResponse.status, "headers": headers });
            }
            this.logger.trace(`ZitiFirstStrategy: formed native response for [${zitiResponse.url}]`);
        }
        catch (fetchError) {
            this.logger.error(`Got error: `, fetchError);
            if (fetchError instanceof Error) {
                error = fetchError;
            }
        }
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        if (response) {
            this.logger.debug(`Got response from Ziti network.`);
        }
        else {
            if (useCache) {
                this.logger.warn(`Unable to get a response from Ziti network. Will respond ` +
                    `with a cached response.`);
            }
        }
        if ((error || !response) && useCache) {
            response = await handler.cacheMatch(request);
            if (response) {
                this.logger.debug(`Found a cached response`);
            }
        }
        return response;
    }
    /**
     * @param {Object} options
     * @param {number|undefined} options.timeoutId
     * @param {Request} options.request
     * @param {Event} options.event
     * @return {Promise<Response>}
     *
     * @private
     */
    async _getNetworkPromise({ timeoutId, request, handler, useCache, }) {
        let error;
        let response;
        try {
            // if (useCache) {
            //   this.logger.debug(`doing raw internet fetchAndCachePut for: `, request.url);
            //   response = await handler.fetchAndCachePut(request);
            // } else {
            this.logger.debug(`doing raw internet fetch for [${request.url}]`);
            response = await handler.fetch(request);
            // }
            this.logger.debug(`Got raw internet response from [${request.url}]`);
        }
        catch (fetchError) {
            this.logger.error(`Got error: [${fetchError}]`);
            if (fetchError instanceof Error) {
                error = fetchError;
            }
        }
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        if (response) {
            this.logger.debug(`Got response from network.`);
            /**
             * If we get a failed respose that is also an 'opaqueredirect', it is most likely
             * because the attempt to fetch something served by the HTTP Agent is being canceled,
             * then redirected to the IdP (re)authentication URL.
             *
             * HACK ALERT:
             * For reaons currently unknown, if we do NOT unregister the SW, before letting
             * the OIDC middleware continue, it will fail...
             * So, we force an unregister of the SW here to keep the (re)authentication flow working.
             */
            if (!response.ok && (response.type === 'opaqueredirect')) {
                this.logger.debug(`Got 'opaqueredirect' response from network; doing SW unregister now`);
                await this._zitiBrowzerServiceWorkerGlobalScope._unregister();
            }
        }
        else {
            if (useCache) {
                this.logger.warn(`Unable to get a response from the network. Will respond ` +
                    `with a cached response.`);
            }
        }
        if ((error || !response) && useCache) {
            response = await handler.cacheMatch(request);
            if (response) {
                this.logger.debug(`Found a cached response in the '${this.cacheName}'` + ` cache.`);
            }
        }
        return response;
    }
}
export { ZitiFirstStrategy };