Path: blob/trunk/third_party/closure/goog/net/browsertestchannel.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Definition of the BrowserTestChannel class. A16* BrowserTestChannel is used during the first part of channel negotiation17* with the server to create the channel. It helps us determine whether we're18* behind a buffering proxy. It also runs the logic to see if the channel19* has been blocked by a network administrator. This class is part of the20* BrowserChannel implementation and is not for use by normal application code.21*22*/23242526goog.provide('goog.net.BrowserTestChannel');2728goog.require('goog.json.NativeJsonProcessor');29goog.require('goog.net.ChannelRequest');30goog.require('goog.net.ChannelRequest.Error');31goog.require('goog.net.tmpnetwork');32goog.require('goog.string.Parser');33343536/**37* Encapsulates the logic for a single BrowserTestChannel.38*39* @constructor40* @param {goog.net.BrowserChannel} channel The BrowserChannel that owns this41* test channel.42* @param {goog.net.ChannelDebug} channelDebug A ChannelDebug to use for43* logging.44* @final45*/46goog.net.BrowserTestChannel = function(channel, channelDebug) {47/**48* The BrowserChannel that owns this test channel49* @type {goog.net.BrowserChannel}50* @private51*/52this.channel_ = channel;5354/**55* The channel debug to use for logging56* @type {goog.net.ChannelDebug}57* @private58*/59this.channelDebug_ = channelDebug;6061/**62* Parser for a response payload. The parser should return an array.63* @type {goog.string.Parser}64* @private65*/66this.parser_ = new goog.json.NativeJsonProcessor();67};686970/**71* Extra HTTP headers to add to all the requests sent to the server.72* @type {Object}73* @private74*/75goog.net.BrowserTestChannel.prototype.extraHeaders_ = null;767778/**79* The test request.80* @type {goog.net.ChannelRequest}81* @private82*/83goog.net.BrowserTestChannel.prototype.request_ = null;848586/**87* Whether we have received the first result as an intermediate result. This88* helps us determine whether we're behind a buffering proxy.89* @type {boolean}90* @private91*/92goog.net.BrowserTestChannel.prototype.receivedIntermediateResult_ = false;939495/**96* The time when the test request was started. We use timing in IE as97* a heuristic for whether we're behind a buffering proxy.98* @type {?number}99* @private100*/101goog.net.BrowserTestChannel.prototype.startTime_ = null;102103104/**105* The time for of the first result part. We use timing in IE as a106* heuristic for whether we're behind a buffering proxy.107* @type {?number}108* @private109*/110goog.net.BrowserTestChannel.prototype.firstTime_ = null;111112113/**114* The time for of the last result part. We use timing in IE as a115* heuristic for whether we're behind a buffering proxy.116* @type {?number}117* @private118*/119goog.net.BrowserTestChannel.prototype.lastTime_ = null;120121122/**123* The relative path for test requests.124* @type {?string}125* @private126*/127goog.net.BrowserTestChannel.prototype.path_ = null;128129130/**131* The state of the state machine for this object.132*133* @type {?number}134* @private135*/136goog.net.BrowserTestChannel.prototype.state_ = null;137138139/**140* The last status code received.141* @type {number}142* @private143*/144goog.net.BrowserTestChannel.prototype.lastStatusCode_ = -1;145146147/**148* A subdomain prefix for using a subdomain in IE for the backchannel149* requests.150* @type {?string}151* @private152*/153goog.net.BrowserTestChannel.prototype.hostPrefix_ = null;154155156/**157* A subdomain prefix for testing whether the channel was disabled by158* a network administrator;159* @type {?string}160* @private161*/162goog.net.BrowserTestChannel.prototype.blockedPrefix_ = null;163164165/**166* Enum type for the browser test channel state machine167* @enum {number}168* @private169*/170goog.net.BrowserTestChannel.State_ = {171/**172* The state for the BrowserTestChannel state machine where we making the173* initial call to get the server configured parameters.174*/175INIT: 0,176177/**178* The state for the BrowserTestChannel state machine where we're checking to179* see if the channel has been blocked.180*/181CHECKING_BLOCKED: 1,182183/**184* The state for the BrowserTestChannel state machine where we're checking to185* se if we're behind a buffering proxy.186*/187CONNECTION_TESTING: 2188};189190191/**192* Time in MS for waiting for the request to see if the channel is blocked.193* If the response takes longer than this many ms, we assume the request has194* failed.195* @type {number}196* @private197*/198goog.net.BrowserTestChannel.BLOCKED_TIMEOUT_ = 5000;199200201/**202* Number of attempts to try to see if the check to see if we're blocked203* succeeds. Sometimes the request can fail because of flaky network conditions204* and checking multiple times reduces false positives.205* @type {number}206* @private207*/208goog.net.BrowserTestChannel.BLOCKED_RETRIES_ = 3;209210211/**212* Time in ms between retries of the blocked request213* @type {number}214* @private215*/216goog.net.BrowserTestChannel.BLOCKED_PAUSE_BETWEEN_RETRIES_ = 2000;217218219/**220* Time between chunks in the test connection that indicates that we221* are not behind a buffering proxy. This value should be less than or222* equals to the time between chunks sent from the server.223* @type {number}224* @private225*/226goog.net.BrowserTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_ = 500;227228229/**230* Sets extra HTTP headers to add to all the requests sent to the server.231*232* @param {Object} extraHeaders The HTTP headers.233*/234goog.net.BrowserTestChannel.prototype.setExtraHeaders = function(extraHeaders) {235this.extraHeaders_ = extraHeaders;236};237238239/**240* Sets a new parser for the response payload.241* @param {!goog.string.Parser} parser Parser.242*/243goog.net.BrowserTestChannel.prototype.setParser = function(parser) {244this.parser_ = parser;245};246247248/**249* Starts the test channel. This initiates connections to the server.250*251* @param {string} path The relative uri for the test connection.252*/253goog.net.BrowserTestChannel.prototype.connect = function(path) {254this.path_ = path;255var sendDataUri = this.channel_.getForwardChannelUri(this.path_);256257goog.net.BrowserChannel.notifyStatEvent(258goog.net.BrowserChannel.Stat.TEST_STAGE_ONE_START);259this.startTime_ = goog.now();260261// If the channel already has the result of the first test, then skip it.262var firstTestResults = this.channel_.getFirstTestResults();263if (goog.isDefAndNotNull(firstTestResults)) {264this.hostPrefix_ = this.channel_.correctHostPrefix(firstTestResults[0]);265this.blockedPrefix_ = firstTestResults[1];266if (this.blockedPrefix_) {267this.state_ = goog.net.BrowserTestChannel.State_.CHECKING_BLOCKED;268this.checkBlocked_();269} else {270this.state_ = goog.net.BrowserTestChannel.State_.CONNECTION_TESTING;271this.connectStage2_();272}273return;274}275276// the first request returns server specific parameters277sendDataUri.setParameterValues('MODE', 'init');278this.request_ =279goog.net.BrowserChannel.createChannelRequest(this, this.channelDebug_);280this.request_.setExtraHeaders(this.extraHeaders_);281this.request_.xmlHttpGet(282sendDataUri, false /* decodeChunks */, null /* hostPrefix */,283true /* opt_noClose */);284this.state_ = goog.net.BrowserTestChannel.State_.INIT;285};286287288/**289* Checks to see whether the channel is blocked. This is for implementing the290* feature that allows network administrators to block Gmail Chat. The291* strategy to determine if we're blocked is to try to load an image off a292* special subdomain that network administrators will block access to if they293* are trying to block chat. For Gmail Chat, the subdomain is294* chatenabled.mail.google.com.295* @private296*/297goog.net.BrowserTestChannel.prototype.checkBlocked_ = function() {298var uri = this.channel_.createDataUri(299this.blockedPrefix_, '/mail/images/cleardot.gif');300uri.makeUnique();301goog.net.tmpnetwork.testLoadImageWithRetries(302uri.toString(), goog.net.BrowserTestChannel.BLOCKED_TIMEOUT_,303goog.bind(this.checkBlockedCallback_, this),304goog.net.BrowserTestChannel.BLOCKED_RETRIES_,305goog.net.BrowserTestChannel.BLOCKED_PAUSE_BETWEEN_RETRIES_);306this.notifyServerReachabilityEvent(307goog.net.BrowserChannel.ServerReachability.REQUEST_MADE);308};309310311/**312* Callback for testLoadImageWithRetries to check if browser channel is313* blocked.314* @param {boolean} succeeded Whether the request succeeded.315* @private316*/317goog.net.BrowserTestChannel.prototype.checkBlockedCallback_ = function(318succeeded) {319if (succeeded) {320this.state_ = goog.net.BrowserTestChannel.State_.CONNECTION_TESTING;321this.connectStage2_();322} else {323goog.net.BrowserChannel.notifyStatEvent(324goog.net.BrowserChannel.Stat.CHANNEL_BLOCKED);325this.channel_.testConnectionBlocked(this);326}327328// We don't dispatch a REQUEST_FAILED server reachability event when the329// block request fails, as such a failure is not a good signal that the330// server has actually become unreachable.331if (succeeded) {332this.notifyServerReachabilityEvent(333goog.net.BrowserChannel.ServerReachability.REQUEST_SUCCEEDED);334}335};336337338/**339* Begins the second stage of the test channel where we test to see if we're340* behind a buffering proxy. The server sends back a multi-chunked response341* with the first chunk containing the content '1' and then two seconds later342* sending the second chunk containing the content '2'. Depending on how we343* receive the content, we can tell if we're behind a buffering proxy.344* @private345* @suppress {missingRequire} goog.net.BrowserChannel346*/347goog.net.BrowserTestChannel.prototype.connectStage2_ = function() {348this.channelDebug_.debug('TestConnection: starting stage 2');349350// If the second test results are available, skip its execution.351var secondTestResults = this.channel_.getSecondTestResults();352if (goog.isDefAndNotNull(secondTestResults)) {353this.channelDebug_.debug(354'TestConnection: skipping stage 2, precomputed result is ' +355secondTestResults ?356'Buffered' :357'Unbuffered');358goog.net.BrowserChannel.notifyStatEvent(359goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_START);360if (secondTestResults) { // Buffered/Proxy connection361goog.net.BrowserChannel.notifyStatEvent(362goog.net.BrowserChannel.Stat.PROXY);363this.channel_.testConnectionFinished(this, false);364} else { // Unbuffered/NoProxy connection365goog.net.BrowserChannel.notifyStatEvent(366goog.net.BrowserChannel.Stat.NOPROXY);367this.channel_.testConnectionFinished(this, true);368}369return; // Skip the test370}371/** @private @suppress {missingRequire} Circular dep. */372this.request_ =373goog.net.BrowserChannel.createChannelRequest(this, this.channelDebug_);374this.request_.setExtraHeaders(this.extraHeaders_);375var recvDataUri = this.channel_.getBackChannelUri(376this.hostPrefix_,377/** @type {string} */ (this.path_));378379goog.net.BrowserChannel.notifyStatEvent(380goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_START);381if (!goog.net.ChannelRequest.supportsXhrStreaming()) {382recvDataUri.setParameterValues('TYPE', 'html');383this.request_.tridentGet(recvDataUri, Boolean(this.hostPrefix_));384} else {385recvDataUri.setParameterValues('TYPE', 'xmlhttp');386this.request_.xmlHttpGet(387recvDataUri, false /** decodeChunks */, this.hostPrefix_,388false /** opt_noClose */);389}390};391392393/**394* Factory method for XhrIo objects.395* @param {?string} hostPrefix The host prefix, if we need an XhrIo object396* capable of calling a secondary domain.397* @return {!goog.net.XhrIo} New XhrIo object.398*/399goog.net.BrowserTestChannel.prototype.createXhrIo = function(hostPrefix) {400return this.channel_.createXhrIo(hostPrefix);401};402403404/**405* Aborts the test channel.406*/407goog.net.BrowserTestChannel.prototype.abort = function() {408if (this.request_) {409this.request_.cancel();410this.request_ = null;411}412this.lastStatusCode_ = -1;413};414415416/**417* Returns whether the test channel is closed. The ChannelRequest object expects418* this method to be implemented on its handler.419*420* @return {boolean} Whether the channel is closed.421*/422goog.net.BrowserTestChannel.prototype.isClosed = function() {423return false;424};425426427/**428* Callback from ChannelRequest for when new data is received429*430* @param {goog.net.ChannelRequest} req The request object.431* @param {string} responseText The text of the response.432*/433goog.net.BrowserTestChannel.prototype.onRequestData = function(434req, responseText) {435this.lastStatusCode_ = req.getLastStatusCode();436if (this.state_ == goog.net.BrowserTestChannel.State_.INIT) {437this.channelDebug_.debug('TestConnection: Got data for stage 1');438if (!responseText) {439this.channelDebug_.debug('TestConnection: Null responseText');440// The server should always send text; something is wrong here441this.channel_.testConnectionFailure(442this, goog.net.ChannelRequest.Error.BAD_DATA);443return;444}445446try {447var respArray = this.parser_.parse(responseText);448} catch (e) {449this.channelDebug_.dumpException(e);450this.channel_.testConnectionFailure(451this, goog.net.ChannelRequest.Error.BAD_DATA);452return;453}454this.hostPrefix_ = this.channel_.correctHostPrefix(respArray[0]);455this.blockedPrefix_ = respArray[1];456} else if (457this.state_ == goog.net.BrowserTestChannel.State_.CONNECTION_TESTING) {458if (this.receivedIntermediateResult_) {459goog.net.BrowserChannel.notifyStatEvent(460goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_DATA_TWO);461this.lastTime_ = goog.now();462} else {463// '11111' is used instead of '1' to prevent a small amount of buffering464// by Safari.465if (responseText == '11111') {466goog.net.BrowserChannel.notifyStatEvent(467goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_DATA_ONE);468this.receivedIntermediateResult_ = true;469this.firstTime_ = goog.now();470if (this.checkForEarlyNonBuffered_()) {471// If early chunk detection is on, and we passed the tests,472// assume HTTP_OK, cancel the test and turn on noproxy mode.473this.lastStatusCode_ = 200;474this.request_.cancel();475this.channelDebug_.debug(476'Test connection succeeded; using streaming connection');477goog.net.BrowserChannel.notifyStatEvent(478goog.net.BrowserChannel.Stat.NOPROXY);479this.channel_.testConnectionFinished(this, true);480}481} else {482goog.net.BrowserChannel.notifyStatEvent(483goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_DATA_BOTH);484this.firstTime_ = this.lastTime_ = goog.now();485this.receivedIntermediateResult_ = false;486}487}488}489};490491492/**493* Callback from ChannelRequest that indicates a request has completed.494*495* @param {goog.net.ChannelRequest} req The request object.496* @suppress {missingRequire} Cannot depend on goog.net.BrowserChannel because497* it creates a circular dependency.498*/499goog.net.BrowserTestChannel.prototype.onRequestComplete = function(req) {500this.lastStatusCode_ = this.request_.getLastStatusCode();501if (!this.request_.getSuccess()) {502this.channelDebug_.debug(503'TestConnection: request failed, in state ' + this.state_);504if (this.state_ == goog.net.BrowserTestChannel.State_.INIT) {505goog.net.BrowserChannel.notifyStatEvent(506goog.net.BrowserChannel.Stat.TEST_STAGE_ONE_FAILED);507} else if (508this.state_ == goog.net.BrowserTestChannel.State_.CONNECTION_TESTING) {509goog.net.BrowserChannel.notifyStatEvent(510goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_FAILED);511}512this.channel_.testConnectionFailure(513this,514/** @type {goog.net.ChannelRequest.Error} */515(this.request_.getLastError()));516return;517}518519if (this.state_ == goog.net.BrowserTestChannel.State_.INIT) {520this.channelDebug_.debug(521'TestConnection: request complete for initial check');522if (this.blockedPrefix_) {523this.state_ = goog.net.BrowserTestChannel.State_.CHECKING_BLOCKED;524this.checkBlocked_();525} else {526this.state_ = goog.net.BrowserTestChannel.State_.CONNECTION_TESTING;527this.connectStage2_();528}529} else if (530this.state_ == goog.net.BrowserTestChannel.State_.CONNECTION_TESTING) {531this.channelDebug_.debug('TestConnection: request complete for stage 2');532var goodConn = false;533534if (!goog.net.ChannelRequest.supportsXhrStreaming()) {535// we always get Trident responses in separate calls to536// onRequestData, so we have to check the time they came537var ms = this.lastTime_ - this.firstTime_;538if (ms < 200) {539// TODO: need to empirically verify that this number is OK540// for slow computers541goodConn = false;542} else {543goodConn = true;544}545} else {546goodConn = this.receivedIntermediateResult_;547}548549if (goodConn) {550this.channelDebug_.debug(551'Test connection succeeded; using streaming connection');552goog.net.BrowserChannel.notifyStatEvent(553goog.net.BrowserChannel.Stat.NOPROXY);554this.channel_.testConnectionFinished(this, true);555} else {556this.channelDebug_.debug('Test connection failed; not using streaming');557/** @suppress {missingRequire} Circular dep */558goog.net.BrowserChannel.notifyStatEvent(559goog.net.BrowserChannel.Stat.PROXY);560this.channel_.testConnectionFinished(this, false);561}562}563};564565566/**567* Returns the last status code received for a request.568* @return {number} The last status code received for a request.569*/570goog.net.BrowserTestChannel.prototype.getLastStatusCode = function() {571return this.lastStatusCode_;572};573574575/**576* @return {boolean} Whether we should be using secondary domains when the577* server instructs us to do so.578*/579goog.net.BrowserTestChannel.prototype.shouldUseSecondaryDomains = function() {580return this.channel_.shouldUseSecondaryDomains();581};582583584/**585* Gets whether this channel is currently active. This is used to determine the586* length of time to wait before retrying.587*588* @param {goog.net.BrowserChannel} browserChannel The browser channel.589* @return {boolean} Whether the channel is currently active.590*/591goog.net.BrowserTestChannel.prototype.isActive = function(browserChannel) {592return this.channel_.isActive();593};594595596/**597* @return {boolean} True if test stage 2 detected a non-buffered598* channel early and early no buffering detection is enabled.599* @private600*/601goog.net.BrowserTestChannel.prototype.checkForEarlyNonBuffered_ = function() {602var ms = this.firstTime_ - this.startTime_;603604// we always get Trident responses in separate calls to605// onRequestData, so we have to check the time that the first came in606// and verify that the data arrived before the second portion could607// have been sent. For all other browser's we skip the timing test.608return goog.net.ChannelRequest.supportsXhrStreaming() ||609ms < goog.net.BrowserTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_;610};611612613/**614* Notifies the channel of a fine grained network event.615* @param {goog.net.BrowserChannel.ServerReachability} reachabilityType The616* reachability event type.617*/618goog.net.BrowserTestChannel.prototype.notifyServerReachabilityEvent = function(619reachabilityType) {620this.channel_.notifyServerReachabilityEvent(reachabilityType);621};622623624