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 (TRUSTED_ORIGINS.includes(origin)) return true; 34 if (new URL(origin).hostname.endsWith('corp.google.com')) return true; 35 return false; 36} 37 38 39// The message handler supports loading traces from an ArrayBuffer. 40// There is no other requirement than sending the ArrayBuffer as the |data| 41// property. However, since this will happen across different origins, it is not 42// possible for the source website to inspect whether the message handler is 43// ready, so the message handler always replies to a 'PING' message with 'PONG', 44// which indicates it is ready to receive a trace. 45export function postMessageHandler(messageEvent: MessageEvent) { 46 if (messageEvent.origin === 'https://tagassistant.google.com') { 47 // The GA debugger, does a window.open() and sends messages to the GA 48 // script. Ignore them. 49 return; 50 } 51 52 if (document.readyState !== 'complete') { 53 console.error('Ignoring message - document not ready yet.'); 54 return; 55 } 56 57 if (messageEvent.source === null || messageEvent.source !== window.opener) { 58 // This can happen if an extension tries to postMessage. 59 return; 60 } 61 62 if (!('data' in messageEvent)) { 63 throw new Error('Incoming message has no data property'); 64 } 65 66 if (messageEvent.data === 'PING') { 67 // Cross-origin messaging means we can't read |messageEvent.source|, but 68 // it still needs to be of the correct type to be able to invoke the 69 // correct version of postMessage(...). 70 const windowSource = messageEvent.source as Window; 71 windowSource.postMessage('PONG', messageEvent.origin); 72 return; 73 } 74 75 let postedTrace: PostedTrace; 76 77 if (isPostedTraceWrapped(messageEvent.data)) { 78 postedTrace = sanitizePostedTrace(messageEvent.data.perfetto); 79 } else if (messageEvent.data instanceof ArrayBuffer) { 80 postedTrace = {title: 'External trace', buffer: messageEvent.data}; 81 } else { 82 console.warn( 83 'Unknown postMessage() event received. If you are trying to open a ' + 84 'trace via postMessage(), this is a bug in your code. If not, this ' + 85 'could be due to some Chrome extension.'); 86 console.log('origin:', messageEvent.origin, 'data:', messageEvent.data); 87 return; 88 } 89 90 if (postedTrace.buffer.byteLength === 0) { 91 throw new Error('Incoming message trace buffer is empty'); 92 } 93 94 const openTrace = () => { 95 // For external traces, we need to disable other features such as 96 // downloading and sharing a trace. 97 globals.frontendLocalState.localOnlyMode = true; 98 globals.dispatch(Actions.openTraceFromBuffer(postedTrace)); 99 }; 100 101 // If the origin is trusted open the trace directly. 102 if (isTrustedOrigin(messageEvent.origin)) { 103 openTrace(); 104 return; 105 } 106 107 // If not ask the user if they expect this and trust the origin. 108 showModal({ 109 title: 'Open trace?', 110 content: 111 m('div', 112 m('div', `${messageEvent.origin} is trying to open a trace file.`), 113 m('div', 'Do you trust the origin and want to proceed?')), 114 buttons: [ 115 {text: 'NO', primary: true, id: 'pm_reject_trace', action: () => {}}, 116 {text: 'YES', primary: false, id: 'pm_open_trace', action: openTrace}, 117 ], 118 }); 119} 120 121function sanitizePostedTrace(postedTrace: PostedTrace): PostedTrace { 122 const result: PostedTrace = { 123 title: sanitizeString(postedTrace.title), 124 buffer: postedTrace.buffer 125 }; 126 if (postedTrace.url !== undefined) { 127 result.url = sanitizeString(postedTrace.url); 128 } 129 return result; 130} 131 132function sanitizeString(str: string): string { 133 return str.replace(/[^A-Za-z0-9.\-_#:/?=&;% ]/g, ' '); 134} 135 136// tslint:disable:no-any 137function isPostedTraceWrapped(obj: any): obj is PostedTraceWrapped { 138 const wrapped = obj as PostedTraceWrapped; 139 if (wrapped.perfetto === undefined) { 140 return false; 141 } 142 return wrapped.perfetto.buffer !== undefined && 143 wrapped.perfetto.title !== undefined; 144} 145