• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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