| // 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. | |
| @JS() | |
| library; | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 | |
| // ignore: deprecated_member_use | |
| import 'dart:js_util'; | |
| import 'package:dwds/data/data_types.dart'; | |
| import 'package:dwds/data/debug_info.dart'; | |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 | |
| // ignore: deprecated_member_use | |
| import 'package:js/js.dart'; | |
| import 'chrome_api.dart'; | |
| import 'logger.dart'; | |
| import 'utils.dart'; | |
| // A default response for the sendResponse callback. | |
| // | |
| // Prevents the message port from closing. See: | |
| // https://developer.chrome.com/docs/extensions/mv3/messaging/#simple | |
| final defaultResponse = jsify({'response': 'received'}); | |
| enum Script { | |
| background, | |
| copier, | |
| debuggerPanel, | |
| detector, | |
| popup; | |
| factory Script.fromString(String value) { | |
| return Script.values.byName(value); | |
| } | |
| } | |
| enum MessageType { | |
| isAuthenticated, | |
| connectFailure, | |
| appId, | |
| debugInfo, | |
| debugStateChange, | |
| devToolsUrl, | |
| multipleAppsDetected; | |
| factory MessageType.fromString(String value) { | |
| return MessageType.values.byName(value); | |
| } | |
| } | |
| class Message { | |
| final Script to; | |
| final Script from; | |
| final MessageType type; | |
| final String body; | |
| final String? error; | |
| Message({ | |
| required this.to, | |
| required this.from, | |
| required this.type, | |
| required this.body, | |
| this.error, | |
| }); | |
| factory Message.fromJSON(String json) { | |
| final decoded = jsonDecode(json) as Map<String, dynamic>; | |
| return Message( | |
| to: Script.fromString(decoded['to'] as String), | |
| from: Script.fromString(decoded['from'] as String), | |
| type: MessageType.fromString(decoded['type'] as String), | |
| body: decoded['body'] as String, | |
| error: decoded['error'] as String?, | |
| ); | |
| } | |
| String toJSON() { | |
| return jsonEncode({ | |
| 'to': to.name, | |
| 'from': from.name, | |
| 'type': type.name, | |
| 'body': body, | |
| if (error != null) 'error': error, | |
| }); | |
| } | |
| } | |
| void interceptMessage<T>({ | |
| required String? message, | |
| required MessageType expectedType, | |
| required Script expectedSender, | |
| required Script expectedRecipient, | |
| required MessageSender sender, | |
| required void Function(T message) messageHandler, | |
| }) { | |
| if (message == null) return; | |
| if (!_isLegitimateSender(sender)) return; | |
| try { | |
| final decodedMessage = Message.fromJSON(message); | |
| if (decodedMessage.type != expectedType || | |
| decodedMessage.to != expectedRecipient || | |
| decodedMessage.from != expectedSender) { | |
| return; | |
| } | |
| if (T == String) { | |
| messageHandler(decodedMessage.body as T); | |
| } else { | |
| messageHandler(_deserialize<T>(decodedMessage.body)); | |
| } | |
| } catch (error) { | |
| debugError( | |
| 'Error intercepting $expectedType from ' | |
| '$expectedSender to $expectedRecipient: $error', | |
| ); | |
| } | |
| } | |
| T _deserialize<T>(String body) { | |
| final decoded = jsonDecode(body); | |
| return switch (decoded) { | |
| ['ConnectFailure', ...] => ConnectFailure.fromJson(decoded) as T, | |
| ['DebugStateChange', ...] => DebugStateChange.fromJson(decoded) as T, | |
| ['DevToolsOpener', ...] => DevToolsOpener.fromJson(decoded) as T, | |
| ['DevToolsUrl', ...] => DevToolsUrl.fromJson(decoded) as T, | |
| ['DebugInfo', ...] => DebugInfo.fromJson(decoded) as T, | |
| _ => throw UnsupportedError('Unknown type for deserialization: $T'), | |
| }; | |
| } | |
| /// Send a message using the chrome.runtime.sendMessage API. | |
| Future<bool> sendRuntimeMessage({ | |
| required MessageType type, | |
| required String body, | |
| required Script sender, | |
| required Script recipient, | |
| }) => | |
| _sendMessage(type: type, body: body, sender: sender, recipient: recipient); | |
| /// Send a message using the chrome.tabs.sendMessage API. | |
| Future<bool> sendTabsMessage({ | |
| required int tabId, | |
| required MessageType type, | |
| required String body, | |
| required Script sender, | |
| required Script recipient, | |
| }) => _sendMessage( | |
| tabId: tabId, | |
| type: type, | |
| body: body, | |
| sender: sender, | |
| recipient: recipient, | |
| ); | |
| Future<bool> _sendMessage({ | |
| required MessageType type, | |
| required String body, | |
| required Script sender, | |
| required Script recipient, | |
| int? tabId, | |
| }) { | |
| final message = Message( | |
| to: recipient, | |
| from: sender, | |
| type: type, | |
| body: body, | |
| ).toJSON(); | |
| final completer = Completer<bool>(); | |
| void responseHandler([dynamic _]) { | |
| final error = chrome.runtime.lastError; | |
| if (error != null) { | |
| debugError( | |
| 'Error sending $type to $recipient from $sender: ${error.message}', | |
| ); | |
| } | |
| completer.complete(error != null); | |
| } | |
| if (tabId != null) { | |
| chrome.tabs.sendMessage( | |
| tabId, | |
| message, | |
| // options | |
| null, | |
| allowInterop(responseHandler), | |
| ); | |
| } else { | |
| chrome.runtime.sendMessage( | |
| // id | |
| null, | |
| message, | |
| // options | |
| null, | |
| allowInterop(responseHandler), | |
| ); | |
| } | |
| return completer.future; | |
| } | |
| // Verify the message sender is our extension. | |
| bool _isLegitimateSender(MessageSender sender) { | |
| // Check that the sender ID matches our extension ID: | |
| if (sender.id != chrome.runtime.id) return false; | |
| final senderUri = Uri.parse(sender.origin ?? ''); | |
| final senderHost = senderUri.host; | |
| final isDartAppHost = | |
| senderHost == 'localhost' || | |
| senderHost == '127.0.0.1' || | |
| _isGoogleHost(senderHost); | |
| final isExtensionOrigin = | |
| senderHost == chrome.runtime.id && senderUri.scheme == 'chrome-extension'; | |
| if (isDartAppHost || isExtensionOrigin) return true; | |
| // If the sender's host is unexpected, display an error. | |
| displayNotification( | |
| 'Unexpected sender ${sender.origin}. Please file a bug at go/dde-bug or https://github.com/dart-lang/webdev', | |
| isError: true, | |
| ); | |
| return false; | |
| } | |
| bool _isGoogleHost(String host) { | |
| const googleSuffices = ['.googlers.com', '.google.com', '.googleprod.com']; | |
| return googleSuffices.any((suffix) => host.endsWith(suffix)); | |
| } |