blob: 06a1ab11358a8d4f34c728b92635003114c6716e [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ParentAccessController} from './parent_access_controller.js';
import {GetOAuthTokenStatus, ParentAccessServerMessageType} from './parent_access_ui.mojom-webui.js';
import {getParentAccessUIHandler} from './parent_access_ui_handler.js';
import {WebviewManager} from './webview_manager.js';
/**
* List of URL hosts that can be requested by the webview. The
* webview URL's host is implicitly included in this list.
* @const {!Array<string>}
*/
const ALLOWED_HOSTS = [
'googleapis.com',
'gstatic.com',
'googleusercontent.com',
'google.com',
];
/**
* The local dev server host, which is the only non-https URL the
* webview is permitted to load.
*/
const LOCAL_DEV_SERVER_HOST = 'localhost:9879';
class ParentAccessUi extends PolymerElement {
constructor() {
super();
this.webview_manager_ = null;
this.server = null;
this.parentAccessUIHandler = getParentAccessUIHandler();
}
static get is() {
return 'parent-access-ui';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
webviewLoading: {type: Boolean, value: true},
};
}
/**
* Returns whether the provided request should be allowed.
* @param {string} url Request that is issued by the webview.
* @return {boolean} Whether the request should be allowed.
*/
isAllowedRequest(url) {
const requestUrl = new URL(url);
// Allow non https only for requests to a local development server webview
// URL, which would have been specified at the command line.
if (requestUrl.host === LOCAL_DEV_SERVER_HOST) {
return true;
}
// Otherwise, all requests should be https and in the ALLOWED_HOSTS list.
const requestIsHttps = requestUrl.protocol === 'https:';
const requestIsInAllowedHosts = ALLOWED_HOSTS.some(
(allowedHost) => requestUrl.host == allowedHost ||
requestUrl.host.endsWith(allowedHost));
return requestIsHttps && requestIsInAllowedHosts;
}
/**
* Returns whether the provided request should receive auth headers.
* @param {string} url Request that is issued by the webview.
* @return {boolean} Whether the request should be allowed.
*/
shouldReceiveAuthHeader(url) {
const requestUrl = new URL(url);
const webviewUrl = new URL(this.webviewUrl_);
// Only the webviewUrl URL should receive the auth header, because for
// security reasons, we shouldn't distribute the OAuth token any more
// broadly that strictly necessary for the widget to function, thereby
// minimizing the attack surface for the token.
return requestUrl.host === webviewUrl.host;
}
/** @override */
ready() {
super.ready();
this.shadowRoot.querySelector('webview').addEventListener(
'contentload', () => {
this.webviewLoading = false;
});
this.configureUi().then(
() => {/* success */},
(error) => {
this.showErrorPage_();
},
);
}
async configureUi() {
/**
* @private {string} The initial URL for the webview.
*/
this.webviewUrl_ =
(await this.parentAccessUIHandler.getParentAccessURL()).url;
try {
const parsedWebviewUrl = new URL(this.webviewUrl_);
// Set the filter to accept postMessages from the webviewURL's origin
// only.
const eventOriginFilter = parsedWebviewUrl.origin;
const oauthFetchResult = await this.parentAccessUIHandler.getOAuthToken();
if (oauthFetchResult.status != GetOAuthTokenStatus.kSuccess) {
throw new Error('OAuth token was not successfully fetched.');
}
const webview =
/** @type {!WebView} */ (this.$.webview);
const accessToken = oauthFetchResult.oauthToken;
// Set up the WebviewManager to handle the configuration and
// access control for the webview.
this.webview_manager_ = new WebviewManager(webview);
this.webview_manager_.setAccessToken(accessToken, (url) => {
return this.shouldReceiveAuthHeader(url);
});
this.webview_manager_.setAllowRequestFn((url) => {
return this.isAllowedRequest(url);
});
// Setting the src of the webview triggers the loading process.
const url = new URL(this.webviewUrl_);
webview.src = url.toString();
webview.addEventListener('loadabort', () => {
this.webviewLoading = false;
this.showErrorPage_();
});
// Set up the controller. It will automatically start the initialization
// handshake with the hosted content.
this.server = new ParentAccessController(
webview, url.toString(), eventOriginFilter);
} catch (e) {
this.showErrorPage_();
return;
}
// What follows is the main message handling loop. The received base64
// encoded proto messages are passed to c++ handler for proto decoding
// before they are handled. When the following while loop terminates, the
// flow will either proceed to the next steps, or show a terminal error.
let lastServerMessageType = ParentAccessServerMessageType.kIgnore;
while (lastServerMessageType === ParentAccessServerMessageType.kIgnore) {
const parentAccessCallback = await Promise.race([
this.server.whenParentAccessCallbackReceived(),
this.server.whenInitializationError(),
]);
// Notify ParentAccessUIHandler that we received a ParentAccessCallback.
// The handler will attempt to parse the callback and return the status.
const parentAccessServerMessage =
await this.parentAccessUIHandler.onParentAccessCallbackReceived(
parentAccessCallback);
// If the parentAccessCallback couldn't be parsed, then an initialization
// or communication error occurred between the ParentAccessController and
// the server.
if (!(parentAccessServerMessage instanceof Object)) {
console.error('Error initializing ParentAccessController');
this.showErrorPage_();
break;
}
lastServerMessageType = parentAccessServerMessage.message.type;
switch (lastServerMessageType) {
case ParentAccessServerMessageType.kParentVerified:
this.dispatchEvent(new CustomEvent('show-after', {
bubbles: true,
composed: true,
}));
break;
case ParentAccessServerMessageType.kIgnore:
continue;
case ParentAccessServerMessageType.kError:
default:
this.showErrorPage_();
break;
}
}
}
showErrorPage_() {
this.dispatchEvent(new CustomEvent('show-error', {
bubbles: true,
composed: true,
}));
}
}
customElements.define(ParentAccessUi.is, ParentAccessUi);