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 m from 'mithril'; 16 17import {Actions, PostedScrollToRange, PostedTrace} from '../common/actions'; 18 19import {initCssConstants} from './css_constants'; 20import {globals} from './globals'; 21import {toggleHelp} from './help_modal'; 22import {showModal} from './modal'; 23import {focusHorizontalRange} from './scroll_helper'; 24 25interface PostedTraceWrapped { 26 perfetto: PostedTrace; 27} 28 29interface PostedScrollToRangeWrapped { 30 perfetto: PostedScrollToRange; 31} 32 33// Returns whether incoming traces should be opened automatically or should 34// instead require a user interaction. 35function isTrustedOrigin(origin: string): boolean { 36 const TRUSTED_ORIGINS = [ 37 'https://chrometto.googleplex.com', 38 'https://uma.googleplex.com', 39 'https://android-build.googleplex.com', 40 ]; 41 if (origin === window.origin) return true; 42 if (TRUSTED_ORIGINS.includes(origin)) return true; 43 44 const hostname = new URL(origin).hostname; 45 if (hostname.endsWith('corp.google.com')) return true; 46 if (hostname === 'localhost' || hostname === '127.0.0.1') return true; 47 return false; 48} 49 50// Returns whether we should ignore a given message based on the value of 51// the 'perfettoIgnore' field in the event data. 52function shouldGracefullyIgnoreMessage(messageEvent: MessageEvent) { 53 return messageEvent.data.perfettoIgnore === true; 54} 55 56// The message handler supports loading traces from an ArrayBuffer. 57// There is no other requirement than sending the ArrayBuffer as the |data| 58// property. However, since this will happen across different origins, it is not 59// possible for the source website to inspect whether the message handler is 60// ready, so the message handler always replies to a 'PING' message with 'PONG', 61// which indicates it is ready to receive a trace. 62export function postMessageHandler(messageEvent: MessageEvent) { 63 if (shouldGracefullyIgnoreMessage(messageEvent)) { 64 // This message should not be handled in this handler, 65 // because it will be handled elsewhere. 66 return; 67 } 68 69 if (messageEvent.origin === 'https://tagassistant.google.com') { 70 // The GA debugger, does a window.open() and sends messages to the GA 71 // script. Ignore them. 72 return; 73 } 74 75 if (document.readyState !== 'complete') { 76 console.error('Ignoring message - document not ready yet.'); 77 return; 78 } 79 80 const fromOpener = messageEvent.source === window.opener; 81 const fromIframeHost = messageEvent.source === window.parent; 82 // This adds support for the folowing flow: 83 // * A (page that whats to open a trace in perfetto) opens B 84 // * B (does something to get the traceBuffer) 85 // * A is navigated to Perfetto UI 86 // * B sends the traceBuffer to A 87 // * closes itself 88 const fromOpenee = (messageEvent.source as WindowProxy).opener === window; 89 90 if (messageEvent.source === null || 91 !(fromOpener || fromIframeHost || fromOpenee)) { 92 // This can happen if an extension tries to postMessage. 93 return; 94 } 95 96 if (!('data' in messageEvent)) { 97 throw new Error('Incoming message has no data property'); 98 } 99 100 if (messageEvent.data === 'PING') { 101 // Cross-origin messaging means we can't read |messageEvent.source|, but 102 // it still needs to be of the correct type to be able to invoke the 103 // correct version of postMessage(...). 104 const windowSource = messageEvent.source as Window; 105 windowSource.postMessage('PONG', messageEvent.origin); 106 return; 107 } 108 109 if (messageEvent.data === 'SHOW-HELP') { 110 toggleHelp(); 111 return; 112 } 113 114 if (messageEvent.data === 'RELOAD-CSS-CONSTANTS') { 115 initCssConstants(); 116 return; 117 } 118 119 let postedScrollToRange: PostedScrollToRange; 120 if (isPostedScrollToRange(messageEvent.data)) { 121 postedScrollToRange = messageEvent.data.perfetto; 122 scrollToTimeRange(postedScrollToRange); 123 return; 124 } 125 126 let postedTrace: PostedTrace; 127 let keepApiOpen = false; 128 if (isPostedTraceWrapped(messageEvent.data)) { 129 postedTrace = sanitizePostedTrace(messageEvent.data.perfetto); 130 if (postedTrace.keepApiOpen) { 131 keepApiOpen = true; 132 } 133 } else if (messageEvent.data instanceof ArrayBuffer) { 134 postedTrace = {title: 'External trace', buffer: messageEvent.data}; 135 } else { 136 console.warn( 137 'Unknown postMessage() event received. If you are trying to open a ' + 138 'trace via postMessage(), this is a bug in your code. If not, this ' + 139 'could be due to some Chrome extension.'); 140 console.log('origin:', messageEvent.origin, 'data:', messageEvent.data); 141 return; 142 } 143 144 if (postedTrace.buffer.byteLength === 0) { 145 throw new Error('Incoming message trace buffer is empty'); 146 } 147 148 if (!keepApiOpen) { 149 /* Removing this event listener to avoid callers posting the trace multiple 150 * times. If the callers add an event listener which upon receiving 'PONG' 151 * posts the trace to ui.perfetto.dev, the callers can receive multiple 152 * 'PONG' messages and accidentally post the trace multiple times. This was 153 * part of the cause of b/182502595. 154 */ 155 window.removeEventListener('message', postMessageHandler); 156 } 157 158 const openTrace = () => { 159 // For external traces, we need to disable other features such as 160 // downloading and sharing a trace. 161 postedTrace.localOnly = true; 162 globals.dispatch(Actions.openTraceFromBuffer(postedTrace)); 163 }; 164 165 // If the origin is trusted open the trace directly. 166 if (isTrustedOrigin(messageEvent.origin)) { 167 openTrace(); 168 return; 169 } 170 171 // If not ask the user if they expect this and trust the origin. 172 showModal({ 173 title: 'Open trace?', 174 content: 175 m('div', 176 m('div', `${messageEvent.origin} is trying to open a trace file.`), 177 m('div', 'Do you trust the origin and want to proceed?')), 178 buttons: [ 179 {text: 'NO', primary: true}, 180 {text: 'YES', primary: false, action: openTrace}, 181 ], 182 }); 183} 184 185function sanitizePostedTrace(postedTrace: PostedTrace): PostedTrace { 186 const result: PostedTrace = { 187 title: sanitizeString(postedTrace.title), 188 buffer: postedTrace.buffer, 189 keepApiOpen: postedTrace.keepApiOpen, 190 }; 191 if (postedTrace.url !== undefined) { 192 result.url = sanitizeString(postedTrace.url); 193 } 194 return result; 195} 196 197function sanitizeString(str: string): string { 198 return str.replace(/[^A-Za-z0-9.\-_#:/?=&;%+$ ]/g, ' '); 199} 200 201function isTraceViewerReady(): boolean { 202 return !!(globals.getCurrentEngine()?.ready); 203} 204 205const _maxScrollToRangeAttempts = 20; 206async function scrollToTimeRange( 207 postedScrollToRange: PostedScrollToRange, maxAttempts?: number) { 208 const ready = isTraceViewerReady(); 209 if (!ready) { 210 if (maxAttempts === undefined) { 211 maxAttempts = 0; 212 } 213 if (maxAttempts > _maxScrollToRangeAttempts) { 214 console.warn('Could not scroll to time range. Trace viewer not ready.'); 215 return; 216 } 217 setTimeout(scrollToTimeRange, 200, postedScrollToRange, maxAttempts + 1); 218 } else { 219 focusHorizontalRange( 220 postedScrollToRange.timeStart, 221 postedScrollToRange.timeEnd, 222 postedScrollToRange.viewPercentage); 223 } 224} 225 226function isPostedScrollToRange(obj: unknown): 227 obj is PostedScrollToRangeWrapped { 228 const wrapped = obj as PostedScrollToRangeWrapped; 229 if (wrapped.perfetto === undefined) { 230 return false; 231 } 232 return wrapped.perfetto.timeStart !== undefined || 233 wrapped.perfetto.timeEnd !== undefined; 234} 235 236function isPostedTraceWrapped(obj: any): obj is PostedTraceWrapped { 237 const wrapped = obj as PostedTraceWrapped; 238 if (wrapped.perfetto === undefined) { 239 return false; 240 } 241 return wrapped.perfetto.buffer !== undefined && 242 wrapped.perfetto.title !== undefined; 243} 244