// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Base TestChannel implementation.
*/
goog.provide('goog.labs.net.webChannel.BaseTestChannel');
goog.forwardDeclare('goog.labs.net.webChannel.WebChannelBase');
goog.require('goog.labs.net.webChannel.Channel');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.WebChannelDebug');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.net.WebChannel');
/**
* A TestChannel is used during the first part of channel negotiation
* with the server to create the channel. It helps us determine whether we're
* behind a buffering proxy.
*
* @constructor
* @struct
* @param {!goog.labs.net.webChannel.Channel} channel The channel
* that owns this test channel.
* @param {!goog.labs.net.webChannel.WebChannelDebug} channelDebug A
* WebChannelDebug instance to use for logging.
* @implements {goog.labs.net.webChannel.Channel}
*/
goog.labs.net.webChannel.BaseTestChannel = function(channel, channelDebug) {
/**
* The channel that owns this test channel
* @private {!goog.labs.net.webChannel.Channel}
*/
this.channel_ = channel;
/**
* The channel debug to use for logging
* @private {!goog.labs.net.webChannel.WebChannelDebug}
*/
this.channelDebug_ = channelDebug;
/**
* Extra HTTP headers to add to all the requests sent to the server.
* @private {?Object}
*/
this.extraHeaders_ = null;
/**
* The test request.
* @private {?goog.labs.net.webChannel.ChannelRequest}
*/
this.request_ = null;
/**
* Whether we have received the first result as an intermediate result. This
* helps us determine whether we're behind a buffering proxy.
* @private {boolean}
*/
this.receivedIntermediateResult_ = false;
/**
* The relative path for test requests.
* @private {?string}
*/
this.path_ = null;
/**
* The last status code received.
* @private {number}
*/
this.lastStatusCode_ = -1;
/**
* A subdomain prefix for using a subdomain in IE for the backchannel
* requests.
* @private {?string}
*/
this.hostPrefix_ = null;
/**
* The effective client protocol as indicated by the initial handshake
* response via the x-client-wire-protocol header.
*
* @private {?string}
*/
this.clientProtocol_ = null;
};
goog.scope(function() {
var WebChannel = goog.net.WebChannel;
var BaseTestChannel = goog.labs.net.webChannel.BaseTestChannel;
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
var requestStats = goog.labs.net.webChannel.requestStats;
var Channel = goog.labs.net.webChannel.Channel;
/**
* Enum type for the test channel state machine
* @enum {number}
* @private
*/
BaseTestChannel.State_ = {
/**
* The state for the TestChannel state machine where we making the
* initial call to get the server configured parameters.
*/
INIT: 0,
/**
* The state for the TestChannel state machine where we're checking to
* se if we're behind a buffering proxy.
*/
CONNECTION_TESTING: 1
};
/**
* The state of the state machine for this object.
*
* @private {?BaseTestChannel.State_}
*/
BaseTestChannel.prototype.state_ = null;
/**
* Sets extra HTTP headers to add to all the requests sent to the server.
*
* @param {Object} extraHeaders The HTTP headers.
*/
BaseTestChannel.prototype.setExtraHeaders = function(extraHeaders) {
this.extraHeaders_ = extraHeaders;
};
/**
* Starts the test channel. This initiates connections to the server.
*
* @param {string} path The relative uri for the test connection.
*/
BaseTestChannel.prototype.connect = function(path) {
this.path_ = path;
var sendDataUri = this.channel_.getForwardChannelUri(this.path_);
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_ONE_START);
// If the channel already has the result of the handshake, then skip it.
var handshakeResult = this.channel_.getConnectionState().handshakeResult;
if (handshakeResult != null) {
this.hostPrefix_ = this.channel_.correctHostPrefix(handshakeResult[0]);
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.checkBufferingProxy_();
return;
}
// the first request returns server specific parameters
sendDataUri.setParameterValues('MODE', 'init');
// http-session-id to be generated as the response
if (!this.channel_.getBackgroundChannelTest() &&
this.channel_.getHttpSessionIdParam()) {
sendDataUri.setParameterValues(WebChannel.X_HTTP_SESSION_ID,
this.channel_.getHttpSessionIdParam());
}
this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
this.request_.xmlHttpGet(
sendDataUri, false /* decodeChunks */, null /* hostPrefix */);
this.state_ = BaseTestChannel.State_.INIT;
};
/**
* Begins the second stage of the test channel where we test to see if we're
* behind a buffering proxy. The server sends back a multi-chunked response
* with the first chunk containing the content '1' and then two seconds later
* sending the second chunk containing the content '2'. Depending on how we
* receive the content, we can tell if we're behind a buffering proxy.
* @private
*/
BaseTestChannel.prototype.checkBufferingProxy_ = function() {
this.channelDebug_.debug('TestConnection: starting stage 2');
// If the test result is already available, skip its execution.
var bufferingProxyResult =
this.channel_.getConnectionState().bufferingProxyResult;
if (bufferingProxyResult != null) {
this.channelDebug_.debug(function() {
return 'TestConnection: skipping stage 2, precomputed result is ' +
bufferingProxyResult ?
'Buffered' :
'Unbuffered';
});
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
if (bufferingProxyResult) { // Buffered/Proxy connection
requestStats.notifyStatEvent(requestStats.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
} else { // Unbuffered/NoProxy connection
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
return; // Skip the test
}
this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
var recvDataUri = this.channel_.getBackChannelUri(
this.hostPrefix_,
/** @type {string} */ (this.path_));
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
recvDataUri.setParameterValues('TYPE', 'xmlhttp');
var param = this.channel_.getHttpSessionIdParam();
var value = this.channel_.getHttpSessionId();
if (param && value) {
recvDataUri.setParameterValue(param, value);
}
this.request_.xmlHttpGet(
recvDataUri, false /** decodeChunks */, this.hostPrefix_);
};
/**
* @override
*/
BaseTestChannel.prototype.createXhrIo = function(hostPrefix) {
return this.channel_.createXhrIo(hostPrefix);
};
/**
* Aborts the test channel.
*/
BaseTestChannel.prototype.abort = function() {
if (this.request_) {
this.request_.cancel();
this.request_ = null;
}
this.lastStatusCode_ = -1;
};
/**
* Returns whether the test channel is closed. The ChannelRequest object expects
* this method to be implemented on its handler.
*
* @return {boolean} Whether the channel is closed.
* @override
*/
BaseTestChannel.prototype.isClosed = function() {
return false;
};
/**
* Callback from ChannelRequest for when new data is received
*
* @param {ChannelRequest} req The request object.
* @param {string} responseText The text of the response.
* @override
*/
BaseTestChannel.prototype.onRequestData = function(req, responseText) {
this.lastStatusCode_ = req.getLastStatusCode();
if (this.state_ == BaseTestChannel.State_.INIT) {
this.channelDebug_.debug('TestConnection: Got data for stage 1');
this.applyControlHeaders_(req);
if (!responseText) {
this.channelDebug_.debug('TestConnection: Null responseText');
// The server should always send text; something is wrong here
this.channel_.testConnectionFailure(this, ChannelRequest.Error.BAD_DATA);
return;
}
try {
var channel = /** @type {!goog.labs.net.webChannel.WebChannelBase} */ (
this.channel_);
var respArray = channel.getWireCodec().decodeMessage(responseText);
} catch (e) {
this.channelDebug_.dumpException(e);
this.channel_.testConnectionFailure(this, ChannelRequest.Error.BAD_DATA);
return;
}
this.hostPrefix_ = this.channel_.correctHostPrefix(respArray[0]);
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
if (this.receivedIntermediateResult_) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_DATA_TWO);
} else {
// '11111' is used instead of '1' to prevent a small amount of buffering
// by Safari.
if (responseText == '11111') {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_DATA_ONE);
this.receivedIntermediateResult_ = true;
if (this.checkForEarlyNonBuffered_()) {
// If early chunk detection is on, and we passed the tests,
// assume HTTP_OK, cancel the test and turn on noproxy mode.
this.lastStatusCode_ = 200;
this.request_.cancel();
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
} else {
requestStats.notifyStatEvent(
requestStats.Stat.TEST_STAGE_TWO_DATA_BOTH);
this.receivedIntermediateResult_ = false;
}
}
}
};
/**
* Callback from ChannelRequest that indicates a request has completed.
*
* @param {!ChannelRequest} req The request object.
* @override
*/
BaseTestChannel.prototype.onRequestComplete = function(req) {
this.lastStatusCode_ = this.request_.getLastStatusCode();
if (!this.request_.getSuccess()) {
this.channelDebug_.debug(
'TestConnection: request failed, in state ' + this.state_);
if (this.state_ == BaseTestChannel.State_.INIT) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_ONE_FAILED);
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_FAILED);
}
this.channel_.testConnectionFailure(
this,
/** @type {ChannelRequest.Error} */
(this.request_.getLastError()));
return;
}
if (this.state_ == BaseTestChannel.State_.INIT) {
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.channelDebug_.debug(
'TestConnection: request complete for initial check');
this.checkBufferingProxy_();
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
this.channelDebug_.debug('TestConnection: request complete for stage 2');
var goodConn = this.receivedIntermediateResult_;
if (goodConn) {
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
} else {
this.channelDebug_.debug('Test connection failed; not using streaming');
requestStats.notifyStatEvent(requestStats.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
}
}
};
/**
* Apply any control headers from the initial handshake response.
*
* @param {!ChannelRequest} req The request object.
* @private
*/
BaseTestChannel.prototype.applyControlHeaders_ = function(req) {
if (this.channel_.getBackgroundChannelTest()) {
return;
}
var xhr = req.getXhr();
if (xhr) {
var protocolHeader = xhr.getStreamingResponseHeader(
WebChannel.X_CLIENT_WIRE_PROTOCOL);
this.clientProtocol_ = protocolHeader ? protocolHeader : null;
if (this.channel_.getHttpSessionIdParam()) {
var httpSessionIdHeader = xhr.getStreamingResponseHeader(
WebChannel.X_HTTP_SESSION_ID);
if (httpSessionIdHeader) {
this.channel_.setHttpSessionId(httpSessionIdHeader);
} else {
this.channelDebug_.warning(
'Missing X_HTTP_SESSION_ID in the handshake response');
}
}
}
};
/**
* @return {?string} The client protocol as recorded with the init handshake
* request.
*/
BaseTestChannel.prototype.getClientProtocol = function() {
return this.clientProtocol_;
};
/**
* Returns the last status code received for a request.
* @return {number} The last status code received for a request.
*/
BaseTestChannel.prototype.getLastStatusCode = function() {
return this.lastStatusCode_;
};
/**
* @return {boolean} Whether we should be using secondary domains when the
* server instructs us to do so.
* @override
*/
BaseTestChannel.prototype.shouldUseSecondaryDomains = function() {
return this.channel_.shouldUseSecondaryDomains();
};
/**
* @override
*/
BaseTestChannel.prototype.isActive = function() {
return this.channel_.isActive();
};
/**
* @return {boolean} True if test stage 2 detected a non-buffered
* channel early and early no buffering detection is enabled.
* @private
*/
BaseTestChannel.prototype.checkForEarlyNonBuffered_ = function() {
return ChannelRequest.supportsXhrStreaming();
};
/**
* @override
*/
BaseTestChannel.prototype.getForwardChannelUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getBackChannelUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.correctHostPrefix = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.createDataUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFinished = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getConnectionState = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.setHttpSessionIdParam = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getHttpSessionIdParam = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.setHttpSessionId = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getHttpSessionId = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getBackgroundChannelTest = goog.abstractMethod;
}); // goog.scope