chromium/third_party/google-closure-library/closure/goog/labs/net/webchannel/environment.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview A single module to define user-agent specific environment
 * details.
 *
 */

goog.module('goog.labs.net.webChannel.environment');

goog.module.declareLegacyNamespace();

var userAgent = goog.require('goog.userAgent');


/**
 * The default polling interval in millis for Edge.
 *
 * Currently on edge, new-chunk events may be not be fired (at all) if a new
 * chunk arrives within 50ms following the previous chunk. This may be fixed
 * in future, which requires changes to the whatwg spec too.
 *
 * @private @const {number}
 */
var EDGE_POLLING_INTERVAL_ = 125;


/**
 * History:
 *
 * IE11 is still using Trident, the traditional engine for IE.
 * Edge is using EdgeHTML, a fork of Trident. We are seeing the same issue
 * on IE-11 (reported in 2017), so treat IE the same as Edge for now.
 *
 * We used to do polling for Opera (only) with an 250ms interval, because Opera
 * only fires readyState == INTERACTIVE once. Opera switched to WebKit in 2013,
 * and then to Blink (chrome).
 *
 * TODO(user): check the raw UA string to keep polling for old, mobile operas
 * that may still be affected. For old Opera, double the polling interval
 * to 250ms.
 *
 * @return {boolean} True if polling is required with XHR.
 */
exports.isPollingRequired = function() {
  return userAgent.EDGE_OR_IE;
};


/**
 * How often to poll (in MS) for changes to responseText in browsers that don't
 * fire onreadystatechange during incremental loading of the response body.
 *
 * @return {number|undefined} The polling interval (MS) for the current U-A;
 * or undefined if polling is not supposed to be enabled.
 */
exports.getPollingInterval = function() {
  if (userAgent.EDGE_OR_IE) {
    return EDGE_POLLING_INTERVAL_;
  }

  return undefined;
};

/**
 * Origin trial token for google.com
 *
 * https://developers.chrome.com/origintrials/#/trials
 *
 * http://googlechrome.github.io/OriginTrials/check-token.html
 * Origin: https://google.com:443
 * Matches Subdomains? Yes
 * Matches Third-party? Yes
 * Feature: FetchUploadStreaming
 * Expires: 7/14/2021, 8:59:59 AM
 *
 * Token for googleapis.com will be registered after google.com's is deployed.
 *
 */
const OT_TOKEN_GOOGLE_COM =
    "A70X6iKIlnS3U/OFBWYlZCJ6rRlXum75MZ6pvi68FKsnyeL+XPCA7KWBMeW75d2+xNHMEeFOWjfqMS+34jdvrw4AAAB/eyJvcmlnaW4iOiJodHRwczovL2dvb2dsZS5jb206NDQzIiwiZmVhdHVyZSI6IkZldGNoVXBsb2FkU3RyZWFtaW5nIiwiZXhwaXJ5IjoxNjI2MjIwNzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ==";
/**
 * Creates ReadableStream to upload
 * @return {!ReadableStream} ReadableStream to upload
 */
function createStream() {
  const encoder = new goog.global.TextEncoder();
  return new goog.global.ReadableStream({
    start: controller => {
      for (const obj of ['test\r\n', 'test\r\n']) {
        controller.enqueue(encoder.encode(obj));
      }
      controller.close();
    }
  });
}

/**
 * Detect the user agent is chrome and its version is higher than M90.
 * This code is hard-coded from goog.labs.userAgent.browser to avoid file size
 * increasing.
 * @return {boolean} Whether the above is true.
 */
function isChromeM90OrHigher() {
  const userAgentStr = function() {
    const navigator = goog.global.navigator;
    if (navigator) {
      const userAgent = navigator.userAgent;
      if (userAgent) {
        return userAgent;
      }
    }
    return '';
  }();

  const matchUserAgent = function (str) {
    return userAgentStr.indexOf(str) != -1;
  };

  if (!matchUserAgent('Chrome') || matchUserAgent('Edg')) {
    return false;
  }

  const match = /Chrome\/(\d+)/.exec(userAgentStr);
  const chromeVersion = parseInt(match[1], 10);
  return chromeVersion >= 90;
}

/**
 * Detect the URL origin is *.google.com.
 * @param {string} url The target URL.
 * @return {boolean} Whether the above is true.
 */
function isUrlGoogle(url) {
  const match = /\/\/([^\/]+)\//.exec(url);
  if (!match) {
    return false;
  }
  const origin = match[1];
  return origin.endsWith("google.com");
}

/**
 * The flag to run the origin trials code only once.
 */
let isStartOriginTrialsCalled = false;

/**
 * For Fetch/upload OT, make three requests against the server endpoint.
 * POST requests contain only dummy payload.
 *
 * https://developers.chrome.com/origintrials/#/view_trial/3524066708417413121
 *
 * This function is expected to be called from background during the handshake.
 * Exceptions will be logged by the caller.
 *
 * No stats or logs are collected on the client-side. To be disabled once the
 * OT is expired.
 *
 * @param {string} path The base URL path for the requests
 * @param {function(*)} logError A function to execute when exceptions are
 *     caught.
 */
exports.startOriginTrials = function(path, logError) {
  if (isStartOriginTrialsCalled) {
    return;
  }
  isStartOriginTrialsCalled = true;
  // NE: may need check if path has already contains query params?

  // Accept only Chrome M90 or later due to service worker support.
  if (!isChromeM90OrHigher()) {
    return;
  }

  // Accept only only google.com and subdoamins.
  if(!isUrlGoogle(path)) {
    return;
  }
  // Since 3P OT is not supported yet, we should check the current page matches
  // the path (absolute one?) to disable this OT for cross-origin calls
  if(!window || !window.document || !isUrlGoogle(window.document.URL)) {
    return;
  }

  // Enable origin trial by injecting OT <meta> tag
  const tokenElement =
    /** @type {! HTMLMetaElement} */ (document.createElement('meta'));
  tokenElement.httpEquiv = 'origin-trial';
  tokenElement.content = OT_TOKEN_GOOGLE_COM;
  // appendChild() synchronously enables OT.
  document.head.appendChild(tokenElement);

  // Check if fetch upload stream is actually enabled.
  // By the spec, Streaming request doesn't has the Content-Type header:
  // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
  // If Chrome doesn't support Streaming, the body stream is converted to a
  // string "[object ReadableStream]" for fallback then it has "Content-Type:
  // text/plain;charset=UTF-8".
  const supportsRequestStreams = !new Request('', {
    body: new ReadableStream(),
    method: 'POST',
  }).headers.has('Content-Type');
  if (!supportsRequestStreams) {
    return;
  }

  // 1st req:  path?ot=1
  // non-streaming upload request
  goog.global.fetch(`${path}?ot=1`, {method: 'POST', body: 'test\r\n'})
    .catch(logError);

  // 2nd req:  path?ot=2
  // h2-only streaming upload request
  goog.global.fetch(`${path}?ot=2`, {
    method: 'POST',
    body: createStream(),
    allowHTTP1ForStreamingUpload: false,
  }).catch(logError);

  // 3rd req:  path?ot=3
  // h1-allowed streaming upload request
  goog.global.fetch(`${path}?ot=3`, {
    method: 'POST',
    body: createStream(),
    allowHTTP1ForStreamingUpload: true,
  }).catch(logError);

  // Example calling a Chrome API:
  // goog.global.chrome.loadTimes().wasFetchedViaSpdy
};