diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-07-08 19:58:06 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-07-08 19:58:06 -0400 | 
| commit | 6f49f426b518bdbca11c7994246eb088903e6619 (patch) | |
| tree | 2ed7618c5eb34de3a90a826c0fac2da2fb72cbd7 /ext/mixed/js | |
| parent | 295ffa6e54d04cedef35a4798cabdae71f824ee1 (diff) | |
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
Diffstat (limited to 'ext/mixed/js')
| -rw-r--r-- | ext/mixed/js/frame-client.js | 173 | ||||
| -rw-r--r-- | ext/mixed/js/frame-endpoint.js | 65 | 
2 files changed, 238 insertions, 0 deletions
| 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 <https://www.gnu.org/licenses/>. + */ + +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 <https://www.gnu.org/licenses/>. + */ + +/* 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}); +    } +} |