From 6f49f426b518bdbca11c7994246eb088903e6619 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 8 Jul 2020 19:58:06 -0400 Subject: Generalized frame connections (#654) * Create FrameClient and FrameEndpoint * Use new Frame* classes for Popup=>frame connection * Update api.sendMessageToFrame and api.broadcastTab to include the sender's frameId * Update FrameClient to store the frame's frameId --- ext/mixed/js/frame-client.js | 173 +++++++++++++++++++++++++++++++++++++++++ ext/mixed/js/frame-endpoint.js | 65 ++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 ext/mixed/js/frame-client.js create mode 100644 ext/mixed/js/frame-endpoint.js (limited to 'ext/mixed/js') diff --git a/ext/mixed/js/frame-client.js b/ext/mixed/js/frame-client.js new file mode 100644 index 00000000..6ea344e2 --- /dev/null +++ b/ext/mixed/js/frame-client.js @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +class FrameClient { + constructor() { + this._secret = null; + this._token = null; + this._frameId = null; + } + + get frameId() { + return this._frameId; + } + + async connect(frame, targetOrigin, hostFrameId, setupFrame, timeout=10000) { + const {secret, token, frameId} = await this._connectIternal(frame, targetOrigin, hostFrameId, setupFrame, timeout); + this._secret = secret; + this._token = token; + this._frameId = frameId; + } + + isConnected() { + return (this._secret !== null); + } + + createMessage(data) { + if (!this.isConnected()) { + throw new Error('Not connected'); + } + return { + token: this._token, + secret: this._secret, + data + }; + } + + _connectIternal(frame, targetOrigin, hostFrameId, setupFrame, timeout) { + return new Promise((resolve, reject) => { + const tokenMap = new Map(); + let timer = null; + let { + promise: frameLoadedPromise, + resolve: frameLoadedResolve, + reject: frameLoadedReject + } = deferPromise(); + + const postMessage = (action, params) => { + const contentWindow = frame.contentWindow; + if (contentWindow === null) { throw new Error('Frame missing content window'); } + + let validOrigin = true; + try { + validOrigin = (contentWindow.location.origin === targetOrigin); + } catch (e) { + // NOP + } + if (!validOrigin) { throw new Error('Unexpected frame origin'); } + + contentWindow.postMessage({action, params}, targetOrigin); + }; + + const onMessage = (message) => { + onMessageInner(message); + return false; + }; + + const onMessageInner = async (message) => { + try { + if (!isObject(message)) { return; } + const {action, params} = message; + if (!isObject(params)) { return; } + await frameLoadedPromise; + if (timer === null) { return; } // Done + + switch (action) { + case 'frameEndpointReady': + { + const {secret} = params; + const token = yomichan.generateId(16); + tokenMap.set(secret, token); + postMessage('frameEndpointConnect', {secret, token, hostFrameId}); + } + break; + case 'frameEndpointConnected': + { + const {secret, token} = params; + const frameId = message.frameId; + const token2 = tokenMap.get(secret); + if (typeof token2 !== 'undefined' && token === token2) { + cleanup(); + resolve({secret, token, frameId}); + } + } + break; + } + } catch (e) { + cleanup(); + reject(e); + } + }; + + const onLoad = () => { + if (frameLoadedResolve === null) { + cleanup(); + reject(new Error('Unexpected load event')); + return; + } + + if (FrameClient.isFrameAboutBlank(frame)) { + return; + } + + frameLoadedResolve(); + frameLoadedResolve = null; + frameLoadedReject = null; + }; + + const cleanup = () => { + if (timer === null) { return; } // Done + clearTimeout(timer); + timer = null; + + frameLoadedResolve = null; + if (frameLoadedReject !== null) { + frameLoadedReject(new Error('Terminated')); + frameLoadedReject = null; + } + + chrome.runtime.onMessage.removeListener(onMessage); + frame.removeEventListener('load', onLoad); + }; + + // Start + timer = setTimeout(() => { + cleanup(); + reject(new Error('Timeout')); + }, timeout); + + chrome.runtime.onMessage.addListener(onMessage); + frame.addEventListener('load', onLoad); + + // Prevent unhandled rejections + frameLoadedPromise.catch(() => {}); // NOP + + setupFrame(frame); + }); + } + + static isFrameAboutBlank(frame) { + try { + const contentDocument = frame.contentDocument; + if (contentDocument === null) { return false; } + const url = contentDocument.location.href; + return /^about:blank(?:[#?]|$)/.test(url); + } catch (e) { + return false; + } + } +} diff --git a/ext/mixed/js/frame-endpoint.js b/ext/mixed/js/frame-endpoint.js new file mode 100644 index 00000000..1cd25bb5 --- /dev/null +++ b/ext/mixed/js/frame-endpoint.js @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * api + */ + +class FrameEndpoint { + constructor() { + this._secret = yomichan.generateId(16); + this._token = null; + this._eventListeners = new EventListenerCollection(); + this._eventListenersSetup = false; + } + + signal() { + if (!this._eventListenersSetup) { + this._eventListeners.addEventListener(window, 'message', this._onMessage.bind(this), false); + this._eventListenersSetup = true; + } + api.broadcastTab('frameEndpointReady', {secret: this._secret}); + } + + authenticate(message) { + return ( + this._token !== null && + isObject(message) && + this._token === message.token && + this._secret === message.secret + ); + } + + _onMessage(e) { + if (this._token !== null) { return; } // Already initialized + + const data = e.data; + if (!isObject(data) || data.action !== 'frameEndpointConnect') { return; } // Invalid message + + const params = data.params; + if (!isObject(params)) { return; } // Invalid data + + const secret = params.secret; + if (secret !== this._secret) { return; } // Invalid authentication + + const {token, hostFrameId} = params; + this._token = token; + + this._eventListeners.removeAllEventListeners(); + api.sendMessageToFrame(hostFrameId, 'frameEndpointConnected', {secret, token}); + } +} -- cgit v1.2.3