/**
* Handles a web sign request.
- * @param {MessageSender} sender The sender of the message.
+ * @param {MessageSender} messageSender The message sender.
* @param {Object} request The web page's sign request.
* @param {Function} sendResponse Called back with the result of the sign.
* @return {Closeable} Request handler that should be closed when the browser
* message channel is closed.
*/
-function handleWebSignRequest(sender, request, sendResponse) {
+function handleWebSignRequest(messageSender, request, sendResponse) {
var sentResponse = false;
var queuedSignRequest;
sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
}
+ var sender = createSenderFromMessageSender(messageSender);
+ if (!sender) {
+ sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
+ return null;
+ }
+
queuedSignRequest =
validateAndEnqueueSignRequest(
sender, request, 'signData', sendErrorResponse,
/**
* Handles a U2F sign request.
- * @param {MessageSender} sender The sender of the message.
+ * @param {MessageSender} messageSender The message sender.
* @param {Object} request The web page's sign request.
* @param {Function} sendResponse Called back with the result of the sign.
* @return {Closeable} Request handler that should be closed when the browser
* message channel is closed.
*/
-function handleU2fSignRequest(sender, request, sendResponse) {
+function handleU2fSignRequest(messageSender, request, sendResponse) {
var sentResponse = false;
var queuedSignRequest;
sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
}
+ var sender = createSenderFromMessageSender(messageSender);
+ if (!sender) {
+ sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
+ return null;
+ }
+
queuedSignRequest =
validateAndEnqueueSignRequest(
sender, request, 'signRequests', sendErrorResponse,
/**
* Validates a sign request using the given sign challenges name, and, if valid,
* enqueues the sign request for eventual processing.
- * @param {MessageSender} sender The sender of the message.
+ * @param {WebRequestSender} sender The sender of the message.
* @param {Object} request The web page's sign request.
* @param {string} signChallengesName The name of the sign challenges value in
* the request.
*/
function validateAndEnqueueSignRequest(sender, request,
signChallengesName, errorCb, successCb) {
- var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
- if (!origin) {
- errorCb({errorCode: ErrorCodes.BAD_REQUEST});
- return null;
+ function timeout() {
+ errorCb({errorCode: ErrorCodes.TIMEOUT});
}
- // More closure type inference fail.
- var nonNullOrigin = /** @type {string} */ (origin);
if (!isValidSignRequest(request, signChallengesName)) {
errorCb({errorCode: ErrorCodes.BAD_REQUEST});
var appId;
if (request['appId']) {
appId = request['appId'];
- } else {
- // A valid sign data has at least one challenge, so get the appId from
- // the first challenge.
+ } else if (signChallenges.length) {
appId = signChallenges[0]['appId'];
}
// Sanity check
errorCb({errorCode: ErrorCodes.BAD_REQUEST});
return null;
}
- var timer = createTimerForRequest(
- FACTORY_REGISTRY.getCountdownFactory(), request);
+ var timeoutValueSeconds = getTimeoutValueFromRequest(request);
+ // Attenuate watchdog timeout value less than the signer's timeout, so the
+ // watchdog only fires after the signer could reasonably have called back,
+ // not before.
+ timeoutValueSeconds = attenuateTimeoutInSeconds(timeoutValueSeconds,
+ MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
+ var watchdog = new WatchdogRequestHandler(timeoutValueSeconds, timeout);
+ var wrappedErrorCb = watchdog.wrapCallback(errorCb);
+ var wrappedSuccessCb = watchdog.wrapCallback(successCb);
+
+ var timer = createAttenuatedTimer(
+ FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
var logMsgUrl = request['logMsgUrl'];
// Queue sign requests from the same origin, to protect against simultaneous
// sign-out on many tabs resulting in repeated sign-in requests.
var queuedSignRequest = new QueuedSignRequest(signChallenges,
- timer, nonNullOrigin, errorCb, successCb, appId, sender.tlsChannelId,
- logMsgUrl);
- var requestToken = signRequestQueue.queueRequest(appId, nonNullOrigin,
+ timer, sender, wrappedErrorCb, wrappedSuccessCb, request['challenge'],
+ appId, logMsgUrl);
+ var requestToken = signRequestQueue.queueRequest(appId, sender.origin,
queuedSignRequest.begin.bind(queuedSignRequest), timer);
queuedSignRequest.setToken(requestToken);
- return queuedSignRequest;
+
+ watchdog.setCloseable(queuedSignRequest);
+ return watchdog;
}
/**
if (!request.hasOwnProperty(signChallengesName))
return false;
var signChallenges = request[signChallengesName];
- // If a sign request contains an empty array of challenges, it could never
- // be fulfilled. Fail.
- if (!signChallenges.length)
- return false;
+ var hasDefaultChallenge = request.hasOwnProperty('challenge');
var hasAppId = request.hasOwnProperty('appId');
- return isValidSignChallengeArray(signChallenges, !hasAppId);
+ // If the sign challenge array is empty, the global appId is required.
+ if (!hasAppId && (!signChallenges || !signChallenges.length)) {
+ return false;
+ }
+ return isValidSignChallengeArray(signChallenges, hasDefaultChallenge,
+ !hasAppId);
}
/**
* Adapter class representing a queued sign request.
* @param {!Array.<SignChallenge>} signChallenges The sign challenges.
* @param {Countdown} timer Timeout timer
- * @param {string} origin Signature origin
+ * @param {WebRequestSender} sender Message sender.
* @param {function(U2fError)} errorCb Error callback
* @param {function(SignChallenge, string, string)} successCb Success callback
+ * @param {string|undefined} opt_defaultChallenge A default sign challenge
+ * value, if a request does not provide one.
* @param {string|undefined} opt_appId The app id for the entire request.
- * @param {string|undefined} opt_tlsChannelId TLS Channel Id
* @param {string|undefined} opt_logMsgUrl Url to post log messages to
* @constructor
* @implements {Closeable}
*/
-function QueuedSignRequest(signChallenges, timer, origin, errorCb,
- successCb, opt_appId, opt_tlsChannelId, opt_logMsgUrl) {
+function QueuedSignRequest(signChallenges, timer, sender, errorCb,
+ successCb, opt_defaultChallenge, opt_appId, opt_logMsgUrl) {
/** @private {!Array.<SignChallenge>} */
this.signChallenges_ = signChallenges;
/** @private {Countdown} */
- this.timer_ = timer;
- /** @private {string} */
- this.origin_ = origin;
+ this.timer_ = timer.clone(this.close.bind(this));
+ /** @private {WebRequestSender} */
+ this.sender_ = sender;
/** @private {function(U2fError)} */
this.errorCb_ = errorCb;
/** @private {function(SignChallenge, string, string)} */
this.successCb_ = successCb;
/** @private {string|undefined} */
- this.appId_ = opt_appId;
+ this.defaultChallenge_ = opt_defaultChallenge;
/** @private {string|undefined} */
- this.tlsChannelId_ = opt_tlsChannelId;
+ this.appId_ = opt_appId;
/** @private {string|undefined} */
this.logMsgUrl_ = opt_logMsgUrl;
/** @private {boolean} */
/** Closes this sign request. */
QueuedSignRequest.prototype.close = function() {
if (this.closed_) return;
+ var hadBegunSigning = false;
if (this.begun_ && this.signer_) {
this.signer_.close();
+ hadBegunSigning = true;
}
if (this.token_) {
+ if (hadBegunSigning) {
+ console.log(UTIL_fmt('closing in-progress request'));
+ } else {
+ console.log(UTIL_fmt('closing timed-out request before processing'));
+ }
this.token_.complete();
}
this.closed_ = true;
* @param {QueuedRequestToken} token Token for this sign request.
*/
QueuedSignRequest.prototype.begin = function(token) {
+ if (this.timer_.expired()) {
+ console.log(UTIL_fmt('Queued request begun after timeout'));
+ this.close();
+ this.errorCb_({errorCode: ErrorCodes.TIMEOUT});
+ return;
+ }
this.begun_ = true;
this.setToken(token);
- this.signer_ = new Signer(this.timer_, this.origin_,
+ this.signer_ = new Signer(this.timer_, this.sender_,
this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
- this.tlsChannelId_, this.logMsgUrl_);
- if (!this.signer_.setChallenges(this.signChallenges_, this.appId_)) {
+ this.logMsgUrl_);
+ if (!this.signer_.setChallenges(this.signChallenges_, this.defaultChallenge_,
+ this.appId_)) {
token.complete();
this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST});
}
+ // Signer now has responsibility for maintaining timeout.
+ this.timer_.clearTimeout();
};
/**
/**
* Creates an object to track signing with a gnubby.
* @param {Countdown} timer Timer for sign request.
- * @param {string} origin The origin making the request.
+ * @param {WebRequestSender} sender The message sender.
* @param {function(U2fError)} errorCb Called when the sign operation fails.
* @param {function(SignChallenge, string, string)} successCb Called when the
* sign operation succeeds.
- * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
- * making the request.
* @param {string=} opt_logMsgUrl The url to post log messages to.
* @constructor
*/
-function Signer(timer, origin, errorCb, successCb,
- opt_tlsChannelId, opt_logMsgUrl) {
+function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) {
/** @private {Countdown} */
- this.timer_ = timer;
- /** @private {string} */
- this.origin_ = origin;
+ this.timer_ = timer.clone();
+ /** @private {WebRequestSender} */
+ this.sender_ = sender;
/** @private {function(U2fError)} */
this.errorCb_ = errorCb;
/** @private {function(SignChallenge, string, string)} */
this.successCb_ = successCb;
/** @private {string|undefined} */
- this.tlsChannelId_ = opt_tlsChannelId;
- /** @private {string|undefined} */
this.logMsgUrl_ = opt_logMsgUrl;
/** @private {boolean} */
// Allow http appIds for http origins. (Broken, but the caller deserves
// what they get.)
/** @private {boolean} */
- this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
+ this.allowHttp_ = this.sender_.origin ?
+ this.sender_.origin.indexOf('http://') == 0 : false;
/** @private {Closeable} */
this.handler_ = null;
}
/**
* Sets the challenges to be signed.
* @param {Array.<SignChallenge>} signChallenges The challenges to set.
+ * @param {string=} opt_defaultChallenge A default sign challenge
+ * value, if a request does not provide one.
* @param {string=} opt_appId The app id for the entire request.
* @return {boolean} Whether the challenges could be set.
*/
-Signer.prototype.setChallenges = function(signChallenges, opt_appId) {
+Signer.prototype.setChallenges = function(signChallenges, opt_defaultChallenge,
+ opt_appId) {
if (this.challengesSet_ || this.done_)
return false;
+ if (this.timer_.expired()) {
+ this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
+ return true;
+ }
/** @private {Array.<SignChallenge>} */
this.signChallenges_ = signChallenges;
/** @private {string|undefined} */
+ this.defaultChallenge_ = opt_defaultChallenge;
+ /** @private {string|undefined} */
this.appId_ = opt_appId;
/** @private {boolean} */
this.challengesSet_ = true;
this.notifyError_(error);
return;
}
- FACTORY_REGISTRY.getOriginChecker().canClaimAppIds(this.origin_, appIds)
+ FACTORY_REGISTRY.getOriginChecker()
+ .canClaimAppIds(this.sender_.origin, appIds)
.then(this.originChecked_.bind(this, appIds));
};
}
/** @private {!AppIdChecker} */
this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
- this.timer_.clone(), this.origin_,
+ this.timer_.clone(), this.sender_.origin,
/** @type {!Array.<string>} */ (appIds), this.allowHttp_,
this.logMsgUrl_);
this.appIdChecker_.doCheck().then(this.appIdChecked_.bind(this));
// Create the browser data for each challenge.
for (var i = 0; i < this.signChallenges_.length; i++) {
var challenge = this.signChallenges_[i];
- var serverChallenge = challenge['challenge'];
+ var serverChallenge;
+ if (challenge.hasOwnProperty('challenge')) {
+ serverChallenge = challenge['challenge'];
+ } else {
+ serverChallenge = this.defaultChallenge_;
+ }
+ if (!serverChallenge) {
+ console.warn(UTIL_fmt('challenge missing'));
+ return false;
+ }
var keyHandle = challenge['keyHandle'];
var browserData =
- makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_);
+ makeSignBrowserData(serverChallenge, this.sender_.origin,
+ this.sender_.tlsChannelId);
this.browserData_[keyHandle] = browserData;
this.serverChallenges_[keyHandle] = challenge;
}
var encodedChallenges = encodeSignChallenges(this.signChallenges_,
- this.appId_, this.getChallengeHash_.bind(this));
+ this.defaultChallenge_, this.appId_, this.getChallengeHash_.bind(this));
var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds,
/** Closes this signer. */
Signer.prototype.close = function() {
+ this.close_();
+};
+
+/**
+ * Closes this signer, and optionally notifies the caller of error.
+ * @param {boolean=} opt_notifying When true, this method is being called in the
+ * process of notifying the caller of an existing status. When false,
+ * the caller is notified with a default error value, ErrorCodes.TIMEOUT.
+ * @private
+ */
+Signer.prototype.close_ = function(opt_notifying) {
if (this.appIdChecker_) {
this.appIdChecker_.close();
}
this.handler_ = null;
}
this.timer_.clearTimeout();
+ if (!opt_notifying) {
+ this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
+ }
};
/**
Signer.prototype.notifyError_ = function(error) {
if (this.done_)
return;
- this.close();
this.done_ = true;
+ this.close_(true);
this.errorCb_(error);
};
Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
if (this.done_)
return;
- this.close();
this.done_ = true;
+ this.close_(true);
this.successCb_(challenge, info, browserData);
};