From bb1eefb64620dfdf1c5b465d36f9b96afff26e0d Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Wed, 23 Nov 2022 16:52:31 -0800 Subject: [PATCH 1/2] Authenticate user when they click on the debug extension --- dwds/debug_extension_mv3/web/background.dart | 56 ++++++++++++++++++++ dwds/debug_extension_mv3/web/chrome_api.dart | 22 ++++++++ dwds/debug_extension_mv3/web/manifest.json | 1 + dwds/debug_extension_mv3/web/web_api.dart | 55 +++++++++++++++++++ 4 files changed, 134 insertions(+) diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index b74913654..552a5f571 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -9,6 +9,7 @@ import 'dart:async'; import 'dart:html'; import 'package:dwds/data/debug_info.dart'; +import 'package:dwds/data/extension_request.dart'; import 'package:js/js.dart'; import 'chrome_api.dart'; @@ -18,6 +19,8 @@ import 'messaging.dart'; import 'storage.dart'; import 'web_api.dart'; +const _authSuccessResponse = 'Dart Debug Authentication Success!'; + void main() { _registerListeners(); } @@ -33,6 +36,16 @@ void _registerListeners() { // TODO(elliette): Start a debug session instead. Future _startDebugSession(Tab currentTab) async { + final tabId = currentTab.id; + final debugInfo = await _fetchDebugInfo(tabId); + final extensionUrl = debugInfo?.extensionUrl; + if (extensionUrl == null) { + _showWarningNotification('Can\'t debug Dart app. Extension URL not found.'); + return; + } + final isAuthenticated = await _authenticateUser(extensionUrl, tabId); + if (!isAuthenticated) return; + maybeCreateLifelinePort(currentTab.id); final devToolsOpener = await fetchStorageObject( type: StorageObject.devToolsOpener); @@ -40,6 +53,29 @@ Future _startDebugSession(Tab currentTab) async { inNewWindow: devToolsOpener?.newWindow ?? false); } +Future _authenticateUser(String extensionUrl, int tabId) async { + final authUrl = _constructAuthUrl(extensionUrl).toString(); + final response = await fetchRequest(authUrl); + final responseBody = response.body ?? ''; + if (!responseBody.contains(_authSuccessResponse)) { + _showWarningNotification('Please re-authenticate and try again.'); + await _createTab(authUrl, inNewWindow: false); + return false; + } + return true; +} + +Uri _constructAuthUrl(String extensionUrl) { + final authUri = Uri.parse(extensionUrl).replace(path: authenticationPath); + if (authUri.scheme == 'ws') { + return authUri.replace(scheme: 'http'); + } + if (authUri.scheme == 'wss') { + return authUri.replace(scheme: 'https'); + } + return authUri; +} + void _handleRuntimeMessages( dynamic jsRequest, MessageSender sender, Function sendResponse) async { if (jsRequest is! String) return; @@ -71,6 +107,26 @@ void _handleRuntimeMessages( }); } +Future _fetchDebugInfo(int tabId) { + return fetchStorageObject( + type: StorageObject.debugInfo, + tabId: tabId, + ); +} + +void _showWarningNotification(String message) { + chrome.notifications.create( + /*notificationId*/ null, + NotificationOptions( + title: '[Error] Dart Debug Extension', + message: message, + iconUrl: 'dart.png', + type: 'basic', + ), + /*callback*/ null, + ); +} + Future _getTab() async { final query = QueryInfo(active: true, currentWindow: true); final tabs = List.from(await promiseToFuture(chrome.tabs.query(query))); diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index de0250499..05ccbfedc 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -11,6 +11,7 @@ external Chrome get chrome; @anonymous class Chrome { external Action get action; + external Notifications get notifications; external Runtime get runtime; external Scripting get scripting; external Storage get storage; @@ -42,6 +43,27 @@ class IconInfo { external factory IconInfo({String path}); } +/// chrome.notification APIs: +/// https://developer.chrome.com/docs/extensions/reference/notifications + +@JS() +@anonymous +class Notifications { + external void create( + String? notificationId, NotificationOptions options, Function? callback); +} + +@JS() +@anonymous +class NotificationOptions { + external factory NotificationOptions({ + String title, + String message, + String iconUrl, + String type, + }); +} + /// chrome.runtime APIs: /// https://developer.chrome.com/docs/extensions/reference/runtime diff --git a/dwds/debug_extension_mv3/web/manifest.json b/dwds/debug_extension_mv3/web/manifest.json index 92e53961e..777d059ee 100644 --- a/dwds/debug_extension_mv3/web/manifest.json +++ b/dwds/debug_extension_mv3/web/manifest.json @@ -7,6 +7,7 @@ }, "permissions": [ "debugger", + "notifications", "scripting", "storage", "tabs" diff --git a/dwds/debug_extension_mv3/web/web_api.dart b/dwds/debug_extension_mv3/web/web_api.dart index 938a0a140..6679af033 100644 --- a/dwds/debug_extension_mv3/web/web_api.dart +++ b/dwds/debug_extension_mv3/web/web_api.dart @@ -1,8 +1,10 @@ // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:html'; import 'package:js/js.dart'; +import 'dart:js_util' as js_util; @JS() external Console get console; @@ -16,3 +18,56 @@ class Console { external void warn(String header, [String style1, String style2, String style3]); } + +// Custom implementation of Fetch API until the Dart implementation supports +// credentials. See https://github.com/dart-lang/http/issues/595. +@JS('fetch') +external Object _nativeJsFetch(String resourceUrl, FetchOptions options); + +Future fetchRequest(String resourceUrl) async { + try { + final options = FetchOptions( + method: 'GET', + credentialsOptions: CredentialsOptions(credentials: 'include'), + ); + final response = + await promiseToFuture(_nativeJsFetch(resourceUrl, options)); + final body = + await promiseToFuture(js_util.callMethod(response, 'text', [])); + final ok = js_util.getProperty(response, 'ok'); + final status = js_util.getProperty(response, 'status'); + return FetchResponse(status: status, ok: ok, body: body); + } catch (error) { + return FetchResponse( + status: 400, ok: false, body: 'Error fetching $resourceUrl: $error'); + } +} + +@JS() +@anonymous +class FetchOptions { + external factory FetchOptions({ + required String method, // e.g., 'GET', 'POST' + required CredentialsOptions credentialsOptions, + }); +} + +@JS() +@anonymous +class CredentialsOptions { + external factory CredentialsOptions({ + required String credentials, // e.g., 'omit', 'same-origin', 'include' + }); +} + +class FetchResponse { + final int status; + final bool ok; + final String? body; + + FetchResponse({ + required this.status, + required this.ok, + required this.body, + }); +} From 585ac6172abce728268f34c51684237335f03b52 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Wed, 23 Nov 2022 17:04:48 -0800 Subject: [PATCH 2/2] Update tests --- dwds/test/puppeteer/extension_test.dart | 6 +++++- dwds/test/puppeteer/lifeline_test.dart | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index 78ec470b1..ad1920a94 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -41,7 +41,11 @@ void main() async { setUpAll(() async { // TODO(elliette): Only start a TestServer, that way we can get rid of // the launchChrome parameter: https://github.com/dart-lang/webdev/issues/1779 - await context.setUp(launchChrome: false, useSse: useSse); + await context.setUp( + launchChrome: false, + useSse: useSse, + enableDebugExtension: true, + ); browser = await puppeteer.launch( headless: false, timeout: Duration(seconds: 60), diff --git a/dwds/test/puppeteer/lifeline_test.dart b/dwds/test/puppeteer/lifeline_test.dart index 8a9cf8169..b179a8704 100644 --- a/dwds/test/puppeteer/lifeline_test.dart +++ b/dwds/test/puppeteer/lifeline_test.dart @@ -24,7 +24,10 @@ void main() async { group('MV3 Debug Extension Lifeline Connection', () { setUpAll(() async { extensionPath = await buildDebugExtension(); - await context.setUp(launchChrome: false); + await context.setUp( + launchChrome: false, + enableDebugExtension: true, + ); browser = await puppeteer.launch( headless: false, timeout: Duration(seconds: 60),