• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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