UNPKG

chrome-devtools-frontend

Version:
1,228 lines (1,104 loc) • 75.5 kB
// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import type * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import * as NetworkForward from '../../panels/network/forward/forward.js'; import * as UI from '../../ui/legacy/legacy.js'; import lockIconStyles from './lockIcon.css.js'; import mainViewStyles from './mainView.css.js'; import originViewStyles from './originView.css.js'; import sidebarStyles from './sidebar.css.js'; import { Events, SecurityModel, SecurityStyleExplanation, SummaryMessages, type PageVisibleSecurityState, } from './SecurityModel.js'; const UIStrings = { /** *@description Title text content in Security Panel of the Security panel */ overview: 'Overview', /** *@description Text in Security Panel of the Security panel */ mainOrigin: 'Main origin', /** *@description Text in Security Panel of the Security panel */ nonsecureOrigins: 'Non-secure origins', /** *@description Text in Security Panel of the Security panel */ secureOrigins: 'Secure origins', /** *@description Text in Security Panel of the Security panel */ unknownCanceled: 'Unknown / canceled', /** *@description Text in Security Panel of the Security panel */ reloadToViewDetails: 'Reload to view details', /** *@description New parent title in Security Panel of the Security panel */ mainOriginSecure: 'Main origin (secure)', /** *@description New parent title in Security Panel of the Security panel */ mainOriginNonsecure: 'Main origin (non-secure)', /** *@description Summary div text content in Security Panel of the Security panel */ securityOverview: 'Security overview', /** *@description Text to show something is secure */ secure: 'Secure', /** *@description Sdk console message message level info of level Labels in Console View of the Console panel */ info: 'Info', /** *@description Not secure div text content in Security Panel of the Security panel */ notSecure: 'Not secure', /** *@description Text to view a security certificate */ viewCertificate: 'View certificate', /** *@description Text in Security Panel of the Security panel */ notSecureBroken: 'Not secure (broken)', /** *@description Main summary for page when it has been deemed unsafe by the SafeBrowsing service. */ thisPageIsDangerousFlaggedBy: 'This page is dangerous (flagged by Google Safe Browsing).', /** *@description Summary phrase for a security problem where the site is deemed unsafe by the SafeBrowsing service. */ flaggedByGoogleSafeBrowsing: 'Flagged by Google Safe Browsing', /** *@description Description of a security problem where the site is deemed unsafe by the SafeBrowsing service. */ toCheckThisPagesStatusVisit: 'To check this page\'s status, visit g.co/safebrowsingstatus.', /** *@description Main summary for a non cert error page. */ thisIsAnErrorPage: 'This is an error page.', /** *@description Main summary for where the site is non-secure HTTP. */ thisPageIsInsecureUnencrypted: 'This page is insecure (unencrypted HTTP).', /** *@description Main summary for where the site has a non-cryptographic secure origin. */ thisPageHasANonhttpsSecureOrigin: 'This page has a non-HTTPS secure origin.', /** *@description Message to display in devtools security tab when the page you are on triggered a safety tip. */ thisPageIsSuspicious: 'This page is suspicious', /** *@description Body of message to display in devtools security tab when you are viewing a page that triggered a safety tip. */ chromeHasDeterminedThatThisSiteS: 'Chrome has determined that this site could be fake or fraudulent.', /** *@description Second part of the body of message to display in devtools security tab when you are viewing a page that triggered a safety tip. */ ifYouBelieveThisIsShownIn: 'If you believe this is shown in error please visit https://g.co/chrome/lookalike-warnings.', /** *@description Summary of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. */ possibleSpoofingUrl: 'Possible spoofing URL', /** *@description Body of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. *@example {wikipedia.org} PH1 */ thisSitesHostnameLooksSimilarToP: 'This site\'s hostname looks similar to {PH1}. Attackers sometimes mimic sites by making small, hard-to-see changes to the domain name.', /** *@description second part of body of a warning when the user visits a page that triggered a Safety Tip because the domain looked like another domain. */ ifYouBelieveThisIsShownInErrorSafety: 'If you believe this is shown in error please visit https://g.co/chrome/lookalike-warnings.', /** *@description Title of the devtools security tab when the page you are on triggered a safety tip. */ thisPageIsSuspiciousFlaggedBy: 'This page is suspicious (flagged by Chrome).', /** *@description Text for a security certificate */ certificate: 'Certificate', /** *@description Summary phrase for a security problem where the site's certificate chain contains a SHA1 signature. */ insecureSha: 'insecure (SHA-1)', /** *@description Description of a security problem where the site's certificate chain contains a SHA1 signature. */ theCertificateChainForThisSite: 'The certificate chain for this site contains a certificate signed using SHA-1.', /** *@description Summary phrase for a security problem where the site's certificate is missing a subjectAltName extension. */ subjectAlternativeNameMissing: '`Subject Alternative Name` missing', /** *@description Description of a security problem where the site's certificate is missing a subjectAltName extension. */ theCertificateForThisSiteDoesNot: 'The certificate for this site does not contain a `Subject Alternative Name` extension containing a domain name or IP address.', /** *@description Summary phrase for a security problem with the site's certificate. */ missing: 'missing', /** *@description Description of a security problem with the site's certificate. *@example {net::ERR_CERT_AUTHORITY_INVALID} PH1 */ thisSiteIsMissingAValidTrusted: 'This site is missing a valid, trusted certificate ({PH1}).', /** *@description Summary phrase for a site that has a valid server certificate. */ validAndTrusted: 'valid and trusted', /** *@description Description of a site that has a valid server certificate. *@example {Let's Encrypt Authority X3} PH1 */ theConnectionToThisSiteIsUsingA: 'The connection to this site is using a valid, trusted server certificate issued by {PH1}.', /** *@description Summary phrase for a security state where Private Key Pinning is ignored because the certificate chains to a locally-trusted root. */ publickeypinningBypassed: 'Public-Key-Pinning bypassed', /** *@description Description of a security state where Private Key Pinning is ignored because the certificate chains to a locally-trusted root. */ publickeypinningWasBypassedByA: 'Public-Key-Pinning was bypassed by a local root certificate.', /** *@description Summary phrase for a site with a certificate that is expiring soon. */ certificateExpiresSoon: 'Certificate expires soon', /** *@description Description for a site with a certificate that is expiring soon. */ theCertificateForThisSiteExpires: 'The certificate for this site expires in less than 48 hours and needs to be renewed.', /** *@description Text that refers to the network connection */ connection: 'Connection', /** *@description Summary phrase for a site that uses a modern, secure TLS protocol and cipher. */ secureConnectionSettings: 'secure connection settings', /** *@description Description of a site's TLS settings. *@example {TLS 1.2} PH1 *@example {ECDHE_RSA} PH2 *@example {AES_128_GCM} PH3 */ theConnectionToThisSiteIs: 'The connection to this site is encrypted and authenticated using {PH1}, {PH2}, and {PH3}.', /** *@description A recommendation to the site owner to use a modern TLS protocol *@example {TLS 1.0} PH1 */ sIsObsoleteEnableTlsOrLater: '{PH1} is obsolete. Enable TLS 1.2 or later.', /** *@description A recommendation to the site owner to use a modern TLS key exchange */ rsaKeyExchangeIsObsoleteEnableAn: 'RSA key exchange is obsolete. Enable an ECDHE-based cipher suite.', /** *@description A recommendation to the site owner to use a modern TLS cipher *@example {3DES_EDE_CBC} PH1 */ sIsObsoleteEnableAnAesgcmbased: '{PH1} is obsolete. Enable an AES-GCM-based cipher suite.', /** *@description A recommendation to the site owner to use a modern TLS server signature */ theServerSignatureUsesShaWhichIs: 'The server signature uses SHA-1, which is obsolete. Enable a SHA-2 signature algorithm instead. (Note this is different from the signature in the certificate.)', /** *@description Summary phrase for a site that uses an outdated SSL settings (protocol, key exchange, or cipher). */ obsoleteConnectionSettings: 'obsolete connection settings', /** *@description A title of the 'Resources' action category */ resources: 'Resources', /** *@description Summary for page when there is active mixed content */ activeMixedContent: 'active mixed content', /** *@description Description for page when there is active mixed content */ youHaveRecentlyAllowedNonsecure: 'You have recently allowed non-secure content (such as scripts or iframes) to run on this site.', /** *@description Summary for page when there is mixed content */ mixedContent: 'mixed content', /** *@description Description for page when there is mixed content */ thisPageIncludesHttpResources: 'This page includes HTTP resources.', /** *@description Summary for page when there is a non-secure form */ nonsecureForm: 'non-secure form', /** *@description Description for page when there is a non-secure form */ thisPageIncludesAFormWithA: 'This page includes a form with a non-secure "action" attribute.', /** *@description Summary for the page when it contains active content with certificate error */ activeContentWithCertificate: 'active content with certificate errors', /** *@description Description for the page when it contains active content with certificate error */ youHaveRecentlyAllowedContent: 'You have recently allowed content loaded with certificate errors (such as scripts or iframes) to run on this site.', /** *@description Summary for page when there is active content with certificate errors */ contentWithCertificateErrors: 'content with certificate errors', /** *@description Description for page when there is content with certificate errors */ thisPageIncludesResourcesThat: 'This page includes resources that were loaded with certificate errors.', /** *@description Summary for page when all resources are served securely */ allServedSecurely: 'all served securely', /** *@description Description for page when all resources are served securely */ allResourcesOnThisPageAreServed: 'All resources on this page are served securely.', /** *@description Text in Security Panel of the Security panel */ blockedMixedContent: 'Blocked mixed content', /** *@description Text in Security Panel of the Security panel */ yourPageRequestedNonsecure: 'Your page requested non-secure resources that were blocked.', /** *@description Refresh prompt text content in Security Panel of the Security panel */ reloadThePageToRecordRequestsFor: 'Reload the page to record requests for HTTP resources.', /** * @description Link text in the Security Panel. Clicking the link navigates the user to the * Network panel. Requests refers to network requests. Each request is a piece of data transmitted * from the current user's browser to a remote server. */ viewDRequestsInNetworkPanel: '{n, plural, =1 {View # request in Network Panel} other {View # requests in Network Panel}}', /** *@description Text for the origin of something */ origin: 'Origin', /** *@description Text in Security Panel of the Security panel */ viewRequestsInNetworkPanel: 'View requests in Network Panel', /** *@description Text for security or network protocol */ protocol: 'Protocol', /** *@description Text in the Security panel that refers to how the TLS handshake *established encryption keys. */ keyExchange: 'Key exchange', /** *@description Text in Security Panel that refers to how the TLS handshake *encrypted data. */ cipher: 'Cipher', /** *@description Text in Security Panel that refers to the signature algorithm *used by the server for authenticate in the TLS handshake. */ serverSignature: 'Server signature', /** *@description Text in Security Panel that refers to whether the ClientHello *message in the TLS handshake was encrypted. */ encryptedClientHello: 'Encrypted ClientHello', /** *@description Sct div text content in Security Panel of the Security panel */ certificateTransparency: 'Certificate Transparency', /** *@description Text that refers to the subject of a security certificate */ subject: 'Subject', /** *@description Text to show since when an item is valid */ validFrom: 'Valid from', /** *@description Text to indicate the expiry date */ validUntil: 'Valid until', /** *@description Text for the issuer of an item */ issuer: 'Issuer', /** *@description Text in Security Panel of the Security panel */ openFullCertificateDetails: 'Open full certificate details', /** *@description Text in Security Panel of the Security panel */ sct: 'SCT', /** *@description Text in Security Panel of the Security panel */ logName: 'Log name', /** *@description Text in Security Panel of the Security panel */ logId: 'Log ID', /** *@description Text in Security Panel of the Security panel */ validationStatus: 'Validation status', /** *@description Text for the source of something */ source: 'Source', /** * @description Label for a date/time string in the Security panel. It indicates the time at which * a security certificate was issued (created by an authority and distributed). */ issuedAt: 'Issued at', /** *@description Text in Security Panel of the Security panel */ hashAlgorithm: 'Hash algorithm', /** *@description Text in Security Panel of the Security panel */ signatureAlgorithm: 'Signature algorithm', /** *@description Text in Security Panel of the Security panel */ signatureData: 'Signature data', /** *@description Toggle scts details link text content in Security Panel of the Security panel */ showFullDetails: 'Show full details', /** *@description Toggle scts details link text content in Security Panel of the Security panel */ hideFullDetails: 'Hide full details', /** *@description Text in Security Panel of the Security panel */ thisRequestCompliesWithChromes: 'This request complies with `Chrome`\'s Certificate Transparency policy.', /** *@description Text in Security Panel of the Security panel */ thisRequestDoesNotComplyWith: 'This request does not comply with `Chrome`\'s Certificate Transparency policy.', /** *@description Text in Security Panel of the Security panel */ thisResponseWasLoadedFromCache: 'This response was loaded from cache. Some security details might be missing.', /** *@description Text in Security Panel of the Security panel */ theSecurityDetailsAboveAreFrom: 'The security details above are from the first inspected response.', /** *@description Main summary for where the site has a non-cryptographic secure origin. */ thisOriginIsANonhttpsSecure: 'This origin is a non-HTTPS secure origin.', /** *@description Text in Security Panel of the Security panel */ yourConnectionToThisOriginIsNot: 'Your connection to this origin is not secure.', /** *@description No info div text content in Security Panel of the Security panel */ noSecurityInformation: 'No security information', /** *@description Text in Security Panel of the Security panel */ noSecurityDetailsAreAvailableFor: 'No security details are available for this origin.', /** *@description San div text content in Security Panel of the Security panel */ na: '(n/a)', /** *@description Text to show less content */ showLess: 'Show less', /** *@description Truncated santoggle text content in Security Panel of the Security panel *@example {2} PH1 */ showMoreSTotal: 'Show more ({PH1} total)', /** *@description Shown when a field refers to an option that is unknown to the frontend. */ unknownField: 'unknown', /** *@description Shown when a field refers to a TLS feature which was enabled. */ enabled: 'enabled', }; const str_ = i18n.i18n.registerUIStrings('panels/security/SecurityPanel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let securityPanelInstance: SecurityPanel; // See https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme // This contains signature schemes supported by Chrome. const SignatureSchemeStrings = new Map([ // The full name for these schemes is RSASSA-PKCS1-v1_5, sometimes // "PKCS#1 v1.5", but those are very long, so let "RSA" vs "RSA-PSS" // disambiguate. [0x0201, 'RSA with SHA-1'], [0x0401, 'RSA with SHA-256'], [0x0501, 'RSA with SHA-384'], [0x0601, 'RSA with SHA-512'], // We omit the curve from these names because in TLS 1.2 these code points // were not specific to a curve. Saying "P-256" for a server that used a P-384 // key with SHA-256 in TLS 1.2 would be confusing. [0x0403, 'ECDSA with SHA-256'], [0x0503, 'ECDSA with SHA-384'], [0x0804, 'RSA-PSS with SHA-256'], [0x0805, 'RSA-PSS with SHA-384'], [0x0806, 'RSA-PSS with SHA-512'], ]); export class SecurityPanel extends UI.Panel.PanelWithSidebar implements SDK.TargetManager.SDKModelObserver<SecurityModel> { readonly mainView: SecurityMainView; private readonly sidebarMainViewElement: SecurityPanelSidebarTreeElement; readonly sidebarTree: SecurityPanelSidebarTree; private readonly lastResponseReceivedForLoaderId: Map<string, SDK.NetworkRequest.NetworkRequest>; private readonly origins: Map<string, OriginState>; private readonly filterRequestCounts: Map<string, number>; private visibleView: UI.Widget.VBox|null; private eventListeners: Common.EventTarget.EventDescriptor[]; private securityModel: SecurityModel|null; private constructor() { super('security'); this.mainView = new SecurityMainView(this); const title = document.createElement('span'); title.classList.add('title'); title.textContent = i18nString(UIStrings.overview); this.sidebarMainViewElement = new SecurityPanelSidebarTreeElement( title, this.setVisibleView.bind(this, this.mainView), 'security-main-view-sidebar-tree-item', 'lock-icon'); this.sidebarMainViewElement.tooltip = title.textContent; this.sidebarTree = new SecurityPanelSidebarTree(this.sidebarMainViewElement, this.showOrigin.bind(this)); this.panelSidebarElement().appendChild(this.sidebarTree.element); this.lastResponseReceivedForLoaderId = new Map(); this.origins = new Map(); this.filterRequestCounts = new Map(); this.visibleView = null; this.eventListeners = []; this.securityModel = null; SDK.TargetManager.TargetManager.instance().observeModels(SecurityModel, this, {scoped: true}); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.onPrimaryPageChanged, this); } static instance(opts: {forceNew: boolean|null} = {forceNew: null}): SecurityPanel { const {forceNew} = opts; if (!securityPanelInstance || forceNew) { securityPanelInstance = new SecurityPanel(); } return securityPanelInstance; } static createCertificateViewerButtonForOrigin(text: string, origin: string): Element { const certificateButton = UI.UIUtils.createTextButton(text, async (e: Event) => { e.consume(); const names = await SDK.NetworkManager.MultitargetNetworkManager.instance().getCertificate(origin); if (names.length > 0) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer(names); } }, 'origin-button'); UI.ARIAUtils.markAsButton(certificateButton); return certificateButton; } static createCertificateViewerButtonForCert(text: string, names: string[]): Element { const certificateButton = UI.UIUtils.createTextButton(text, e => { e.consume(); Host.InspectorFrontendHost.InspectorFrontendHostInstance.showCertificateViewer(names); }, 'origin-button'); UI.ARIAUtils.markAsButton(certificateButton); return certificateButton; } static createHighlightedUrl(url: Platform.DevToolsPath.UrlString, securityState: string): Element { const schemeSeparator = '://'; const index = url.indexOf(schemeSeparator); // If the separator is not found, just display the text without highlighting. if (index === -1) { const text = document.createElement('span'); text.textContent = url; return text; } const highlightedUrl = document.createElement('span'); const scheme = url.substr(0, index); const content = url.substr(index + schemeSeparator.length); highlightedUrl.createChild('span', 'url-scheme-' + securityState).textContent = scheme; highlightedUrl.createChild('span', 'url-scheme-separator').textContent = schemeSeparator; highlightedUrl.createChild('span').textContent = content; return highlightedUrl; } private updateVisibleSecurityState(visibleSecurityState: PageVisibleSecurityState): void { this.sidebarMainViewElement.setSecurityState(visibleSecurityState.securityState); this.mainView.updateVisibleSecurityState(visibleSecurityState); } private onVisibleSecurityStateChanged({data}: Common.EventTarget.EventTargetEvent<PageVisibleSecurityState>): void { this.updateVisibleSecurityState(data); } selectAndSwitchToMainView(): void { // The sidebar element will trigger displaying the main view. Rather than making a redundant call to display the main view, we rely on this. this.sidebarMainViewElement.select(true); } showOrigin(origin: Platform.DevToolsPath.UrlString): void { const originState = this.origins.get(origin); if (!originState) { return; } if (!originState.originView) { originState.originView = new SecurityOriginView(this, origin, originState); } this.setVisibleView(originState.originView); } override wasShown(): void { super.wasShown(); if (!this.visibleView) { this.selectAndSwitchToMainView(); } } override focus(): void { this.sidebarTree.focus(); } private setVisibleView(view: UI.Widget.VBox): void { if (this.visibleView === view) { return; } if (this.visibleView) { this.visibleView.detach(); } this.visibleView = view; if (view) { this.splitWidget().setMainWidget(view); } } private onResponseReceived(event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.ResponseReceivedEvent>): void { const request = event.data.request; if (request.resourceType() === Common.ResourceType.resourceTypes.Document && request.loaderId) { this.lastResponseReceivedForLoaderId.set(request.loaderId, request); } } private processRequest(request: SDK.NetworkRequest.NetworkRequest): void { const origin = Common.ParsedURL.ParsedURL.extractOrigin(request.url()); if (!origin) { // We don't handle resources like data: URIs. Most of them don't affect the lock icon. return; } let securityState: Protocol.Security.SecurityState.Insecure|Protocol.Security.SecurityState = request.securityState() as Protocol.Security.SecurityState; if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable || request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { securityState = Protocol.Security.SecurityState.Insecure; } const originState = this.origins.get(origin); if (originState) { const oldSecurityState = originState.securityState; originState.securityState = this.securityStateMin(oldSecurityState, securityState); if (oldSecurityState !== originState.securityState) { const securityDetails = request.securityDetails() as Protocol.Network.SecurityDetails | null; if (securityDetails) { originState.securityDetails = securityDetails; } this.sidebarTree.updateOrigin(origin, securityState); if (originState.originView) { originState.originView.setSecurityState(securityState); } } } else { // This stores the first security details we see for an origin, but we should // eventually store a (deduplicated) list of all the different security // details we have seen. https://crbug.com/503170 const newOriginState: OriginState = { securityState, securityDetails: request.securityDetails(), loadedFromCache: request.cached(), originView: undefined, }; this.origins.set(origin, newOriginState); this.sidebarTree.addOrigin(origin, securityState); // Don't construct the origin view yet (let it happen lazily). } } private onRequestFinished(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>): void { const request = event.data; this.updateFilterRequestCounts(request); this.processRequest(request); } private updateFilterRequestCounts(request: SDK.NetworkRequest.NetworkRequest): void { if (request.mixedContentType === Protocol.Security.MixedContentType.None) { return; } let filterKey: string = NetworkForward.UIFilter.MixedContentFilterValues.All; if (request.wasBlocked()) { filterKey = NetworkForward.UIFilter.MixedContentFilterValues.Blocked; } else if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable) { filterKey = NetworkForward.UIFilter.MixedContentFilterValues.BlockOverridden; } else if (request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { filterKey = NetworkForward.UIFilter.MixedContentFilterValues.Displayed; } const currentCount = this.filterRequestCounts.get(filterKey); if (!currentCount) { this.filterRequestCounts.set(filterKey, 1); } else { this.filterRequestCounts.set(filterKey, currentCount + 1); } this.mainView.refreshExplanations(); } filterRequestCount(filterKey: string): number { return this.filterRequestCounts.get(filterKey) || 0; } private securityStateMin(stateA: Protocol.Security.SecurityState, stateB: Protocol.Security.SecurityState): Protocol.Security.SecurityState { return SecurityModel.SecurityStateComparator(stateA, stateB) < 0 ? stateA : stateB; } modelAdded(securityModel: SecurityModel): void { if (securityModel.target() !== securityModel.target().outermostTarget()) { return; } this.securityModel = securityModel; const resourceTreeModel = securityModel.resourceTreeModel(); const networkManager = securityModel.networkManager(); if (this.eventListeners.length) { Common.EventTarget.removeEventListeners(this.eventListeners); } this.eventListeners = [ securityModel.addEventListener(Events.VisibleSecurityStateChanged, this.onVisibleSecurityStateChanged, this), resourceTreeModel.addEventListener( SDK.ResourceTreeModel.Events.InterstitialShown, this.onInterstitialShown, this), resourceTreeModel.addEventListener( SDK.ResourceTreeModel.Events.InterstitialHidden, this.onInterstitialHidden, this), networkManager.addEventListener(SDK.NetworkManager.Events.ResponseReceived, this.onResponseReceived, this), networkManager.addEventListener(SDK.NetworkManager.Events.RequestFinished, this.onRequestFinished, this), ]; if (resourceTreeModel.isInterstitialShowing) { this.onInterstitialShown(); } } modelRemoved(securityModel: SecurityModel): void { if (this.securityModel !== securityModel) { return; } this.securityModel = null; Common.EventTarget.removeEventListeners(this.eventListeners); } private onPrimaryPageChanged( event: Common.EventTarget.EventTargetEvent< {frame: SDK.ResourceTreeModel.ResourceTreeFrame, type: SDK.ResourceTreeModel.PrimaryPageChangeType}>): void { const {frame} = event.data; const request = this.lastResponseReceivedForLoaderId.get(frame.loaderId); this.selectAndSwitchToMainView(); this.sidebarTree.clearOrigins(); this.origins.clear(); this.lastResponseReceivedForLoaderId.clear(); this.filterRequestCounts.clear(); // After clearing the filtered request counts, refresh the // explanations to reflect the new counts. this.mainView.refreshExplanations(); // If we could not find a matching request (as in the case of clicking // through an interstitial, see https://crbug.com/669309), set the origin // based upon the url data from the PrimaryPageChanged event itself. const origin = Common.ParsedURL.ParsedURL.extractOrigin(request ? request.url() : frame.url); this.sidebarTree.setMainOrigin(origin); if (request) { this.processRequest(request); } } private onInterstitialShown(): void { // The panel might have been displaying the origin view on the // previously loaded page. When showing an interstitial, switch // back to the Overview view. this.selectAndSwitchToMainView(); this.sidebarTree.toggleOriginsList(true /* hidden */); } private onInterstitialHidden(): void { this.sidebarTree.toggleOriginsList(false /* hidden */); } } export class SecurityPanelSidebarTree extends UI.TreeOutline.TreeOutlineInShadow { private readonly showOriginInPanel: (arg0: Origin) => void; private mainOrigin: string|null; private readonly originGroupTitles: Map<OriginGroup, string>; private originGroups: Map<OriginGroup, UI.TreeOutline.TreeElement>; private readonly elementsByOrigin: Map<string, SecurityPanelSidebarTreeElement>; private readonly mainViewReloadMessage: UI.TreeOutline.TreeElement; constructor(mainViewElement: SecurityPanelSidebarTreeElement, showOriginInPanel: (arg0: Origin) => void) { super(); this.appendChild(mainViewElement); this.registerCSSFiles([lockIconStyles, sidebarStyles]); this.showOriginInPanel = showOriginInPanel; this.mainOrigin = null; this.originGroupTitles = new Map([ [OriginGroup.MainOrigin, i18nString(UIStrings.mainOrigin)], [OriginGroup.NonSecure, i18nString(UIStrings.nonsecureOrigins)], [OriginGroup.Secure, i18nString(UIStrings.secureOrigins)], [OriginGroup.Unknown, i18nString(UIStrings.unknownCanceled)], ]); this.originGroups = new Map(); for (const group of Object.values(OriginGroup)) { const element = this.createOriginGroupElement(this.originGroupTitles.get(group) as string); this.originGroups.set(group, element); this.appendChild(element); } this.mainViewReloadMessage = new UI.TreeOutline.TreeElement(i18nString(UIStrings.reloadToViewDetails)); this.mainViewReloadMessage.selectable = false; this.mainViewReloadMessage.listItemElement.classList.add('security-main-view-reload-message'); const treeElement = this.originGroups.get(OriginGroup.MainOrigin); (treeElement as UI.TreeOutline.TreeElement).appendChild(this.mainViewReloadMessage); this.clearOriginGroups(); this.elementsByOrigin = new Map(); } private originGroupTitle(originGroup: OriginGroup): string { return this.originGroupTitles.get(originGroup) as string; } private originGroupElement(originGroup: OriginGroup): UI.TreeOutline.TreeElement { return this.originGroups.get(originGroup) as UI.TreeOutline.TreeElement; } private createOriginGroupElement(originGroupTitle: string): UI.TreeOutline.TreeElement { const originGroup = new UI.TreeOutline.TreeElement(originGroupTitle, true); originGroup.selectable = false; originGroup.setCollapsible(false); originGroup.expand(); originGroup.listItemElement.classList.add('security-sidebar-origins'); UI.ARIAUtils.setAccessibleName(originGroup.childrenListElement, originGroupTitle); return originGroup; } toggleOriginsList(hidden: boolean): void { for (const element of this.originGroups.values()) { element.hidden = hidden; } } addOrigin(origin: Platform.DevToolsPath.UrlString, securityState: Protocol.Security.SecurityState): void { this.mainViewReloadMessage.hidden = true; const originElement = new SecurityPanelSidebarTreeElement( SecurityPanel.createHighlightedUrl(origin, securityState), this.showOriginInPanel.bind(this, origin), 'security-sidebar-tree-item', 'security-property'); originElement.tooltip = origin; this.elementsByOrigin.set(origin, originElement); this.updateOrigin(origin, securityState); } setMainOrigin(origin: string): void { this.mainOrigin = origin; } updateOrigin(origin: string, securityState: Protocol.Security.SecurityState): void { const originElement = this.elementsByOrigin.get(origin) as SecurityPanelSidebarTreeElement; originElement.setSecurityState(securityState); let newParent: UI.TreeOutline.TreeElement; if (origin === this.mainOrigin) { newParent = this.originGroups.get(OriginGroup.MainOrigin) as UI.TreeOutline.TreeElement; if (securityState === Protocol.Security.SecurityState.Secure) { newParent.title = i18nString(UIStrings.mainOriginSecure); } else { newParent.title = i18nString(UIStrings.mainOriginNonsecure); } UI.ARIAUtils.setAccessibleName(newParent.childrenListElement, newParent.title); } else { switch (securityState) { case Protocol.Security.SecurityState.Secure: newParent = this.originGroupElement(OriginGroup.Secure); break; case Protocol.Security.SecurityState.Unknown: newParent = this.originGroupElement(OriginGroup.Unknown); break; default: newParent = this.originGroupElement(OriginGroup.NonSecure); break; } } const oldParent = originElement.parent; if (oldParent !== newParent) { if (oldParent) { oldParent.removeChild(originElement); if (oldParent.childCount() === 0) { oldParent.hidden = true; } } newParent.appendChild(originElement); newParent.hidden = false; } } private clearOriginGroups(): void { for (const [originGroup, originGroupElement] of this.originGroups) { if (originGroup === OriginGroup.MainOrigin) { for (let i = originGroupElement.childCount() - 1; i > 0; i--) { originGroupElement.removeChildAtIndex(i); } originGroupElement.title = this.originGroupTitle(OriginGroup.MainOrigin); originGroupElement.hidden = false; this.mainViewReloadMessage.hidden = false; } else { originGroupElement.removeChildren(); originGroupElement.hidden = true; } } } clearOrigins(): void { this.clearOriginGroups(); this.elementsByOrigin.clear(); } wasShown(): void { } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum OriginGroup { MainOrigin = 'MainOrigin', NonSecure = 'NonSecure', Secure = 'Secure', Unknown = 'Unknown', } export class SecurityPanelSidebarTreeElement extends UI.TreeOutline.TreeElement { private readonly selectCallback: () => void; private readonly cssPrefix: string; private readonly iconElement: HTMLElement; private securityStateInternal: Protocol.Security.SecurityState|null; constructor(textElement: Element, selectCallback: () => void, className: string, cssPrefix: string) { super('', false); this.selectCallback = selectCallback; this.cssPrefix = cssPrefix; this.listItemElement.classList.add(className); this.iconElement = this.listItemElement.createChild('div', 'icon'); this.iconElement.classList.add(this.cssPrefix); this.listItemElement.appendChild(textElement); this.securityStateInternal = null; this.setSecurityState(Protocol.Security.SecurityState.Unknown); } setSecurityState(newSecurityState: Protocol.Security.SecurityState): void { if (this.securityStateInternal) { this.iconElement.classList.remove(this.cssPrefix + '-' + this.securityStateInternal); } this.securityStateInternal = newSecurityState; this.iconElement.classList.add(this.cssPrefix + '-' + newSecurityState); } securityState(): Protocol.Security.SecurityState|null { return this.securityStateInternal; } override onselect(): boolean { this.selectCallback(); return true; } } export class SecurityMainView extends UI.Widget.VBox { private readonly panel: SecurityPanel; private readonly summarySection: HTMLElement; private readonly securityExplanationsMain: HTMLElement; private readonly securityExplanationsExtra: HTMLElement; private readonly lockSpectrum: Map<Protocol.Security.SecurityState, HTMLElement>; private summaryText: HTMLElement; private explanations: (Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation)[]|null; private securityState: Protocol.Security.SecurityState|null; constructor(panel: SecurityPanel) { super(true); this.setMinimumSize(200, 100); this.contentElement.classList.add('security-main-view'); this.panel = panel; this.summarySection = this.contentElement.createChild('div', 'security-summary'); // Info explanations should appear after all others. this.securityExplanationsMain = this.contentElement.createChild('div', 'security-explanation-list security-explanations-main'); this.securityExplanationsExtra = this.contentElement.createChild('div', 'security-explanation-list security-explanations-extra'); // Fill the security summary section. const summaryDiv = this.summarySection.createChild('div', 'security-summary-section-title'); summaryDiv.textContent = i18nString(UIStrings.securityOverview); UI.ARIAUtils.markAsHeading(summaryDiv, 1); const lockSpectrum = this.summarySection.createChild('div', 'lock-spectrum'); this.lockSpectrum = new Map([ [Protocol.Security.SecurityState.Secure, lockSpectrum.createChild('div', 'lock-icon lock-icon-secure')], [Protocol.Security.SecurityState.Neutral, lockSpectrum.createChild('div', 'lock-icon lock-icon-neutral')], [Protocol.Security.SecurityState.Insecure, lockSpectrum.createChild('div', 'lock-icon lock-icon-insecure')], ]); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Secure), i18nString(UIStrings.secure)); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Neutral), i18nString(UIStrings.info)); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecure)); this.summarySection.createChild('div', 'triangle-pointer-container') .createChild('div', 'triangle-pointer-wrapper') .createChild('div', 'triangle-pointer'); this.summaryText = this.summarySection.createChild('div', 'security-summary-text'); UI.ARIAUtils.markAsHeading(this.summaryText, 2); this.explanations = null; this.securityState = null; } getLockSpectrumDiv(securityState: Protocol.Security.SecurityState): HTMLElement { const element = this.lockSpectrum.get(securityState); if (!element) { throw new Error(`Invalid argument: ${securityState}`); } return element; } private addExplanation( parent: Element, explanation: Protocol.Security.SecurityStateExplanation|SecurityStyleExplanation): Element { const explanationSection = parent.createChild('div', 'security-explanation'); explanationSection.classList.add('security-explanation-' + explanation.securityState); explanationSection.createChild('div', 'security-property') .classList.add('security-property-' + explanation.securityState); const text = explanationSection.createChild('div', 'security-explanation-text'); const explanationHeader = text.createChild('div', 'security-explanation-title'); if (explanation.title) { explanationHeader.createChild('span').textContent = explanation.title + ' - '; explanationHeader.createChild('span', 'security-explanation-title-' + explanation.securityState).textContent = explanation.summary; } else { explanationHeader.textContent = explanation.summary; } text.createChild('div').textContent = explanation.description; if (explanation.certificate.length) { text.appendChild(SecurityPanel.createCertificateViewerButtonForCert( i18nString(UIStrings.viewCertificate), explanation.certificate)); } if (explanation.recommendations && explanation.recommendations.length) { const recommendationList = text.createChild('ul', 'security-explanation-recommendations'); for (const recommendation of explanation.recommendations) { recommendationList.createChild('li').textContent = recommendation; } } return text; } updateVisibleSecurityState(visibleSecurityState: PageVisibleSecurityState): void { // Remove old state. // It's safe to call this even when this.securityState is undefined. this.summarySection.classList.remove('security-summary-' + this.securityState); // Add new state. this.securityState = visibleSecurityState.securityState; this.summarySection.classList.add('security-summary-' + this.securityState); // Update the color and title of the triangle icon in the lock spectrum to // match the security state. if (this.securityState === Protocol.Security.SecurityState.Insecure) { this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.add('lock-icon-insecure'); this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.remove('lock-icon-insecure-broken'); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecure)); } else if (this.securityState === Protocol.Security.SecurityState.InsecureBroken) { this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.add('lock-icon-insecure-broken'); this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure).classList.remove('lock-icon-insecure'); UI.Tooltip.Tooltip.install( this.getLockSpectrumDiv(Protocol.Security.SecurityState.Insecure), i18nString(UIStrings.notSecureBroken)); } const {summary, explanations} = this.getSecuritySummaryAndExplanations(visibleSecurityState); // Use override summary if present, otherwise use base explanation this.summaryText.textContent = summary || SummaryMessages[this.securityState](); this.explanations = this.orderExplanations(explanations); this.refreshExplanations(); } private getSecuritySummaryAndExplanations(visibleSecurityState: PageVisibleSecurityState): {summary: (string|undefined), explanations: Array<SecurityStyleExplanation>} { const {securityState, securityStateIssueIds} = visibleSecurityState; let summary; const explanations: SecurityStyleExplanation[] = []; summary = this.explainSafetyTipSecurity(visibleSecurityState, summary, explanations); if (securityStateIssueIds.includes('malicious-content')) { summary = i18nString(UIStrings.thisPageIsDangerousFlaggedBy); // Always insert SafeBrowsing explanation at the front. explanations.unshift(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, undefined, i18nString(UIStrings.flaggedByGoogleSafeBrowsing), i18nString(UIStrings.toCheckThisPagesStatusVisit))); } else if ( securityStateIssueIds.includes('is-error-page') && (visibleSecurityState.certificateSecurityState === null || visibleSecurityState.certificateSecurityState.certificateNetworkError === null)) { summary = i18nString(UIStrings.thisIsAnErrorPage); // In the case of a non cert error page, we usually don't have a // certificate, connection, or content that needs to be explained, e.g. in // the case of a net error, so we can early return. return {summary, explanations}; } else if ( securityState === Protocol.Security.SecurityState.InsecureBroken && securityStateIssueIds.includes('scheme-is-not-cryptographic')) { summary = summary || i18nString(UIStrings.thisPageIsInsecureUnencrypted); } if (securityStateIssueIds.includes('scheme-is-not-cryptographic')) { if (securityState === Protocol.Security.SecurityState.Neutral && !securityStateIssueIds.includes('insecure-origin')) { summary = i18nString(UIStrings.thisPageHasANonhttpsSecureOrigin); } return {summary, explanations}; } this.explainCertificateSecurity(visibleSecurityState, explanations); this.explainConnectionSecurity(visibleSecurityState, explanations); this.explainContentSecurity(visibleSecurityState, explanations); return {summary, explanations}; } private explainSafetyTipSecurity( visibleSecurityState: PageVisibleSecurityState, summary: string|undefined, explanations: SecurityStyleExplanation[]): string|undefined { const {securityStateIssueIds, safetyTipInfo} = visibleSecurityState; const currentExplanations = []; if (securityStateIssueIds.includes('bad_reputation')) { const formatedDescription = `${i18nString(UIStrings.chromeHasDeterminedThatThisSiteS)}\n\n${ i18nString(UIStrings.ifYouBelieveThisIsShownIn)}`; currentExplanations.push({ summary: i18nString(UIStrings.thisPageIsSuspicious), description: formatedDescription, }); } else if (securityStateIssueIds.includes('lookalike') && safetyTipInfo && safetyTipInfo.safeUrl) { const hostname = new URL(safetyTipInfo.safeUrl).hostname; const hostnamePlaceholder = {PH1: hostname}; const formatedDescriptionSafety = `${i18nString(UIStrings.thisSitesHostnameLooksSimilarToP, hostnamePlaceholder)}\n\n${ i18nString(UIStrings.ifYouBelieveThisIsShownInErrorSafety)}`; currentExplanations.push( {summary: i18nString(UIStrings.possibleSpoofingUrl), description: formatedDescriptionSafety}); } if (currentExplanations.length > 0) { // To avoid overwriting SafeBrowsing's title, set the main summary only if // it's empty. The title set here can be overridden by later checks (e.g. // bad HTTP). summary = summary || i18nString(UIStrings.thisPageIsSuspiciousFlaggedBy); explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, undefined, currentExplanations[0].summary, currentExplanations[0].description)); } return summary; } private explainCertificateSecurity( visibleSecurityState: PageVisibleSecurityState, explanations: SecurityStyleExplanation[]): void { const {certificateSecurityState, securityStateIssueIds} = visibleSecurityState; const title = i18nString(UIStrings.certificate); if (certificateSecurityState && certificateSecurityState.certificateHasSha1Signature) { const explanationSummary = i18nString(UIStrings.insecureSha); const description = i18nString(UIStrings.theCertificateChainForThisSite); if (certificateSecurityState.certificateHasWeakSignature) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, title, explanationSummary, description, certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } else { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Neutral, title, explanationSummary, description, certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } } if (certificateSecurityState && securityStateIssueIds.includes('cert-missing-subject-alt-name')) { explanations.push(new SecurityStyleExplanation( Protocol.Security.SecurityState.Insecure, title, i18nString(UIStrings.subjectAlternativeNameMissing), i18nString(UIStrings.theCertificateForThisSiteDoesNot), certificateSecurityState.certificate, Protocol.Security.MixedContentType.None)); } if (certificateSecurityState && certificateSecuri