blob: 4b05cbced369eb576f3bff7984bb1db6e609d98d [file] [log] [blame]
// 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<string>} 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<SignChallenge>=} signChallenges Input sign challenges.
* @return {Array<string>} 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<string>} appIds The app ids to check.
* @param {boolean} allowHttp Whether to allow http:// URLs.
* @param {string=} opt_logMsgUrl A log message URL.
* @return {Promise<boolean>} 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<string>} appIds The app ids to check.
* @param {boolean} allowHttp Whether to allow http:// URLs.
* @param {string=} opt_logMsgUrl A log message URL.
* @return {Promise<boolean>} 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<string>} */
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<boolean>} 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<!Array<string>>} 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_);
};