From 3d06b52ffbd9c35389e00de85763558150e25c68 Mon Sep 17 00:00:00 2001 From: Curt Tudor Date: Tue, 21 Jun 2022 15:36:15 -0400 Subject: [PATCH] fix: use httpAgent.target.service (#60) --- package.json | 4 +- src/ZitiFirstStrategy.ts | 173 +++++++++++++++++++++++++++++++++++---- tsconfig.json | 1 + yarn.lock | 10 +-- 4 files changed, 164 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index dc6d235..b8227b7 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ ] }, "dependencies": { - "@openziti/ziti-browzer-core": "^0.10.0", + "@openziti/ziti-browzer-core": "^0.10.2", "async-mutex": "^0.3.2", "jwt-decode": "^3.1.2", "lodash-es": "^4.17.21", @@ -92,4 +92,4 @@ "workbox-expiration": "^6.5.3", "workbox-strategies": "^6.5.3" } -} \ No newline at end of file +} diff --git a/src/ZitiFirstStrategy.ts b/src/ZitiFirstStrategy.ts index 3b1eae1..48378db 100644 --- a/src/ZitiFirstStrategy.ts +++ b/src/ZitiFirstStrategy.ts @@ -1,7 +1,6 @@ import {WorkboxError} from 'workbox-core/_private/WorkboxError.js'; import {StrategyHandler} from 'workbox-strategies/StrategyHandler.js'; -import {NetworkFirst} from 'workbox-strategies/NetworkFirst.js'; import {CacheFirst} from 'workbox-strategies/CacheFirst.js'; import {StrategyOptions} from 'workbox-strategies/Strategy.js'; import {Mutex} from 'async-mutex'; @@ -59,6 +58,7 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { private _zitiContext: any; private _initialized: boolean; private _initializationMutex: any; + private _fetchMutex: any; private _uuid: any; /** @@ -82,6 +82,7 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { regexControllerAPI = new RegExp( this._controllerApi, 'g' ); this._initializationMutex = new Mutex(); + this._fetchMutex = new Mutex(); this._uuid = options.uuid; this._core = new ZitiBrowzerCore({}); @@ -93,10 +94,10 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { } -/** - * Remain in lazy-sleepy loop until z-b-runtime sends us the _zitiConfig - * - */ + /** + * Remain in lazy-sleepy loop until z-b-runtime sends us the _zitiConfig + * + */ async await_zitiConfig() { let self = this; let ctr = 0; @@ -113,7 +114,7 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { } /** - * Do all work necessry to initialize the ZitiFirstStrategy instance. + * Do all work necessary to initialize the ZitiFirstStrategy instance. * */ async _initialize() { @@ -189,6 +190,7 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { // which the app was loaded. var regex = new RegExp( this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.self.host, 'g' ); + var targetServiceRegex = new RegExp( this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.target.service, 'g' ); if (request.url.match( regex )) { // yes, the request is targeting the Ziti HTTP Agent @@ -196,18 +198,22 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { var newUrl = new URL( request.url ); - // If seeking root page, do NOT route over Ziti. Instead, we if ( newUrl.pathname === '/' ) { - this.logger.trace('_shouldRouteOverZiti: root path, bypassing intercept of [%s]: ', request.url); + // this.logger.trace('_shouldRouteOverZiti: root path, bypassing intercept of [%s]: ', request.url); + + result.url = request.url; + result.routeOverZiti = true; + result.serviceName = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.target.service; } - else if ( (request.url.match( regexZBR )) || ((request.url.match( regexZBWASM ))) ) { // the request seeks z-b-r/wasm + else + if ( (request.url.match( regexZBR )) || ((request.url.match( regexZBWASM ))) ) { // the request seeks z-b-r/wasm this.logger.trace('_shouldRouteOverZiti: z-b-r/wasm, bypassing intercept of [%s]: ', request.url); } else { - newUrl.hostname = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.target.host; + newUrl.hostname = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.target.service; newUrl.port = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.target.port; this.logger.trace( '_shouldRouteOverZiti: transformed URL: ', newUrl.toString()); @@ -215,7 +221,7 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { this.logger.trace(`_shouldRouteOverZiti result.serviceName[%o]`, result.serviceName); if (isEqual(result.serviceName, '')) { // If we have no config associated with the hostname:port, do not intercept - this.logger.warn('_shouldRouteOverZiti: no associated config, bypassing intercept of [%s]', request.url); + this.logger.warn('_shouldRouteOverZiti: no associated Ziti config, bypassing intercept of [%s]', request.url); } else { result.url = newUrl.toString(); result.routeOverZiti = true; @@ -227,6 +233,14 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { else if ( (request.url.match( regexZBR )) || ((request.url.match( regexZBWASM ))) ) { // the request seeks z-b-r/wasm this.logger.trace('_shouldRouteOverZiti: z-b-r/wasm, bypassing intercept of [%s]: ', request.url); } + else if (request.url.match( targetServiceRegex )) { // yes, the request is targeting the 'dark' web app + + result.url = request.url; + result.routeOverZiti = true; + result.serviceName = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.target.service; + + } + else if ( (request.url.match( regexSlash )) || ((request.url.match( regexDotSlash ))) ) { // the request starts with a slash this.logger.error(`_shouldRouteOverZiti implement me `); @@ -268,6 +282,9 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { this.logger.trace(`_handle entered for: `, request.url); + // const release = await this._fetchMutex.acquire(); + // try { + let url = new URL(request.url); let useCache = true; @@ -301,7 +318,7 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { // If hitting the root page, or Controller, or seeking z-b-runtime/WASM, then // we never go over Ziti, and we let the browser route the request to the HTTP Agent. if ( - (newUrl.pathname === '/' ) || // seeking root page + // (newUrl.pathname === '/' ) || // seeking root page (request.url.match( regexControllerAPI )) || // seeking Ziti Controller (request.url.match( regexZBR )) || // seeking Ziti BrowZer Runtime (request.url.match( regexZBWASM )) // seeking Ziti BrowZer WASM @@ -327,8 +344,8 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { promises.push(promise); } - let networkPromise = null; - let zitiNetworkPromise = null; + let networkPromise; + let zitiNetworkPromise; if (!shouldRoute.routeOverZiti) { @@ -387,7 +404,130 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { await handler.waitUntil(handler.cachePut(request, response.clone())); } + if (url.pathname.match( regexSlash )) { // seeking root page of webapp + + this.logger.trace('we need to inject z-b-r onto this response ', response); + + var ignore = false; + + var lowercaseUrl = request.url.toLowerCase(); + + if ((lowercaseUrl.indexOf('.js', request.url.length - 3) !== -1) || + (lowercaseUrl.indexOf('.css', request.url.length - 4) !== -1)) { + ignore = true; + } + + if (!ignore) { + + let zbrLocation = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.browzer.runtime.src; + let httpAgentLocation = this._zitiBrowzerServiceWorkerGlobalScope._zitiConfig.httpAgent.self.host; + + + // Inject the Ziti browZer Runtime at the front of element so we are prepared to intercept as soon as possible over on the browser + let zbr_inject_html = ` + + +`; + + let reH = new RegExp('','gi'); + let replace = `${zbr_inject_html}`; + + // let reCSP = new RegExp('^.*${zbr_inject_html}`; + + let buffer = ''; + + return new TransformStream({ + transform(chunk, controller) { + buffer += chunk; + let outChunk = ''; + + while (true) { + const index = buffer.indexOf(find); + if (index === -1) break; + outChunk += buffer.slice(0, index) + replace; + buffer = buffer.slice(index + find.length); + } + + outChunk += buffer.slice(0, -(find.length - 1)); + buffer = buffer.slice(-(find.length - 1)); + controller.enqueue(outChunk); + }, + flush(controller) { + if (buffer) controller.enqueue(buffer); + } + }) + } + + function streamingCSPReplace() { + let find = 'js.stripe.com/v3'; // TEMP + let replace = `${find} 'unsafe-eval' 'wasm-eval';`; + + let buffer = ''; + + return new TransformStream({ + transform(chunk, controller) { + buffer += chunk; + let outChunk = ''; + + while (true) { + const index = buffer.indexOf(find); + if (index === -1) break; + outChunk += buffer.slice(0, index) + replace; + buffer = buffer.slice(index + find.length); + } + + outChunk += buffer.slice(0, -(find.length - 1)); + buffer = buffer.slice(-(find.length - 1)); + controller.enqueue(outChunk); + }, + flush(controller) { + if (buffer) controller.enqueue(buffer); + } + }) + } + + const bodyStream = response.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(streamingHEADReplace()) + .pipeThrough(streamingCSPReplace()) + .pipeThrough(new TextEncoderStream()); + + const newHeaders = new Headers(response.headers); + // let cspheader = newHeaders.get('Content-Security-Policy'); + // cspheader = cspheader + " * " + httpAgentLocation + " 'unsafe-inline' 'unsafe-eval' 'wasm-eval' blob:; worker-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-eval' blob:;" + // this.logger.trace('cspheader ', cspheader); + // newHeaders.set('Content-Security-Policy', "script-src 'self' " + httpAgentLocation + " 'unsafe-inline' 'unsafe-eval' 'wasm-eval' blob:; worker-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-eval' blob:;"); + newHeaders.delete('Content-Security-Policy'); + + // let newResponse = new Response(bodyStream, response); + const newResponse = new Response(bodyStream, { + status: response.status, + statusText: response.statusText, + headers: newHeaders + }); + + this.logger.trace('we injected z-b-r onto this (new) response ', newResponse); + + return newResponse; + } + + } + + } + return response; + + // } finally { + // release(); + // } + } /** @@ -521,7 +661,6 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { newHeaders.append( 'Cookie', cookieHeaderValue ); } - this.logger.debug(`cookieHeaderValue is: `, cookieHeaderValue); var blob = await this.getRequestBody( zitiRequest ); @@ -594,11 +733,11 @@ class ZitiFirstStrategy extends CacheFirst /* NetworkFirst */ { } if (response) { - this.logger.debug(`Got response from network.`); + this.logger.debug(`Got response from Ziti network.`); } else { if (useCache) { this.logger.warn( - `Unable to get a response from the network. Will respond ` + + `Unable to get a response from Ziti network. Will respond ` + `with a cached response.`, ); } diff --git a/tsconfig.json b/tsconfig.json index a5e73a9..ddf9614 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, + "noImplicitAny": false, "noUnusedLocals": false, "allowSyntheticDefaultImports": true, "noUnusedParameters": true, diff --git a/yarn.lock b/yarn.lock index d0f232c..d5c4f81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -969,10 +969,10 @@ "@types/emscripten" "^1.39.6" "@wasmer/wasi" "^1.0.2" -"@openziti/ziti-browzer-core@^0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.10.0.tgz#3d6530ac3a26dab0799a07be35a979d1f2877ee3" - integrity sha512-JUXdo/DfVgtNY/aF9oe1hjl7rc1lvz4jxeHfICmHAxzpP2YcM52eNXABxBfRGOckEuNlo/YgTK5pr1E1w3CePA== +"@openziti/ziti-browzer-core@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.10.2.tgz#4c1fa330e7f9d6fca17b106cb66db76cee4bee81" + integrity sha512-EH1OLk+VtJ95dVGgtLKKmTeLD0EVYUm07TgNU5m9QGJ8FFm8jtbNZk+bMp0WSwcwxmhrFQNLD85QUuEhZZecPA== dependencies: "@openziti/libcrypto-js" "^0.9.0" "@openziti/ziti-browzer-edge-client" "^0.2.0" @@ -4254,7 +4254,7 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== promise-controller@^1.0.0: version "1.0.0"