// Copyright (C) 2019 The Android Open Source Project // // 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 // // http://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 * as m from 'mithril'; import {Actions, PostedTrace} from '../common/actions'; import {globals} from './globals'; import {showModal} from './modal'; interface PostedTraceWrapped { perfetto: PostedTrace; } // Returns whether incoming traces should be opened automatically or should // instead require a user interaction. function isTrustedOrigin(origin: string): boolean { const TRUSTED_ORIGINS = [ 'https://chrometto.googleplex.com', 'https://uma.googleplex.com', ]; if (TRUSTED_ORIGINS.includes(origin)) return true; if (new URL(origin).hostname.endsWith('corp.google.com')) return true; return false; } // The message handler supports loading traces from an ArrayBuffer. // There is no other requirement than sending the ArrayBuffer as the |data| // property. However, since this will happen across different origins, it is not // possible for the source website to inspect whether the message handler is // ready, so the message handler always replies to a 'PING' message with 'PONG', // which indicates it is ready to receive a trace. export function postMessageHandler(messageEvent: MessageEvent) { if (messageEvent.origin === 'https://tagassistant.google.com') { // The GA debugger, does a window.open() and sends messages to the GA // script. Ignore them. return; } if (document.readyState !== 'complete') { console.error('Ignoring message - document not ready yet.'); return; } if (messageEvent.source === null || messageEvent.source !== window.opener) { // This can happen if an extension tries to postMessage. return; } if (!('data' in messageEvent)) { throw new Error('Incoming message has no data property'); } if (messageEvent.data === 'PING') { // Cross-origin messaging means we can't read |messageEvent.source|, but // it still needs to be of the correct type to be able to invoke the // correct version of postMessage(...). const windowSource = messageEvent.source as Window; windowSource.postMessage('PONG', messageEvent.origin); return; } let postedTrace: PostedTrace; if (isPostedTraceWrapped(messageEvent.data)) { postedTrace = sanitizePostedTrace(messageEvent.data.perfetto); } else if (messageEvent.data instanceof ArrayBuffer) { postedTrace = {title: 'External trace', buffer: messageEvent.data}; } else { console.warn( 'Unknown postMessage() event received. If you are trying to open a ' + 'trace via postMessage(), this is a bug in your code. If not, this ' + 'could be due to some Chrome extension.'); console.log('origin:', messageEvent.origin, 'data:', messageEvent.data); return; } if (postedTrace.buffer.byteLength === 0) { throw new Error('Incoming message trace buffer is empty'); } const openTrace = () => { // For external traces, we need to disable other features such as // downloading and sharing a trace. globals.frontendLocalState.localOnlyMode = true; globals.dispatch(Actions.openTraceFromBuffer(postedTrace)); }; // If the origin is trusted open the trace directly. if (isTrustedOrigin(messageEvent.origin)) { openTrace(); return; } // If not ask the user if they expect this and trust the origin. showModal({ title: 'Open trace?', content: m('div', m('div', `${messageEvent.origin} is trying to open a trace file.`), m('div', 'Do you trust the origin and want to proceed?')), buttons: [ {text: 'NO', primary: true, id: 'pm_reject_trace', action: () => {}}, {text: 'YES', primary: false, id: 'pm_open_trace', action: openTrace}, ], }); } function sanitizePostedTrace(postedTrace: PostedTrace): PostedTrace { const result: PostedTrace = { title: sanitizeString(postedTrace.title), buffer: postedTrace.buffer }; if (postedTrace.url !== undefined) { result.url = sanitizeString(postedTrace.url); } return result; } function sanitizeString(str: string): string { return str.replace(/[^A-Za-z0-9.\-_#:/?=&;% ]/g, ' '); } // tslint:disable:no-any function isPostedTraceWrapped(obj: any): obj is PostedTraceWrapped { const wrapped = obj as PostedTraceWrapped; if (wrapped.perfetto === undefined) { return false; } return wrapped.perfetto.buffer !== undefined && wrapped.perfetto.title !== undefined; }