chromium/chrome/browser/resources/net_internals/domain_security_policy_view.js

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {$} from 'chrome://resources/js/util.js';

import {BrowserBridge} from './browser_bridge.js';
import {addNode, addNodeWithText, addTextNode} from './util.js';
import {DivView} from './view.js';

/** @type {?DomainSecurityPolicyView} */
let instance = null;

/**
 * This UI allows a user to query and update the browser's list of per-domain
 * security policies. These policies include:
 * - HSTS: HTTPS Strict Transport Security. A way for sites to elect to always
 *   use HTTPS. See https://www.chromium.org/hsts
 * - PKP. A way for sites to pin to specific certification authorities. Only
 * available via manual preloading.
 */

export class DomainSecurityPolicyView extends DivView {
  constructor() {
    // Call superclass's constructor.
    super(DomainSecurityPolicyView.MAIN_BOX_ID);

    this.browserBridge_ = BrowserBridge.getInstance();
    this.deleteInput_ = $(DomainSecurityPolicyView.DELETE_INPUT_ID);
    this.addStsInput_ = $(DomainSecurityPolicyView.ADD_HSTS_INPUT_ID);
    this.addStsCheck_ = $(DomainSecurityPolicyView.ADD_STS_CHECK_ID);
    this.queryStsInput_ = $(DomainSecurityPolicyView.QUERY_HSTS_INPUT_ID);
    this.queryStsOutputDiv_ =
        $(DomainSecurityPolicyView.QUERY_HSTS_OUTPUT_DIV_ID);

    let form = $(DomainSecurityPolicyView.DELETE_FORM_ID);
    form.addEventListener('submit', this.onSubmitDelete_.bind(this), false);

    form = $(DomainSecurityPolicyView.ADD_HSTS_FORM_ID);
    form.addEventListener('submit', this.onSubmitHSTSAdd_.bind(this), false);

    form = $(DomainSecurityPolicyView.QUERY_HSTS_FORM_ID);
    form.addEventListener('submit', this.onSubmitHSTSQuery_.bind(this), false);

    this.hstsObservers_ = [];
  }

  // Test specific functions.
  addHSTSObserverForTest(observer) {
    this.hstsObservers_.push(observer);
  }

  onSubmitDelete_(event) {
    this.browserBridge_.sendDomainSecurityPolicyDelete(
        this.deleteInput_.value.trim());
    this.deleteInput_.value = '';
    event.preventDefault();
  }

  onSubmitHSTSAdd_(event) {
    this.browserBridge_.sendHSTSAdd(
        this.addStsInput_.value.trim(), this.addStsCheck_.checked);
    this.browserBridge_.sendHSTSQuery(this.addStsInput_.value).then(result => {
      this.onHSTSQueryResult_(result);
    });
    this.queryStsInput_.value = this.addStsInput_.value;
    this.addStsCheck_.checked = false;
    this.addStsInput_.value = '';
    event.preventDefault();
  }

  onSubmitHSTSQuery_(event) {
    this.browserBridge_.sendHSTSQuery(this.queryStsInput_.value.trim())
        .then(result => {
          this.onHSTSQueryResult_(result);
        });
    event.preventDefault();
  }

  onHSTSQueryResult_(result) {
    this.queryStsOutputDiv_.innerHTML = trustedTypes.emptyHTML;
    if (result.error !== undefined) {
      const s = addNode(this.queryStsOutputDiv_, 'span');
      s.textContent = result.error;
      s.style.color = '#e00';
    } else if (result.result === false) {
      const notFound = document.createElement('b');
      notFound.textContent = 'Not found';
      this.queryStsOutputDiv_.appendChild(notFound);
    } else {
      const s = addNode(this.queryStsOutputDiv_, 'span');
      const found = document.createElement('b');
      found.textContent = 'Found:';
      s.appendChild(found);
      s.appendChild(document.createElement('br'));

      const keys = [
        'static_sts_domain',
        'static_upgrade_mode',
        'static_sts_include_subdomains',
        'static_sts_observed',
        'static_pkp_domain',
        'static_pkp_include_subdomains',
        'static_pkp_observed',
        'static_spki_hashes',
        'dynamic_sts_domain',
        'dynamic_upgrade_mode',
        'dynamic_sts_include_subdomains',
        'dynamic_sts_observed',
        'dynamic_sts_expiry',
      ];

      const kStaticHashKeys =
          ['public_key_hashes', 'preloaded_spki_hashes', 'static_spki_hashes'];

      const staticHashes = [];
      for (let i = 0; i < kStaticHashKeys.length; ++i) {
        const staticHashValue = result[kStaticHashKeys[i]];
        if (staticHashValue !== undefined && staticHashValue !== '') {
          staticHashes.push(staticHashValue);
        }

        for (let i = 0; i < keys.length; ++i) {
          const key = keys[i];
          const value = result[key];
          addTextNode(this.queryStsOutputDiv_, ' ' + key + ': ');

          // If there are no static_hashes, do not make it seem like there is a
          // static PKP policy in place.
          if (staticHashes.length === 0 && key.startsWith('static_pkp_')) {
            addNode(this.queryStsOutputDiv_, 'br');
            continue;
          }

          if (key === 'static_spki_hashes') {
            addNodeWithText(
                this.queryStsOutputDiv_, 'tt', staticHashes.join(','));
          } else if (key.indexOf('_upgrade_mode') >= 0) {
            addNodeWithText(this.queryStsOutputDiv_, 'tt', modeToString(value));
          } else {
            addNodeWithText(
                this.queryStsOutputDiv_, 'tt',
                value === undefined ? '' : value);
          }
          addNode(this.queryStsOutputDiv_, 'br');
        }
      }
    }

    highlightFade(this.queryStsOutputDiv_);
    for (const observer of this.hstsObservers_) {
      observer.onHSTSQueryResult(result);
    }
  }

  static getInstance() {
    return instance || (instance = new DomainSecurityPolicyView());
  }
}

function modeToString(m) {
  // These numbers must match those in
  // TransportSecurityState::STSState::UpgradeMode.
  if (m === 0) {
    return 'FORCE_HTTPS';
  } else if (m === 1) {
    return 'DEFAULT';
  } else {
    return 'UNKNOWN';
  }
}

function highlightFade(element) {
  element.style.transitionProperty = 'background-color';
  element.style.transitionDuration = '0ms';
  element.style.backgroundColor = 'var(--color-highlight)';
  setTimeout(function() {
    element.style.transitionDuration = '1s';
    element.style.backgroundColor = 'var(--color-background)';
  }, 0);
}

DomainSecurityPolicyView.TAB_ID = 'tab-handle-domain-security-policy';
DomainSecurityPolicyView.TAB_NAME = 'Domain Security Policy';
// This tab was originally limited to HSTS. Even though it now encompasses
// domain security policy more broadly, keep the hash as "#hsts" to preserve
// links/documentation that directs users to chrome://net-internals#hsts.
DomainSecurityPolicyView.TAB_HASH = '#hsts';

// IDs for special HTML elements in domain_security_policy_view.html
DomainSecurityPolicyView.MAIN_BOX_ID =
    'domain-security-policy-view-tab-content';
DomainSecurityPolicyView.DELETE_INPUT_ID =
    'domain-security-policy-view-delete-input';
DomainSecurityPolicyView.DELETE_FORM_ID =
    'domain-security-policy-view-delete-form';
DomainSecurityPolicyView.DELETE_SUBMIT_ID =
    'domain-security-policy-view-delete-submit';
// HSTS form elements
DomainSecurityPolicyView.ADD_HSTS_INPUT_ID = 'hsts-view-add-input';
DomainSecurityPolicyView.ADD_STS_CHECK_ID = 'hsts-view-check-sts-input';
DomainSecurityPolicyView.ADD_PINS_ID = 'hsts-view-add-pins';
DomainSecurityPolicyView.ADD_HSTS_FORM_ID = 'hsts-view-add-form';
DomainSecurityPolicyView.ADD_HSTS_SUBMIT_ID = 'hsts-view-add-submit';
DomainSecurityPolicyView.QUERY_HSTS_INPUT_ID = 'hsts-view-query-input';
DomainSecurityPolicyView.QUERY_HSTS_OUTPUT_DIV_ID = 'hsts-view-query-output';
DomainSecurityPolicyView.QUERY_HSTS_FORM_ID = 'hsts-view-query-form';
DomainSecurityPolicyView.QUERY_HSTS_SUBMIT_ID = 'hsts-view-query-submit';