// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Implements a check whether an app id lists an origin. */ 'use strict'; /** * Parses the text as JSON and returns it as an array of strings. * @param {string} text Input JSON * @return {!Array} Array of origins */ function getOriginsFromJson(text) { try { var urls, i; var appIdData = JSON.parse(text); var trustedFacets = appIdData['trustedFacets']; if (trustedFacets) { var versionBlock; for (i = 0; versionBlock = trustedFacets[i]; i++) { if (versionBlock['version'] && versionBlock['version']['major'] == 1 && versionBlock['version']['minor'] == 0) { urls = versionBlock['ids']; break; } } } if (typeof urls == 'undefined') { throw Error('Could not find trustedFacets for version 1.0'); } var origins = {}; var url; for (i = 0; url = urls[i]; i++) { var origin = getOriginFromUrl(url); if (origin) { origins[origin] = origin; } } return Object.keys(origins); } catch (e) { console.error(UTIL_fmt('could not parse ' + text)); return []; } } /** * Retrieves a set of distinct app ids from the sign challenges. * @param {Array=} signChallenges Input sign challenges. * @return {Array} array of distinct app ids. */ function getDistinctAppIds(signChallenges) { if (!signChallenges) { return []; } var appIds = {}; for (var i = 0, request; request = signChallenges[i]; i++) { var appId = request['appId']; if (appId) { appIds[appId] = appId; } } return Object.keys(appIds); } /** * An object that checks one or more appIds' contents against an origin. * @interface */ function AppIdChecker() {} /** * Checks whether the given origin is allowed by all of the given appIds. * @param {!Countdown} timer A timer by which to resolve all provided app ids. * @param {string} origin The origin to check. * @param {!Array} appIds The app ids to check. * @param {boolean} allowHttp Whether to allow http:// URLs. * @param {string=} opt_logMsgUrl A log message URL. * @return {Promise} A promise for the result of the check */ AppIdChecker.prototype.checkAppIds = function( timer, origin, appIds, allowHttp, opt_logMsgUrl) {}; /** * An interface to create an AppIdChecker. * @interface */ function AppIdCheckerFactory() {} /** * @return {!AppIdChecker} A new AppIdChecker. */ AppIdCheckerFactory.prototype.create = function() {}; /** * Provides an object to track checking a list of appIds. * @param {!TextFetcher} fetcher A URL fetcher. * @constructor * @implements AppIdChecker */ function XhrAppIdChecker(fetcher) { /** @private {!TextFetcher} */ this.fetcher_ = fetcher; } /** * Checks whether all the app ids provided can be asserted by the given origin. * @param {!Countdown} timer A timer by which to resolve all provided app ids. * @param {string} origin The origin to check. * @param {!Array} appIds The app ids to check. * @param {boolean} allowHttp Whether to allow http:// URLs. * @param {string=} opt_logMsgUrl A log message URL. * @return {Promise} A promise for the result of the check */ XhrAppIdChecker.prototype.checkAppIds = function( timer, origin, appIds, allowHttp, opt_logMsgUrl) { if (this.timer_) { // Can't use the same object to check appIds more than once. return Promise.resolve(false); } /** @private {!Countdown} */ this.timer_ = timer; /** @private {string} */ this.origin_ = origin; var appIdsMap = {}; if (appIds) { for (var i = 0; i < appIds.length; i++) { appIdsMap[appIds[i]] = appIds[i]; } } /** @private {Array} */ this.distinctAppIds_ = Object.keys(appIdsMap); /** @private {boolean} */ this.allowHttp_ = allowHttp; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; if (!this.distinctAppIds_.length) { return Promise.resolve(false); } if (this.allAppIdsEqualOrigin_()) { // Trivially allowed. return Promise.resolve(true); } else { var self = this; // Begin checking remaining app ids. var appIdChecks = self.distinctAppIds_.map(self.checkAppId_.bind(self)); return Promise.all(appIdChecks).then(function(results) { return results.every(function(result) { return result; }); }); } }; /** * Checks if a single appId can be asserted by the given origin. * @param {string} appId The appId to check * @return {Promise} A promise for the result of the check * @private */ XhrAppIdChecker.prototype.checkAppId_ = function(appId) { if (appId == this.origin_) { // Trivially allowed return Promise.resolve(true); } var p = this.fetchAllowedOriginsForAppId_(appId); var self = this; return p.then(function(allowedOrigins) { if (allowedOrigins.indexOf(self.origin_) == -1) { console.warn(UTIL_fmt( 'Origin ' + self.origin_ + ' not allowed by app id ' + appId)); return false; } return true; }); }; /** * @return {boolean} Whether all the app ids being checked are equal to the * calling origin. * @private */ XhrAppIdChecker.prototype.allAppIdsEqualOrigin_ = function() { var self = this; return this.distinctAppIds_.every(function(appId) { return appId == self.origin_; }); }; /** * Fetches the allowed origins for an appId. * @param {string} appId Application id * @return {Promise>} A promise for a list of allowed origins * for appId * @private */ XhrAppIdChecker.prototype.fetchAllowedOriginsForAppId_ = function(appId) { if (!appId) { return Promise.resolve([]); } if (appId.indexOf('http://') == 0 && !this.allowHttp_) { console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested')); return Promise.resolve([]); } var origin = getOriginFromUrl(appId); if (!origin) { return Promise.resolve([]); } var p = this.fetcher_.fetch(appId); var self = this; return p.then(getOriginsFromJson, function(rc_) { var rc = /** @type {number} */ (rc_); console.log(UTIL_fmt('fetching ' + appId + ' failed: ' + rc)); if (!(rc >= 400 && rc < 500) && !self.timer_.expired()) { // Retry return self.fetchAllowedOriginsForAppId_(appId); } return []; }); }; /** * A factory to create an XhrAppIdChecker. * @implements AppIdCheckerFactory * @param {!TextFetcher} fetcher * @constructor */ function XhrAppIdCheckerFactory(fetcher) { /** @private {!TextFetcher} */ this.fetcher_ = fetcher; } /** * @return {!AppIdChecker} A new AppIdChecker. */ XhrAppIdCheckerFactory.prototype.create = function() { return new XhrAppIdChecker(this.fetcher_); };