1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import * as m from 'mithril'; 16 17import {Actions, PostedTrace} from '../common/actions'; 18 19import {globals} from './globals'; 20import {showModal} from './modal'; 21 22interface PostedTraceWrapped { 23 perfetto: PostedTrace; 24} 25 26// Returns whether incoming traces should be opened automatically or should 27// instead require a user interaction. 28function isTrustedOrigin(origin: string): boolean { 29 const TRUSTED_ORIGINS = [ 30 'https://chrometto.googleplex.com', 31 'https://uma.googleplex.com', 32 ]; 33 if (origin === window.origin) return true; 34 if (TRUSTED_ORIGINS.includes(origin)) return true; 35 if (new URL(origin).hostname.endsWith('corp.google.com')) return true; 36 return false; 37} 38 39 40// The message handler supports loading traces from an ArrayBuffer. 41// There is no other requirement than sending the ArrayBuffer as the |data| 42// property. However, since this will happen across different origins, it is not 43// possible for the source website to inspect whether the message handler is 44// ready, so the message handler always replies to a 'PING' message with 'PONG', 45// which indicates it is ready to receive a trace. 46export function postMessageHandler(messageEvent: MessageEvent) { 47 if (messageEvent.origin === 'https://tagassistant.google.com') { 48 // The GA debugger, does a window.open() and sends messages to the GA 49 // script. Ignore them. 50 return; 51 } 52 53 if (document.readyState !== 'complete') { 54 console.error('Ignoring message - document not ready yet.'); 55 return; 56 } 57 58 if (messageEvent.source === null || messageEvent.source !== window.opener) { 59 // This can happen if an extension tries to postMessage. 60 return; 61 } 62 63 if (!('data' in messageEvent)) { 64 throw new Error('Incoming message has no data property'); 65 } 66 67 if (messageEvent.data === 'PING') { 68 // Cross-origin messaging means we can't read |messageEvent.source|, but 69 // it still needs to be of the correct type to be able to invoke the 70 // correct version of postMessage(...). 71 const windowSource = messageEvent.source as Window; 72 windowSource.postMessage('PONG', messageEvent.origin); 73 return; 74 } 75 76 let postedTrace: PostedTrace; 77 78 if (isPostedTraceWrapped(messageEvent.data)) { 79 postedTrace = sanitizePostedTrace(messageEvent.data.perfetto); 80 } else if (messageEvent.data instanceof ArrayBuffer) { 81 postedTrace = {title: 'External trace', buffer: messageEvent.data}; 82 } else { 83 console.warn( 84 'Unknown postMessage() event received. If you are trying to open a ' + 85 'trace via postMessage(), this is a bug in your code. If not, this ' + 86 'could be due to some Chrome extension.'); 87 console.log('origin:', messageEvent.origin, 'data:', messageEvent.data); 88 return; 89 } 90 91 if (postedTrace.buffer.byteLength === 0) { 92 throw new Error('Incoming message trace buffer is empty'); 93 } 94 95 /* Removing this event listener to avoid callers posting the trace multiple 96 * times. If the callers add an event listener which upon receiving 'PONG' 97 * posts the trace to ui.perfetto.dev, the callers can receive multiple 'PONG' 98 * messages and accidentally post the trace multiple times. This was part of 99 * the cause of b/182502595. 100 */ 101 window.removeEventListener('message', postMessageHandler); 102 103 const openTrace = () => { 104 // For external traces, we need to disable other features such as 105 // downloading and sharing a trace. 106 postedTrace.localOnly = true; 107 globals.dispatch(Actions.openTraceFromBuffer(postedTrace)); 108 }; 109 110 // If the origin is trusted open the trace directly. 111 if (isTrustedOrigin(messageEvent.origin)) { 112 openTrace(); 113 return; 114 } 115 116 // If not ask the user if they expect this and trust the origin. 117 showModal({ 118 title: 'Open trace?', 119 content: 120 m('div', 121 m('div', `${messageEvent.origin} is trying to open a trace file.`), 122 m('div', 'Do you trust the origin and want to proceed?')), 123 buttons: [ 124 {text: 'NO', primary: true, id: 'pm_reject_trace', action: () => {}}, 125 {text: 'YES', primary: false, id: 'pm_open_trace', action: openTrace}, 126 ], 127 }); 128} 129 130function sanitizePostedTrace(postedTrace: PostedTrace): PostedTrace { 131 const result: PostedTrace = { 132 title: sanitizeString(postedTrace.title), 133 buffer: postedTrace.buffer 134 }; 135 if (postedTrace.url !== undefined) { 136 result.url = sanitizeString(postedTrace.url); 137 } 138 return result; 139} 140 141function sanitizeString(str: string): string { 142 return str.replace(/[^A-Za-z0-9.\-_#:/?=&;%+ ]/g, ' '); 143} 144 145// tslint:disable:no-any 146function isPostedTraceWrapped(obj: any): obj is PostedTraceWrapped { 147 const wrapped = obj as PostedTraceWrapped; 148 if (wrapped.perfetto === undefined) { 149 return false; 150 } 151 return wrapped.perfetto.buffer !== undefined && 152 wrapped.perfetto.title !== undefined; 153} 154