• 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 * 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