| // 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.require('goog.labs.net.webChannel.Channel'); |
| goog.require('goog.labs.net.webChannel.ChannelRequest'); |
| goog.require('goog.labs.net.webChannel.requestStats'); |
| goog.require('goog.labs.net.webChannel.requestStats.Stat'); |
| |
| |
| |
| /** |
| * 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 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 (goog.isDefAndNotNull(handshakeResult)) { |
| 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'); |
| this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_); |
| this.request_.setExtraHeaders(this.extraHeaders_); |
| this.request_.xmlHttpGet(sendDataUri, false /* decodeChunks */, |
| null /* hostPrefix */, true /* opt_noClose */); |
| 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 (goog.isDefAndNotNull(bufferingProxyResult)) { |
| this.channelDebug_.debug( |
| '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'); |
| this.request_.xmlHttpGet(recvDataUri, false /** decodeChunks */, |
| this.hostPrefix_, false /** opt_noClose */); |
| }; |
| |
| |
| /** |
| * @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'); |
| 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; |
| } |
| /** @preserveTry */ |
| try { |
| var respArray = this.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.recordClientProtocol_(req); |
| 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); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Record the client protocol header from the initial handshake response. |
| * |
| * @param {!ChannelRequest} req The request object. |
| * @private |
| */ |
| BaseTestChannel.prototype.recordClientProtocol_ = function(req) { |
| var xmlHttp = req.getXhr(); |
| if (xmlHttp) { |
| var protocolHeader = xmlHttp.getResponseHeader('x-client-wire-protocol'); |
| this.clientProtocol_ = protocolHeader ? protocolHeader : null; |
| } |
| }; |
| |
| |
| /** |
| * @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; |
| }); // goog.scope |