33import 'dart:async' ;
44import 'dart:convert' ;
55import 'dart:io' ;
6+ import 'dart:math' ;
67
78import 'package:http/http.dart' as http;
89
@@ -44,6 +45,7 @@ class CdpDriver implements AppDriver {
4445 final Map <String , List <void Function (Map <String , dynamic >)>> _eventListeners =
4546 {};
4647 bool _dialogHandlerInstalled = false ;
48+ bool _isChrome146ConsentPort = false ;
4749 final Map <String , Map <String , dynamic >> _interceptRules = {};
4850
4951 /// Create a CDP driver.
@@ -226,8 +228,11 @@ class CdpDriver implements AppDriver {
226228 }
227229 }
228230
229- /// Check if CDP port is already responding
231+ /// Check if CDP port is already responding.
232+ /// Handles both standard CDP (HTTP /json/version) and Chrome 146+'s
233+ /// consent-based port (WebSocket /devtools/browser/{uuid} — no HTTP).
230234 Future <bool > _isCdpPortAlive () async {
235+ // Standard CDP: HTTP /json/version
231236 try {
232237 final client = HttpClient ()
233238 ..connectionTimeout = const Duration (seconds: 2 );
@@ -236,10 +241,27 @@ class CdpDriver implements AppDriver {
236241 final response = await request.close ();
237242 await response.drain <void >();
238243 client.close ();
244+ _isChrome146ConsentPort = false ;
239245 return true ;
240- } catch (_) {
241- return false ;
242- }
246+ } catch (_) {}
247+
248+ // Chrome 146+ consent port: HTTP returns 404, but WebSocket to
249+ // /devtools/browser/{uuid} either connects or hangs (waiting for Allow).
250+ // A quick 1s probe: if the socket connects (not refused), port is alive.
251+ try {
252+ final sock = await Socket .connect (
253+ InternetAddress .loopbackIPv4,
254+ _port,
255+ timeout: const Duration (seconds: 1 ),
256+ );
257+ await sock.close ();
258+ // Port is open. Check if it's a Chrome 146 consent port by probing /json/version.
259+ // 404 = Chrome 146 consent port. Connection refused = not alive.
260+ _isChrome146ConsentPort = true ;
261+ return true ;
262+ } catch (_) {}
263+
264+ return false ;
243265 }
244266
245267 /// Check if a Chrome/Chromium process is running (any instance, debug port or not).
@@ -2803,7 +2825,141 @@ end tell
28032825 _eventSubscriptions.remove ('Page.frameStoppedLoading' );
28042826 }
28052827
2828+ /// For Chrome 146+'s consent-based port (no HTTP endpoints):
2829+ /// Connect via WebSocket to the browser-level CDP endpoint, send
2830+ /// Target.getTargets, find the best matching target, and return its WS URL.
2831+ ///
2832+ /// Chrome shows "Allow remote debugging?" dialog on first connection.
2833+ /// We wait up to [timeout] seconds for the user to click Allow.
2834+ Future <String ?> _discoverTargetViaConsentPort ({
2835+ Duration timeout = const Duration (seconds: 30 ),
2836+ }) async {
2837+ final wsUrl = 'ws://127.0.0.1:$_port /devtools/browser/${_generateUuid ()}' ;
2838+ WebSocket ? ws;
2839+ try {
2840+ ws = await WebSocket .connect (wsUrl).timeout (timeout);
2841+ } catch (_) {
2842+ return null ;
2843+ }
2844+
2845+ try {
2846+ // Send Target.getTargets
2847+ const msgId = 1 ;
2848+ ws.add (jsonEncode ({
2849+ 'id' : msgId,
2850+ 'method' : 'Target.getTargets' ,
2851+ 'params' : {},
2852+ }));
2853+
2854+ // Wait for response
2855+ await for (final msg in ws.timeout (const Duration (seconds: 5 ))) {
2856+ final data = jsonDecode (msg as String ) as Map <String , dynamic >;
2857+ if (data['id' ] == msgId) {
2858+ final result = data['result' ] as Map <String , dynamic >? ;
2859+ final targetInfos = result? ['targetInfos' ] as List ? ?? [];
2860+ final pages = targetInfos
2861+ .whereType <Map >()
2862+ .where ((t) => t['type' ] == 'page' )
2863+ .toList ();
2864+
2865+ // Match by URL (same logic as _discoverTarget HTTP path)
2866+ final targetUri = _url.isNotEmpty ? Uri .tryParse (_url) : null ;
2867+ final targetHost = targetUri? .host ?? '' ;
2868+
2869+ // Exact URL match
2870+ if (targetHost.isNotEmpty) {
2871+ for (final t in pages) {
2872+ if (t['url' ] == _url) {
2873+ connectedToExistingTab = true ;
2874+ return 'ws://127.0.0.1:$_port /devtools/page/${t ['targetId' ]}' ;
2875+ }
2876+ }
2877+ // Same host match
2878+ for (final t in pages) {
2879+ final tabUri = Uri .tryParse (t['url' ]? .toString () ?? '' );
2880+ if (tabUri != null && tabUri.host == targetHost) {
2881+ connectedToExistingTab = true ;
2882+ return 'ws://127.0.0.1:$_port /devtools/page/${t ['targetId' ]}' ;
2883+ }
2884+ }
2885+ // Same root domain
2886+ final targetParts = targetHost.split ('.' );
2887+ final targetRoot = targetParts.length >= 2
2888+ ? targetParts.sublist (targetParts.length - 2 ).join ('.' )
2889+ : targetHost;
2890+ for (final t in pages) {
2891+ final tabUri = Uri .tryParse (t['url' ]? .toString () ?? '' );
2892+ if (tabUri != null ) {
2893+ final tabParts = tabUri.host.split ('.' );
2894+ final tabRoot = tabParts.length >= 2
2895+ ? tabParts.sublist (tabParts.length - 2 ).join ('.' )
2896+ : tabUri.host;
2897+ if (tabRoot == targetRoot) {
2898+ connectedToExistingTab = true ;
2899+ return 'ws://127.0.0.1:$_port /devtools/page/${t ['targetId' ]}' ;
2900+ }
2901+ }
2902+ }
2903+ }
2904+
2905+ // Blank tab or first non-chrome tab
2906+ for (final t in pages) {
2907+ final tabUrl = t['url' ]? .toString () ?? '' ;
2908+ if (tabUrl == 'about:blank' ) {
2909+ return 'ws://127.0.0.1:$_port /devtools/page/${t ['targetId' ]}' ;
2910+ }
2911+ }
2912+ if (_url.isEmpty && pages.isNotEmpty) {
2913+ return 'ws://127.0.0.1:$_port /devtools/page/${pages .first ['targetId' ]}' ;
2914+ }
2915+
2916+ // No match found — create a new tab via Target.createTarget
2917+ ws.add (jsonEncode ({
2918+ 'id' : msgId + 1 ,
2919+ 'method' : 'Target.createTarget' ,
2920+ 'params' : {'url' : _url.isNotEmpty ? _url : 'about:blank' },
2921+ }));
2922+ await for (final msg2 in ws.timeout (const Duration (seconds: 5 ))) {
2923+ final d2 = jsonDecode (msg2 as String ) as Map <String , dynamic >;
2924+ if (d2['id' ] == msgId + 1 ) {
2925+ final targetId = d2['result' ]? ['targetId' ] as String ? ;
2926+ if (targetId != null ) {
2927+ return 'ws://127.0.0.1:$_port /devtools/page/$targetId ' ;
2928+ }
2929+ break ;
2930+ }
2931+ }
2932+ break ;
2933+ }
2934+ }
2935+ } catch (_) {}
2936+
2937+ try {
2938+ await ws.close ();
2939+ } catch (_) {}
2940+ return null ;
2941+ }
2942+
2943+ /// Generate a random UUID v4.
2944+ static String _generateUuid () {
2945+ final r = Random .secure ();
2946+ final bytes = List <int >.generate (16 , (_) => r.nextInt (256 ));
2947+ bytes[6 ] = (bytes[6 ] & 0x0f ) | 0x40 ;
2948+ bytes[8 ] = (bytes[8 ] & 0x3f ) | 0x80 ;
2949+ String hex (int b) => b.toRadixString (16 ).padLeft (2 , '0' );
2950+ return '${hex (bytes [0 ])}${hex (bytes [1 ])}${hex (bytes [2 ])}${hex (bytes [3 ])}'
2951+ '-${hex (bytes [4 ])}${hex (bytes [5 ])}'
2952+ '-${hex (bytes [6 ])}${hex (bytes [7 ])}'
2953+ '-${hex (bytes [8 ])}${hex (bytes [9 ])}'
2954+ '-${hex (bytes [10 ])}${hex (bytes [11 ])}${hex (bytes [12 ])}${hex (bytes [13 ])}${hex (bytes [14 ])}${hex (bytes [15 ])}' ;
2955+ }
2956+
28062957 Future <String ?> _discoverTarget () async {
2958+ // Chrome 146+ consent port: HTTP endpoints not available, use WebSocket CDP.
2959+ if (_isChrome146ConsentPort) {
2960+ return _discoverTargetViaConsentPort (timeout: const Duration (seconds: 30 ));
2961+ }
2962+
28072963 // Try multiple times as Chrome may still be starting
28082964 for (var i = 0 ; i < 10 ; i++ ) {
28092965 try {
0 commit comments