blob: 85c6d90d7ce6d669793b525399cb48c8c7389f7d [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 handling of appIds.
*/
'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 = JSON.parse(text);
var origins = {};
for (var i = 0, url; url = urls[i]; i++) {
var origin = getOriginFromUrl(url);
if (origin) {
origins[origin] = origin;
}
}
return Object.keys(origins);
} catch (e) {
console.log(UTIL_fmt('could not parse ' + text));
return [];
}
}
/**
* Fetches the app id, and calls a callback with list of allowed origins for it.
* @param {string} appId The app id to fetch.
* @param {function(number, Array.<string>=)} cb
* Called with the result of the app id fetch, and, if the fetch was
* successful, a list of allowed origins for the app id.
*/
function fetchAppId(appId, cb) {
var origin = getOriginFromUrl(appId);
if (!origin) {
cb(404);
return;
}
var xhr = new XMLHttpRequest();
var origins = [];
xhr.open('GET', appId, true);
xhr.onloadend = function() {
if (xhr.status != 200) {
cb(xhr.status);
return;
}
cb(xhr.status, getOriginsFromJson(xhr.responseText));
};
xhr.send();
}
/**
* Retrieves a set of distinct app ids from the SignData.
* @param {SignData=} signData Input signature data
* @return {Array.<string>} array of distinct app ids.
*/
function getDistinctAppIds(signData) {
if (!signData) {
return [];
}
var appIds = {};
for (var i = 0, request; request = signData[i]; i++) {
var appId = request['appId'];
if (appId) {
appIds[appId] = appId;
}
}
return Object.keys(appIds);
}
/**
* Checks whether an appId is valid for a given origin.
* @param {!string} appId Application id
* @param {!string} origin Origin
* @param {!Array.<string>} allowedOrigins the list of allowed origins for each
* appId.
* @return {boolean} whether the appId is allowed for the origin.
*/
function isValidAppIdForOrigin(appId, origin, allowedOrigins) {
if (!appId)
return false;
if (appId == origin) {
// trivially allowed
return true;
}
if (!allowedOrigins)
return false;
return allowedOrigins.indexOf(origin) >= 0;
}
/**
* Provides an object to track checking a list of 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.
* @constructor
*/
function AppIdChecker(timer, origin, appIds, allowHttp, opt_logMsgUrl) {
/** @private {!Countdown} */
this.timer_ = timer;
var appIdsMap = {};
if (appIds) {
for (var i = 0; i < appIds.length; i++) {
appIdsMap[appIds[i]] = appIds[i];
}
}
/** @private {string} */
this.origin_ = origin;
/** @private {Array.<string>} */
this.distinctAppIds_ = Object.keys(appIdsMap);
/** @private {boolean} */
this.allowHttp_ = allowHttp;
/** @private {string|undefined} */
this.logMsgUrl_ = opt_logMsgUrl;
/** @private {boolean} */
this.closed_ = false;
/** @private {boolean} */
this.anyInvalidAppIds_ = false;
/** @private {number} */
this.fetchedAppIds_ = 0;
}
/**
* Checks all the app ids provided, and calls a callback indicating whether
* all of them can be asserted by the given orign.
* @param {function(boolean)} cb Called with the result of the check.
*/
AppIdChecker.prototype.doCheck = function(cb) {
if (this.cb_) {
// Check already in progress: no go.
this.notify_(false);
return;
}
/** @private {function(boolean)} */
this.cb_ = cb;
if (!this.distinctAppIds_.length) {
this.notify_(false);
return;
}
for (var i = 0; i < this.distinctAppIds_.length; i++) {
var appId = this.distinctAppIds_[i];
if (appId == this.origin_) {
// Trivially allowed.
this.fetchedAppIds_++;
if (this.fetchedAppIds_ == this.distinctAppIds_.length &&
!this.anyInvalidAppIds_) {
// Last app id was fetched, and they were all valid: we're done.
// (Note that the case when anyInvalidAppIds_ is true doesn't need to
// be handled here: the callback was already called with false at that
// point, see fetchedAllowedOriginsForAppId_.)
this.notify_(true);
}
} else {
var start = new Date();
this.fetchAllowedOriginsForAppId_(appId,
this.fetchedAllowedOriginsForAppId_.bind(this, appId, start));
}
}
};
/**
* Closes this checker. No callback will be called after this checker is closed.
*/
AppIdChecker.prototype.close = function() {
this.closed_ = true;
};
/**
* Notifies the callback with the result.
* @param {boolean} result The result to notify.
* @private
*/
AppIdChecker.prototype.notify_ = function(result) {
if (!this.closed_) {
this.closed_ = true;
if (this.cb_) {
this.cb_(result);
}
}
};
/**
* Fetches the allowed origins for an appId.
* @param {string} appId Application id
* @param {function(number, !Array.<string>)} cb Called back with an HTTP
* response code and a list of allowed origins for appId.
* @private
*/
AppIdChecker.prototype.fetchAllowedOriginsForAppId_ = function(appId, cb) {
var allowedOrigins = [];
if (!appId) {
cb(200, allowedOrigins);
return;
}
if (appId.indexOf('http://') == 0 && !this.allowHttp_) {
console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested'));
cb(200, allowedOrigins);
return;
}
// TODO: hack for old enrolled gnubbies, don't treat
// accounts.google.com/login.corp.google.com specially when cryptauth server
// stops reporting them as appId.
if (appId == 'https://accounts.google.com') {
allowedOrigins = ['https://login.corp.google.com'];
cb(200, allowedOrigins);
return;
}
if (appId == 'https://login.corp.google.com') {
allowedOrigins = ['https://accounts.google.com'];
cb(200, allowedOrigins);
return;
}
// Termination of this function relies in fetchAppId completing.
// (Not completing would be a bug in XMLHttpRequest.)
// TODO: provide a termination guarantee, e.g. with a timer?
fetchAppId(appId, function(rc, origins) {
if (rc != 200) {
console.log(UTIL_fmt('fetching ' + appId + ' failed: ' + rc));
allowedOrigins = [];
} else {
allowedOrigins = /** @type {!Array.<string>} */ (origins);
}
cb(rc, allowedOrigins);
});
};
/**
* Called with the result of an app id fetch.
* @param {string} appId the app id that was fetched.
* @param {Date} start the time the fetch request started.
* @param {number} rc The HTTP response code for the app id fetch.
* @param {!Array.<string>} allowedOrigins The origins allowed for this app id.
* @private
*/
AppIdChecker.prototype.fetchedAllowedOriginsForAppId_ =
function(appId, start, rc, allowedOrigins) {
var end = new Date();
this.fetchedAppIds_++;
this.logFetchAppIdResult_(appId, end - start, allowedOrigins);
if (rc != 200 && !(rc >= 400 && rc < 500)) {
if (this.timer_.expired()) {
this.notify_(false);
} else {
start = new Date();
this.fetchAllowedOriginsForAppId_(appId,
this.fetchedAllowedOriginsForAppId_.bind(this, appId, start));
}
return;
}
if (!isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) {
console.warn(UTIL_fmt('Origin ' + this.origin_ + ' not allowed by app id ' +
appId));
this.logInvalidOriginForAppId_(appId);
this.anyInvalidAppIds_ = true;
this.notify_(false);
}
if (this.fetchedAppIds_ == this.distinctAppIds_.length &&
!this.anyInvalidAppIds_) {
// Last app id was fetched, and they were all valid: we're done.
this.notify_(true);
}
};
/**
* Logs the result of fetching an appId.
* @param {!string} appId Application Id
* @param {number} millis elapsed time while fetching the appId.
* @param {Array.<string>} allowedOrigins the allowed origins retrieved.
* @private
*/
AppIdChecker.prototype.logFetchAppIdResult_ =
function(appId, millis, allowedOrigins) {
var logMsg = 'log=fetchappid&appid=' + appId + '&millis=' + millis +
'&numorigins=' + allowedOrigins.length;
logMessage(logMsg, this.logMsgUrl_);
};
/**
* Logs a mismatch between an origin and an appId.
* @param {!string} appId Application id
* @private
*/
AppIdChecker.prototype.logInvalidOriginForAppId_ = function(appId) {
var logMsg = 'log=originrejected&origin=' + this.origin_ + '&appid=' + appId;
logMessage(logMsg, this.logMsgUrl_);
};