From 45b4c69b107cb37b093577532c3854b30343ecbc Mon Sep 17 00:00:00 2001 From: Curt Tudor <curt@rentallect.com> Date: Mon, 5 Feb 2024 10:16:19 -0700 Subject: [PATCH] feat: introduce ZitiDummyWebSocketWrapper (#258) --- package.json | 4 +- rollup.config.js | 2 + src/http/ziti-dummy-websocket-wrapper.js | 143 +++++++++++++++++++++++ src/http/ziti-xhr.js | 19 +++ src/runtime.js | 89 ++++++++------ yarn.lock | 55 ++++++++- 6 files changed, 273 insertions(+), 39 deletions(-) create mode 100644 src/http/ziti-dummy-websocket-wrapper.js diff --git a/package.json b/package.json index 70b5855..1ed3692 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "rollup-plugin-babel": "^4.4.0", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-esformatter": "^2.0.1", + "rollup-plugin-polyfill-node": "^0.13.0", "rollup-plugin-prettier": "^2.2.2", "rollup-plugin-terser": "^7.0.2", "tinyify": "^3.0.0" @@ -94,10 +95,11 @@ "@auth0/auth0-spa-js": "^2.0.4", "@azure/msal-browser": "^2.38.0", "@babel/runtime": "^7.17.9", - "@openziti/ziti-browzer-core": "^0.36.1", + "@openziti/ziti-browzer-core": "^0.37.0", "bowser": "^2.11.0", "cookie-interceptor": "^1.0.0", "core-js": "^3.22.8", + "events": "^3.3.0", "js-base64": "^3.7.2", "jwt-decode": "^3.1.2", "localforage": "^1.10.0", diff --git a/rollup.config.js b/rollup.config.js index e537f84..cb26a81 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,6 +5,7 @@ import json from '@rollup/plugin-json'; // import commonjs from '@rollup/plugin-commonjs'; import prettier from 'rollup-plugin-prettier'; import copy from 'rollup-plugin-copy'; +import nodePolyfills from 'rollup-plugin-polyfill-node'; const SRC_DIR = 'src'; @@ -33,6 +34,7 @@ let plugins = [ } ] }), + nodePolyfills(), nodeResolve(), ]; diff --git a/src/http/ziti-dummy-websocket-wrapper.js b/src/http/ziti-dummy-websocket-wrapper.js new file mode 100644 index 0000000..436af42 --- /dev/null +++ b/src/http/ziti-dummy-websocket-wrapper.js @@ -0,0 +1,143 @@ +/* +Copyright NetFoundry, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EventEmitter from 'events'; +import { isEqual, isUndefined } from 'lodash-es'; + + +/** + * ZitiDummyWebSocketWrapper: + * + */ +class ZitiDummyWebSocketWrapper extends EventEmitter { + + /** + * Create a new `ZitiDummyWebSocketWrapper`. + * + * @param {(String|url.URL)} address The URL to which to connect + */ + constructor(address) { + + super(); + + this.address = address; + + setTimeout(async function(self) { + + await window.zitiBrowzerRuntime.awaitInitializationComplete(); + + let serviceName; + var url = new URL( self.address ); + + if (isEqual(url.hostname, zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host)) { // if targeting the bootstrapper + serviceName = zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.target.service; + } else { + serviceName = await zitiBrowzerRuntime.zitiContext.shouldRouteOverZiti( self.address ); + } + + if (isUndefined(serviceName)) { // If we have no serviceConfig associated with the address, do not intercept + + self.innerWebSocket = new window._ziti_realWebSocket(self.address); + + } else { + + let opts = {} + + opts.serviceName = serviceName; + opts.configHostAndPort = await zitiBrowzerRuntime.zitiContext.getConfigHostAndPortByServiceName (serviceName); + + self.innerWebSocket = new zitiBrowzerRuntime.zitiContext.zitiWebSocketWrapper(self.address, undefined, opts); + + } + + self.innerWebSocket.addEventListener('open', (event) => { + if (self.onopen) { + self.onopen(event); + } + self.emit(event); + }); + + self.innerWebSocket.addEventListener('close', (event) => { + if (self.onclose) { + self.onclose(event); + } + self.emit(event); + }); + + self.innerWebSocket.addEventListener('error', (event) => { + if (self.onerror) { + self.onerror(event); + } + self.emit(event); + }); + + self.innerWebSocket.addEventListener('message', (event) => { + if (self.onmessage) { + self.onmessage(event); + } + self.emit(event); + }); + + }, 1, this); + + } + + /** + * Remain in lazy-sleepy loop until inner WebSocket is present. + * + */ + awaitInnerWebSocketPresent() { + let self = this; + return new Promise((resolve) => { + (function waitForInnerWebSocketPresent() { + if (!self.innerWebSocket) { + setTimeout(waitForInnerWebSocketPresent, 5); + } else { + if (self.innerWebSocket.READYSTATE !== self.innerWebSocket.OPEN) { + setTimeout(waitForInnerWebSocketPresent, 5); + } else { + return resolve(); + } + } + })(); + }); + } + + /** + * + * @param {*} code + * @param {*} data + */ + async close(code, data) { + await this.awaitInnerWebSocketPresent(); + this.innerWebSocket.close(code, data); + } + + /** + * + * @param {*} data + */ + async send(data) { + await this.awaitInnerWebSocketPresent(); + this.innerWebSocket.send(data); + } + +} + + +export { + ZitiDummyWebSocketWrapper +}; diff --git a/src/http/ziti-xhr.js b/src/http/ziti-xhr.js index f98dff3..e6b9f7b 100644 --- a/src/http/ziti-xhr.js +++ b/src/http/ziti-xhr.js @@ -274,6 +274,25 @@ function ZitiXMLHttpRequest () { settings.headers = headers; + await window.zitiBrowzerRuntime.awaitInitializationComplete(); + + let url; + let targetHost; + if (!settings.url.startsWith('/')) { + url = new URL(settings.url); + targetHost = url.hostname; + } else { + url = new URL(`https://${window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host}${settings.url}`); + targetHost = window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host; + } + if (isEqual(targetHost, window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host)) { + let protocol = url.protocol; + if (!isEqual(protocol, 'https:')) { + url.protocol = 'https:'; + settings.url = url.toString(); + } + } + response = await fetch(settings.url, settings); this.status = response.status; diff --git a/src/runtime.js b/src/runtime.js index 0aa574f..f10a024 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -31,6 +31,7 @@ import { flatOptions } from './utils/flat-options' import { defaultOptions } from './options' import { ZBR_CONSTANTS } from './constants'; import { ZitiXMLHttpRequest } from './http/ziti-xhr'; +import { ZitiDummyWebSocketWrapper } from './http/ziti-dummy-websocket-wrapper'; import { buildInfo } from './buildInfo' import { ZitiBrowzerLocalStorage } from './utils/localstorage'; import { Auth0Client } from '@auth0/auth0-spa-js'; @@ -237,15 +238,21 @@ class ZitiBrowzerRuntime { return cookie; }); - // Toast infra - this.PolipopCreated = false; - setTimeout(this._createPolipop, 1000, this); + const loadedViaBootstrapper = document.getElementById('from-ziti-browzer-bootstrapper'); + + if (!loadedViaBootstrapper) { - // HotKey infra - setTimeout(this._createHotKey, 5000, this); + // Toast infra + this.PolipopCreated = false; + setTimeout(this._createPolipop, 1000, this); - // Click intercept infra - setTimeout(this._createClickIntercept, 3000, this); + // HotKey infra + setTimeout(this._createHotKey, 5000, this); + + // Click intercept infra + setTimeout(this._createClickIntercept, 3000, this); + + } this.authClient = null; this.idp = null; @@ -410,7 +417,7 @@ class ZitiBrowzerRuntime { _createPolipop(self) { - if (!this.PolipopCreated) { + if (!self.PolipopCreated) { try { if (document.body && (typeof Polipop !== 'undefined')) { self.polipop = new Polipop('ziti-browzer-toast', { @@ -427,17 +434,17 @@ class ZitiBrowzerRuntime { life: 3000, icons: true, }); - this.PolipopCreated = true; - self.logger.debug(`_createPolipop: Polipop bootstrap completed`); + self.PolipopCreated = true; + self.logger?.debug(`_createPolipop: Polipop bootstrap completed`); } else { - self.logger.debug(`_createPolipop: awaiting Polipop bootstrap`); - setTimeout(this._createPolipop, 100, this); + self.logger?.debug(`_createPolipop: awaiting Polipop bootstrap`); + setTimeout(self._createPolipop, 100, self); } } catch (e) { - self.logger.error(`_createPolipop: bootstrap error`, e); - setTimeout(this._createPolipop, 1000, this); + self.logger?.error(`_createPolipop: bootstrap error`, e); + setTimeout(self._createPolipop, 1000, self); } } } @@ -1609,9 +1616,9 @@ class ZitiBrowzerRuntime { /** * Logic devoted to acquiring an access_token from the IdP runs _ONLY_ - * when this ZBR has been loaded from the BrowZer Gateway (not the ZBSW). + * when this ZBR has been loaded from the BrowZer Bootstrapper (not the ZBSW). * - * If we were loaded via the ZBSW, then the the access_token we + * If we were loaded via the ZBSW, then the access_token we * need should be in a cookie, and we will obtain it from there instead of * interacting with the IdP because doing so will lead to a never ending loop. */ @@ -1758,8 +1765,6 @@ class ZitiBrowzerRuntime { }); this.logger.trace(`ZitiContext created`); - window.WebSocket = zitiBrowzerRuntime.zitiContext.zitiWebSocketWrapper; - this.zbrSWM = new ZitiBrowzerRuntimeServiceWorkerMock(); navigator.serviceWorker._ziti_realRegister = navigator.serviceWorker.register; @@ -1913,6 +1918,8 @@ if (isUndefined(window.zitiBrowzerRuntime)) { window.zitiBrowzerRuntime._serviceWorkerKeepAliveHeartBeat(window.zitiBrowzerRuntime); + window.zitiBrowzerRuntime.loadedViaBootstrapper = document.getElementById('from-ziti-browzer-bootstrapper'); + /** * Use an async IIFE to initialize the runtime and register the SW. */ @@ -1984,11 +1991,6 @@ if (isUndefined(window.zitiBrowzerRuntime)) { */ await window.zitiBrowzerRuntime.zitiContext.enroll(); - /** - * - */ - window.WebSocket = zitiBrowzerRuntime.zitiContext.zitiWebSocketWrapper; - } window.addEventListener('online', (e) => { @@ -2475,6 +2477,7 @@ if (isUndefined(window.zitiBrowzerRuntime)) { var regex = new RegExp( `${window.zitiBrowzerRuntime._obtainBootStrapperURL()}`, 'gi' ); var regexSlash = new RegExp( /^\//, 'g' ); var regexDotSlash = new RegExp( /^\.\//, 'g' ); +var regexZBR = new RegExp( /ziti-browzer-runtime-\w{8}\.js/, 'g' ); var regexZBWASM = new RegExp( /libcrypto.*.wasm/, 'g' ); @@ -2489,27 +2492,45 @@ var regexZBWASM = new RegExp( /libcrypto.*.wasm/, 'g' ); const zitiFetch = async ( urlObj, opts ) => { - if (!window.zitiBrowzerRuntime.isAuthenticated) { - return window._ziti_realFetch(urlObj, opts); - } - let url; - if (urlObj instanceof Request) { url = urlObj.url; + } else { + url = urlObj; + } - for (var pair of urlObj.headers.entries()) { - console.log(`${pair[0]} : ${pair[1]}`); - } + if (url.match( regexZBR ) || url.match( regexZBWASM )) { // the request seeks z-b-r/wasm + return window._ziti_realFetch(urlObj, opts); + } + + if (!window.zitiBrowzerRuntime.loadedViaBootstrapper) { + await window.zitiBrowzerRuntime.awaitInitializationComplete(); + } + if (!window.zitiBrowzerRuntime.isAuthenticated) { + return window._ziti_realFetch(urlObj, opts); + } + + let targetHost; + if (!url.startsWith('/')) { + url = new URL(url); + targetHost = url.hostname; } else { - url = urlObj; + url = new URL(`https://${window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host}${url}`); + targetHost = window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host; + } + if (isEqual(targetHost, window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host)) { + let protocol = url.protocol; + if (!isEqual(protocol, 'https:')) { + url.protocol = 'https:'; + url = url.toString(); + } } window.zitiBrowzerRuntime.logger.trace( 'zitiFetch: entered for URL: ', url); //TEMP TEST... always attempt to go thru SW - return window._ziti_realFetch(urlObj, opts); + return window._ziti_realFetch(url, opts); if (url.match( regexZBWASM )) { // the request seeks z-b-r/wasm window.zitiBrowzerRuntime.logger.trace('zitiFetch: seeking Ziti z-b-r/wasm, bypassing intercept of [%s]', url); @@ -2729,4 +2750,4 @@ const zitiDocumentDomain = ( arg ) => { window.fetch = zitiFetch; window.XMLHttpRequest = ZitiXMLHttpRequest; window.document.zitidomain = zitiDocumentDomain; - +window.WebSocket = ZitiDummyWebSocketWrapper; diff --git a/yarn.lock b/yarn.lock index 2471665..03c4056 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1145,6 +1145,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" @@ -1195,10 +1200,10 @@ "@types/emscripten" "^1.39.6" "@wasmer/wasi" "^1.0.2" -"@openziti/ziti-browzer-core@^0.36.1": - version "0.36.1" - resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.36.1.tgz#fee5c55a47f462d0dab9fbd2c7b19755cdb75109" - integrity sha512-b+7jqEhbAvEOYCl+v12+q3CfwAt30mvKpbeNqM6y5c1nHVyDHcnrKKVjK9Xvt4JV9ZobMjIn34XuDuwQDT5Cyw== +"@openziti/ziti-browzer-core@^0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.37.0.tgz#49d3984ffcecc5f746cbf3e27d93e7a9cdc208fc" + integrity sha512-TL04Jxplv2VusklbbeHh2xiM4XiCbPGrvS/3Cs+sqwdmVyV9+z+GVLYE9Bv4V2ofMeq2V2CFvQbrRL06qC8a/w== dependencies: "@openziti/libcrypto-js" "^0.19.0" "@openziti/ziti-browzer-edge-client" "^0.6.2" @@ -1246,6 +1251,15 @@ dependencies: superagent "^7.1.3" +"@rollup/plugin-inject@^5.0.4": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz#616f3a73fe075765f91c5bec90176608bed277a3" + integrity sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + estree-walker "^2.0.2" + magic-string "^0.30.3" + "@rollup/plugin-json@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" @@ -1274,6 +1288,15 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^5.0.1": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" + integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + "@types/emscripten@^1.39.6": version "1.39.6" resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.39.6.tgz#698b90fe60d44acf93c31064218fbea93fbfd85a" @@ -1284,6 +1307,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/fs-extra@^8.0.1": version "8.1.2" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" @@ -2862,6 +2890,11 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3934,6 +3967,13 @@ magic-string@^0.23.2: dependencies: sourcemap-codec "^1.4.1" +magic-string@^0.30.3: + version "0.30.6" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.6.tgz#996e21b42f944e45591a68f0905d6a740a12506c" + integrity sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -4940,6 +4980,13 @@ rollup-plugin-esformatter@^2.0.1: lodash.omitby "4.6.0" magic-string "0.25.7" +rollup-plugin-polyfill-node@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.13.0.tgz#28e5705b59438da894e55133a0fe7a86b57d9b0a" + integrity sha512-FYEvpCaD5jGtyBuBFcQImEGmTxDTPbiHjJdrYIp+mFIwgXiXabxvKUK7ZT9P31ozu2Tqm9llYQMRWsfvTMTAOw== + dependencies: + "@rollup/plugin-inject" "^5.0.4" + rollup-plugin-prettier@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/rollup-plugin-prettier/-/rollup-plugin-prettier-2.2.2.tgz#733c25a3cea2ce65b14635729bd1eb3a2a147ebc"