1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview Implements a check whether an app id lists an origin. 7 */ 8'use strict'; 9 10/** 11 * Parses the text as JSON and returns it as an array of strings. 12 * @param {string} text Input JSON 13 * @return {!Array.<string>} Array of origins 14 */ 15function getOriginsFromJson(text) { 16 try { 17 var urls = JSON.parse(text); 18 var origins = {}; 19 for (var i = 0, url; url = urls[i]; i++) { 20 var origin = getOriginFromUrl(url); 21 if (origin) { 22 origins[origin] = origin; 23 } 24 } 25 return Object.keys(origins); 26 } catch (e) { 27 console.log(UTIL_fmt('could not parse ' + text)); 28 return []; 29 } 30} 31 32/** 33 * Retrieves a set of distinct app ids from the sign challenges. 34 * @param {Array.<SignChallenge>=} signChallenges Input sign challenges. 35 * @return {Array.<string>} array of distinct app ids. 36 */ 37function getDistinctAppIds(signChallenges) { 38 if (!signChallenges) { 39 return []; 40 } 41 var appIds = {}; 42 for (var i = 0, request; request = signChallenges[i]; i++) { 43 var appId = request['appId']; 44 if (appId) { 45 appIds[appId] = appId; 46 } 47 } 48 return Object.keys(appIds); 49} 50 51/** 52 * Provides an object to track checking a list of appIds. 53 * @param {!TextFetcher} fetcher A URL fetcher. 54 * @param {!Countdown} timer A timer by which to resolve all provided app ids. 55 * @param {string} origin The origin to check. 56 * @param {!Array.<string>} appIds The app ids to check. 57 * @param {boolean} allowHttp Whether to allow http:// URLs. 58 * @param {string=} opt_logMsgUrl A log message URL. 59 * @constructor 60 */ 61function AppIdChecker(fetcher, timer, origin, appIds, allowHttp, opt_logMsgUrl) 62 { 63 /** @private {!TextFetcher} */ 64 this.fetcher_ = fetcher; 65 /** @private {!Countdown} */ 66 this.timer_ = timer; 67 /** @private {string} */ 68 this.origin_ = origin; 69 var appIdsMap = {}; 70 if (appIds) { 71 for (var i = 0; i < appIds.length; i++) { 72 appIdsMap[appIds[i]] = appIds[i]; 73 } 74 } 75 /** @private {Array.<string>} */ 76 this.distinctAppIds_ = Object.keys(appIdsMap); 77 /** @private {boolean} */ 78 this.allowHttp_ = allowHttp; 79 /** @private {string|undefined} */ 80 this.logMsgUrl_ = opt_logMsgUrl; 81 82 /** @private {boolean} */ 83 this.closed_ = false; 84 /** @private {boolean} */ 85 this.anyInvalidAppIds_ = false; 86 /** @private {number} */ 87 this.fetchedAppIds_ = 0; 88} 89 90/** 91 * Checks whether all the app ids provided can be asserted by the given origin. 92 * @return {Promise.<boolean>} A promise for the result of the check 93 */ 94AppIdChecker.prototype.doCheck = function() { 95 if (!this.distinctAppIds_.length) 96 return Promise.resolve(false); 97 98 if (this.allAppIdsEqualOrigin_()) { 99 // Trivially allowed. 100 return Promise.resolve(true); 101 } else { 102 var self = this; 103 // Begin checking remaining app ids. 104 var appIdChecks = self.distinctAppIds_.map(self.checkAppId_.bind(self)); 105 return Promise.all(appIdChecks).then(function(results) { 106 return results.every(function(result) { 107 if (!result) 108 self.anyInvalidAppIds_ = true; 109 return result; 110 }); 111 }); 112 } 113}; 114 115/** 116 * Checks if a single appId can be asserted by the given origin. 117 * @param {string} appId The appId to check 118 * @return {Promise.<boolean>} A promise for the result of the check 119 * @private 120 */ 121AppIdChecker.prototype.checkAppId_ = function(appId) { 122 if (appId == this.origin_) { 123 // Trivially allowed 124 return Promise.resolve(true); 125 } 126 var p = this.fetchAllowedOriginsForAppId_(appId); 127 var self = this; 128 return p.then(function(allowedOrigins) { 129 if (allowedOrigins.indexOf(self.origin_) == -1) { 130 console.warn(UTIL_fmt('Origin ' + self.origin_ + 131 ' not allowed by app id ' + appId)); 132 return false; 133 } 134 return true; 135 }); 136}; 137 138/** 139 * Closes this checker. No callback will be called after this checker is closed. 140 */ 141AppIdChecker.prototype.close = function() { 142 this.closed_ = true; 143}; 144 145/** 146 * @return {boolean} Whether all the app ids being checked are equal to the 147 * calling origin. 148 * @private 149 */ 150AppIdChecker.prototype.allAppIdsEqualOrigin_ = function() { 151 var self = this; 152 return this.distinctAppIds_.every(function(appId) { 153 return appId == self.origin_; 154 }); 155}; 156 157/** 158 * Fetches the allowed origins for an appId. 159 * @param {string} appId Application id 160 * @return {Promise.<!Array.<string>>} A promise for a list of allowed origins 161 * for appId 162 * @private 163 */ 164AppIdChecker.prototype.fetchAllowedOriginsForAppId_ = function(appId) { 165 if (!appId) { 166 return Promise.resolve([]); 167 } 168 169 if (appId.indexOf('http://') == 0 && !this.allowHttp_) { 170 console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested')); 171 return Promise.resolve([]); 172 } 173 174 var origin = getOriginFromUrl(appId); 175 if (!origin) { 176 return Promise.resolve([]); 177 } 178 179 var p = this.fetcher_.fetch(appId); 180 var self = this; 181 return p.then(getOriginsFromJson, function(rc_) { 182 var rc = /** @type {number} */(rc_); 183 console.log(UTIL_fmt('fetching ' + appId + ' failed: ' + rc)); 184 if (!(rc >= 400 && rc < 500) && !self.timer_.expired()) { 185 // Retry 186 return self.fetchAllowedOriginsForAppId_(appId); 187 } 188 return []; 189 }); 190}; 191