From 19e85ccca5e196e3103db526e6550c0b11717ded Mon Sep 17 00:00:00 2001 From: Joey Arhar Date: Tue, 29 Jan 2019 22:07:02 +0000 Subject: [PATCH] [DevTools] Add binary websocket message viewer behind experiment Screenshots: https://imgur.com/a/0rtukft Bug: 605262 Change-Id: I1049d2196e51193419b9dd7ebbe7105a8655a631 Reviewed-on: https://chromium-review.googlesource.com/c/1321570 Commit-Queue: Joey Arhar Reviewed-by: Dmitry Gozman Cr-Original-Commit-Position: refs/heads/master@{#627166} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 9eb859773e133754b856482a4b6ea79ba39412fe --- BUILD.gn | 2 + front_end/main/Main.js | 1 + front_end/network/BinaryResourceView.js | 37 +++++ .../network/ResourceWebSocketFrameView.js | 63 +++++--- front_end/network/module.json | 1 + front_end/platform/utilities.js | 15 ++ .../source_frame/BinaryResourceViewFactory.js | 145 ++++++++++++++++++ front_end/source_frame/ImageView.js | 17 +- front_end/source_frame/ResourceSourceFrame.js | 5 +- front_end/source_frame/SourceFrame.js | 5 +- front_end/source_frame/SourcesTextEditor.js | 11 +- front_end/source_frame/module.json | 1 + 12 files changed, 261 insertions(+), 42 deletions(-) create mode 100644 front_end/network/BinaryResourceView.js create mode 100644 front_end/source_frame/BinaryResourceViewFactory.js diff --git a/BUILD.gn b/BUILD.gn index 6c85481b53..fa36800ea9 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -402,6 +402,7 @@ all_devtools_files = [ "front_end/mobile_throttling/ThrottlingSettingsTab.js", "front_end/ndb_app.json", "front_end/network/blockedURLsPane.css", + "front_end/network/BinaryResourceView.js", "front_end/network/BlockedURLsPane.js", "front_end/network/eventSourceMessagesView.css", "front_end/network/EventSourceMessagesView.js", @@ -647,6 +648,7 @@ all_devtools_files = [ "front_end/snippets/module.json", "front_end/snippets/ScriptSnippetFileSystem.js", "front_end/snippets/SnippetsQuickOpen.js", + "front_end/source_frame/BinaryResourceViewFactory.js", "front_end/source_frame/fontView.css", "front_end/source_frame/FontView.js", "front_end/source_frame/imageView.css", diff --git a/front_end/main/Main.js b/front_end/main/Main.js index f8a51b93cd..f34bba547c 100644 --- a/front_end/main/Main.js +++ b/front_end/main/Main.js @@ -106,6 +106,7 @@ Main.Main = class { _initializeExperiments() { // Keep this sorted alphabetically: both keys and values. Runtime.experiments.register('applyCustomStylesheet', 'Allow custom UI themes'); + Runtime.experiments.register('binaryWebsocketViewer', 'Binary WebSocket Message Viewer'); Runtime.experiments.register('blackboxJSFramesOnTimeline', 'Blackbox JavaScript frames on Timeline', true); Runtime.experiments.register('consoleBelowPrompt', 'Console eager evaluation'); Runtime.experiments.register('consoleKeyboardNavigation', 'Console keyboard navigation', true); diff --git a/front_end/network/BinaryResourceView.js b/front_end/network/BinaryResourceView.js new file mode 100644 index 0000000000..0d4339d0da --- /dev/null +++ b/front_end/network/BinaryResourceView.js @@ -0,0 +1,37 @@ +// Copyright 2019 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. + +Network.BinaryResourceView = class extends UI.VBox { + /** + * @param {string} base64content + * @param {string} contentUrl + * @param {!Common.ResourceType} resourceType + */ + constructor(base64content, contentUrl, resourceType) { + super(); + + if (!base64content.length) { + new UI.EmptyWidget('No data present in selected item').show(this.element); + return; + } + + const binaryResourceViewFactory = + new SourceFrame.BinaryResourceViewFactory(base64content, contentUrl, resourceType); + + const tabbedPane = new UI.TabbedPane(); + tabbedPane.appendTab( + 'base64-tab-id', 'Base64', binaryResourceViewFactory.createBase64View(), + ls`View selected binary WebSocket message as a Base64 string`); + tabbedPane.appendTab( + 'hex-tab-id', 'Hex', binaryResourceViewFactory.createHexView(), + ls`View selected binary WebSocket message as a hexadecimal string`); + tabbedPane.appendTab( + 'hex-viewer-tab-id', 'Hex Viewer', binaryResourceViewFactory.createHexViewerView(), + ls`View selected binary WebSocket message in a hex viewer`); + tabbedPane.appendTab( + 'utf8-id', 'UTF-8', binaryResourceViewFactory.createUtf8View(), + ls`View selected binary WebSocket message decoded as UTF-8`); + tabbedPane.show(this.element); + } +}; diff --git a/front_end/network/ResourceWebSocketFrameView.js b/front_end/network/ResourceWebSocketFrameView.js index bf364c1230..e431016b46 100644 --- a/front_end/network/ResourceWebSocketFrameView.js +++ b/front_end/network/ResourceWebSocketFrameView.js @@ -166,19 +166,25 @@ Network.ResourceWebSocketFrameView = class extends UI.VBox { * @param {!Common.Event} event */ async _onFrameSelected(event) { - const selectedNode = /** @type {!Network.ResourceWebSocketFrameNode} */ (event.data); - this._currentSelectedNode = selectedNode; - const contentProvider = selectedNode.contentProvider(); - let content = await contentProvider.requestContent(); - if (await contentProvider.contentEncoded()) - content = window.atob(content); + this._currentSelectedNode = /** @type {!Network.ResourceWebSocketFrameNode} */ (event.data); + const content = this._currentSelectedNode.dataText(); + + if (Runtime.experiments.isEnabled('binaryWebsocketViewer')) { + if (this._currentSelectedNode.opCode() === Network.ResourceWebSocketFrameView.OpCodes.BinaryFrame) { + this._splitWidget.setSidebarWidget( + new Network.BinaryResourceView(content, this._request.url(), Common.resourceTypes.WebSocket)); + return; + } + } + const jsonView = await SourceFrame.JSONView.createView(content); - if (this._currentSelectedNode !== selectedNode) - return; - if (jsonView) + if (jsonView) { this._splitWidget.setSidebarWidget(jsonView); - else - this._splitWidget.setSidebarWidget(new SourceFrame.ResourceSourceFrame(contentProvider)); + return; + } + + this._splitWidget.setSidebarWidget(new SourceFrame.ResourceSourceFrame( + Common.StaticContentProvider.fromString(this._request.url(), Common.resourceTypes.WebSocket, content))); } /** @@ -244,8 +250,7 @@ Network.ResourceWebSocketFrameNode = class extends DataGrid.SortableDataGridNode * @param {!SDK.NetworkRequest.WebSocketFrame} frame */ constructor(url, frame) { - let dataText = frame.text; - const length = frame.text.length; + let length = frame.text.length; const time = new Date(frame.time * 1000); const timeText = ('0' + time.getHours()).substr(-2) + ':' + ('0' + time.getMinutes()).substr(-2) + ':' + ('0' + time.getSeconds()).substr(-2) + '.' + ('00' + time.getMilliseconds()).substr(-3); @@ -253,11 +258,24 @@ Network.ResourceWebSocketFrameNode = class extends DataGrid.SortableDataGridNode timeNode.createTextChild(timeText); timeNode.title = time.toLocaleString(); + let dataText = frame.text; + let description = Network.ResourceWebSocketFrameView.opCodeDescription(frame.opCode, frame.mask); const isTextFrame = frame.opCode === Network.ResourceWebSocketFrameView.OpCodes.TextFrame; - if (!isTextFrame) - dataText = Network.ResourceWebSocketFrameView.opCodeDescription(frame.opCode, frame.mask); - super({data: dataText, length: length, time: timeNode}); + if (isTextFrame) { + description = dataText; + + } else if ( + Runtime.experiments.isEnabled('binaryWebsocketViewer') && + frame.opCode === Network.ResourceWebSocketFrameView.OpCodes.BinaryFrame) { + length = base64ToSize(frame.text) + ' bytes'; + description = 'Binary Message'; + + } else { + dataText = description; + } + + super({data: description, length: length, time: timeNode}); this._url = url; this._frame = frame; @@ -289,10 +307,17 @@ Network.ResourceWebSocketFrameNode = class extends DataGrid.SortableDataGridNode } /** - * @return {!Common.ContentProvider} + * @return {string} */ - contentProvider() { - return Common.StaticContentProvider.fromString(this._url, Common.resourceTypes.WebSocket, this._dataText); + dataText() { + return this._dataText; + } + + /** + * @return {!Network.ResourceWebSocketFrameView.OpCodes} + */ + opCode() { + return /** @type {!Network.ResourceWebSocketFrameView.OpCodes} */ (this._frame.opCode); } }; diff --git a/front_end/network/module.json b/front_end/network/module.json index 7262319089..8dfcc3dc9e 100644 --- a/front_end/network/module.json +++ b/front_end/network/module.json @@ -182,6 +182,7 @@ "persistence" ], "scripts": [ + "BinaryResourceView.js", "BlockedURLsPane.js", "EventSourceMessagesView.js", "HARWriter.js", diff --git a/front_end/platform/utilities.js b/front_end/platform/utilities.js index ae7c0764cf..599bd8bd02 100644 --- a/front_end/platform/utilities.js +++ b/front_end/platform/utilities.js @@ -1446,3 +1446,18 @@ function singleton(constructorFunction) { constructorFunction[_singletonSymbol] = instance; return instance; } + +/** + * @param {?string} content + * @return {number} + */ +function base64ToSize(content) { + if (!content) + return 0; + let size = content.length * 3 / 4; + if (content[content.length - 1] === '=') + size--; + if (content.length > 1 && content[content.length - 2] === '=') + size--; + return size; +} diff --git a/front_end/source_frame/BinaryResourceViewFactory.js b/front_end/source_frame/BinaryResourceViewFactory.js new file mode 100644 index 0000000000..5fd9503dea --- /dev/null +++ b/front_end/source_frame/BinaryResourceViewFactory.js @@ -0,0 +1,145 @@ +// Copyright 2019 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. + +SourceFrame.BinaryResourceViewFactory = class { + /** + * @param {string} base64content + * @param {string} contentUrl + * @param {!Common.ResourceType} resourceType + */ + constructor(base64content, contentUrl, resourceType) { + this._base64content = base64content; + this._contentUrl = contentUrl; + this._resourceType = resourceType; + /** @type {?Promise} */ + this._arrayPromise = null; + } + + async _fetchContentAsArray() { + if (!this._arrayPromise) { + this._arrayPromise = new Promise(async resolve => { + const fetchResponse = await fetch('data:;base64,' + this._base64content); + resolve(new Uint8Array(await fetchResponse.arrayBuffer())); + }); + } + return await this._arrayPromise; + } + + /** + * @return {!SourceFrame.ResourceSourceFrame} + */ + createBase64View() { + return new SourceFrame.ResourceSourceFrame( + Common.StaticContentProvider.fromString(this._contentUrl, this._resourceType, this._base64content), + /* autoPrettyPrint */ false, {lineNumbers: false, lineWrapping: true}); + } + + /** + * @return {!SourceFrame.ResourceSourceFrame} + */ + createHexView() { + const hexContentProvider = new Common.StaticContentProvider( + this._contentUrl, this._resourceType, + async () => SourceFrame.BinaryResourceViewFactory.uint8ArrayToHexString(await this._fetchContentAsArray())); + return new SourceFrame.ResourceSourceFrame( + hexContentProvider, + /* autoPrettyPrint */ false, {lineNumbers: false, lineWrapping: true}); + } + + /** + * @return {!SourceFrame.ResourceSourceFrame} + */ + createHexViewerView() { + const hexViewerContentProvider = new Common.StaticContentProvider( + this._contentUrl, this._resourceType, + async () => SourceFrame.BinaryResourceViewFactory.uint8ArrayToHexViewer(await this._fetchContentAsArray())); + return new SourceFrame.ResourceSourceFrame( + hexViewerContentProvider, + /* autoPrettyPrint */ false, {lineNumbers: false, lineWrapping: false}); + } + + /** + * @return {!SourceFrame.ResourceSourceFrame} + */ + createUtf8View() { + const utf8ContentProvider = new Common.StaticContentProvider( + this._contentUrl, this._resourceType, + async () => new TextDecoder('utf8').decode(await this._fetchContentAsArray())); + return new SourceFrame.ResourceSourceFrame( + utf8ContentProvider, + /* autoPrettyPrint */ false, {lineNumbers: true, lineWrapping: true}); + } + + /** + * @param {!Uint8Array} uint8Array + * @return {string} + */ + static uint8ArrayToHexString(uint8Array) { + let output = ''; + for (let i = 0; i < uint8Array.length; i++) + output += SourceFrame.BinaryResourceViewFactory.numberToHex(uint8Array[i], 2); + return output; + } + + /** + * @param {number} number + * @param {number} padding + * @return {string} + */ + static numberToHex(number, padding) { + let hex = number.toString(16); + while (hex.length < padding) + hex = '0' + hex; + return hex; + } + + /** + * @param {!Uint8Array} array + * @return {string} + */ + static uint8ArrayToHexViewer(array) { + let output = ''; + let line = 0; + + while ((line * 16) < array.length) { + const lineArray = array.slice(line * 16, (line + 1) * 16); + + // line number + output += SourceFrame.BinaryResourceViewFactory.numberToHex(line, 8) + ':'; + + // hex + let hexColsPrinted = 0; + for (let i = 0; i < lineArray.length; i++) { + if (i % 2 === 0) { + output += ' '; + hexColsPrinted++; + } + output += SourceFrame.BinaryResourceViewFactory.numberToHex(lineArray[i], 2); + hexColsPrinted += 2; + } + + // hex-ascii padding + while (hexColsPrinted < 42) { + output += ' '; + hexColsPrinted++; + } + + // ascii + for (let i = 0; i < lineArray.length; i++) { + const code = lineArray[i]; + if (code >= 32 && code <= 126) { + // printable ascii character + output += String.fromCharCode(code); + } else { + // non-printable + output += '.'; + } + } + + output += '\n'; + line++; + } + return output; + } +}; diff --git a/front_end/source_frame/ImageView.js b/front_end/source_frame/ImageView.js index bed1ce2592..b69709b207 100644 --- a/front_end/source_frame/ImageView.js +++ b/front_end/source_frame/ImageView.js @@ -103,28 +103,13 @@ SourceFrame.ImageView = class extends UI.SimpleView { imageSrc = this._url; const loadPromise = new Promise(x => this._imagePreviewElement.onload = x); this._imagePreviewElement.src = imageSrc; - const size = content && !contentEncoded ? content.length : this._base64ToSize(content); + const size = content && !contentEncoded ? content.length : base64ToSize(content); this._sizeLabel.setText(Number.bytesToString(size)); await loadPromise; this._dimensionsLabel.setText( Common.UIString('%d × %d', this._imagePreviewElement.naturalWidth, this._imagePreviewElement.naturalHeight)); } - /** - * @param {?string} content - * @return {number} - */ - _base64ToSize(content) { - if (!content) - return 0; - let size = content.length * 3 / 4; - if (content[content.length - 1] === '=') - size--; - if (content.length > 1 && content[content.length - 2] === '=') - size--; - return size; - } - _contextMenu(event) { const contextMenu = new UI.ContextMenu(event); if (!this._parsedURL.isDataURL()) diff --git a/front_end/source_frame/ResourceSourceFrame.js b/front_end/source_frame/ResourceSourceFrame.js index 707fd8c8f0..a9640e8f9d 100644 --- a/front_end/source_frame/ResourceSourceFrame.js +++ b/front_end/source_frame/ResourceSourceFrame.js @@ -34,14 +34,15 @@ SourceFrame.ResourceSourceFrame = class extends SourceFrame.SourceFrame { /** * @param {!Common.ContentProvider} resource * @param {boolean=} autoPrettyPrint + * @param {!UI.TextEditor.Options=} codeMirrorOptions */ - constructor(resource, autoPrettyPrint) { + constructor(resource, autoPrettyPrint, codeMirrorOptions) { super(async () => { let content = await resource.requestContent(); if (await resource.contentEncoded()) content = window.atob(content); return content; - }); + }, codeMirrorOptions); this._resource = resource; this.setCanPrettyPrint(this._resource.contentType().isDocumentOrScriptOrStyleSheet(), autoPrettyPrint); } diff --git a/front_end/source_frame/SourceFrame.js b/front_end/source_frame/SourceFrame.js index f2d5e15bbe..a391c487fe 100644 --- a/front_end/source_frame/SourceFrame.js +++ b/front_end/source_frame/SourceFrame.js @@ -36,8 +36,9 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { /** * @param {function(): !Promise} lazyContent + * @param {!UI.TextEditor.Options=} codeMirrorOptions */ - constructor(lazyContent) { + constructor(lazyContent, codeMirrorOptions) { super(Common.UIString('Source')); this._lazyContent = lazyContent; @@ -56,7 +57,7 @@ SourceFrame.SourceFrame = class extends UI.SimpleView { this._shouldAutoPrettyPrint = false; this._prettyToggle.setVisible(false); - this._textEditor = new SourceFrame.SourcesTextEditor(this); + this._textEditor = new SourceFrame.SourcesTextEditor(this, codeMirrorOptions); this._textEditor.show(this.element); /** @type {?number} */ diff --git a/front_end/source_frame/SourcesTextEditor.js b/front_end/source_frame/SourcesTextEditor.js index 7b104c1540..ec9551e913 100644 --- a/front_end/source_frame/SourcesTextEditor.js +++ b/front_end/source_frame/SourcesTextEditor.js @@ -7,14 +7,19 @@ SourceFrame.SourcesTextEditor = class extends TextEditor.CodeMirrorTextEditor { /** * @param {!SourceFrame.SourcesTextEditorDelegate} delegate + * @param {!UI.TextEditor.Options=} codeMirrorOptions */ - constructor(delegate) { - super({ + constructor(delegate, codeMirrorOptions) { + const defaultCodeMirrorOptions = { lineNumbers: true, lineWrapping: false, bracketMatchingSetting: Common.moduleSetting('textEditorBracketMatching'), padBottom: true - }); + }; + if (codeMirrorOptions) + Object.assign(defaultCodeMirrorOptions, codeMirrorOptions); + + super(defaultCodeMirrorOptions); this.codeMirror().addKeyMap({'Enter': 'smartNewlineAndIndent', 'Esc': 'sourcesDismiss'}); diff --git a/front_end/source_frame/module.json b/front_end/source_frame/module.json index c670fc1c4f..993ccac446 100644 --- a/front_end/source_frame/module.json +++ b/front_end/source_frame/module.json @@ -41,6 +41,7 @@ "workspace_diff" ], "scripts": [ + "BinaryResourceViewFactory.js", "SourcesTextEditor.js", "FontView.js", "ImageView.js",