@@ -9,6 +9,143 @@ module DEBUGGER__
9
9
module UI_CDP
10
10
SHOW_PROTOCOL = ENV [ 'RUBY_DEBUG_CDP_SHOW_PROTOCOL' ] == '1'
11
11
12
+ def setup_chrome
13
+ require 'open3'
14
+ require 'tmpdir'
15
+
16
+ set_chrome_path
17
+ @dir = Dir . mktmpdir
18
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
19
+ stdin , stdout , stderr , @wait_thr = *Open3 . popen3 ( "#{ CONFIG [ :chrome_path ] } --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{ @dir } " )
20
+ stdin . close
21
+ stdout . close
22
+
23
+ data = stderr . readpartial 4096
24
+ if data . match /DevTools listening on ws:\/ \/ 127.0.0.1:(\d +)(.*)/
25
+ port = $1
26
+ path = $2
27
+ else
28
+ if File . exist? "#{ @dir } /DevToolsActivePort"
29
+ port , path = File . read ( "#{ @dir } /DevToolsActivePort" ) . split ( "\n " )
30
+ else
31
+ raise "Can't open Chrome browser"
32
+ end
33
+ end
34
+
35
+ s = Socket . tcp "127.0.0.1" , port
36
+ ws_client = WebSocketClient . new ( s )
37
+ ws_client . handshake port , path
38
+ ws_client . send id : 1 , method : 'Target.getTargets'
39
+
40
+ 3 . times do
41
+ res = ws_client . extract_data
42
+ case
43
+ when res [ 'id' ] == 1 && target_info = res . dig ( 'result' , 'targetInfos' )
44
+ p = target_info . find { |t | t [ 'type' ] == 'page' }
45
+ ws_client . send id : 2 , method : 'Target.attachToTarget' ,
46
+ params : {
47
+ targetId : p [ 'targetId' ] ,
48
+ flatten : true
49
+ }
50
+ when res [ 'id' ] == 2
51
+ s_id = res . dig ( 'result' , 'sessionId' )
52
+ sleep 0.1
53
+ ws_client . send sessionId : s_id , id : 1 ,
54
+ method : 'Page.navigate' ,
55
+ params : {
56
+ url : "devtools://devtools/bundled/inspector.html?ws=#{ @addr } "
57
+ }
58
+ end
59
+ end
60
+ end
61
+
62
+ def set_chrome_path
63
+ # The process to check OS is based on `selenium` project.
64
+ case RbConfig ::CONFIG [ 'host_os' ]
65
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
66
+ CONFIG [ :chrome_path ] ||= 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
67
+ when /darwin|mac os/
68
+ CONFIG [ :chrome_path ] ||= '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
69
+ when /linux/
70
+ CONFIG [ :chrome_path ] ||= 'google-chrome'
71
+ else
72
+ raise "Unsupported OS"
73
+ end
74
+ end
75
+
76
+ class WebSocketClient
77
+ def initialize s
78
+ @sock = s
79
+ end
80
+
81
+ def handshake port , path
82
+ key = SecureRandom . hex ( 11 )
83
+ @sock . print "GET #{ path } HTTP/1.1\r \n Host: 127.0.0.1:#{ port } \r \n Connection: Upgrade\r \n Upgrade: websocket\r \n Sec-WebSocket-Version: 13\r \n Sec-WebSocket-Key: #{ key } ==\r \n \r \n "
84
+ res = @sock . readpartial 4092
85
+ $stderr. puts '[>]' + res if SHOW_PROTOCOL
86
+
87
+ if res . match /^Sec-WebSocket-Accept: (.*)\r \n /
88
+ correct_key = Base64 . strict_encode64 Digest ::SHA1 . digest "#{ key } ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
89
+ raise "The Sec-WebSocket-Accept value: #{ $1} is not valid" unless $1 == correct_key
90
+ else
91
+ raise "Unknown response: #{ res } "
92
+ end
93
+ end
94
+
95
+ def send **msg
96
+ msg = JSON . generate ( msg )
97
+ frame = [ ]
98
+ fin = 0b10000000
99
+ opcode = 0b00000001
100
+ frame << fin + opcode
101
+
102
+ mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
103
+ bytesize = msg . bytesize
104
+ if bytesize < 126
105
+ payload_len = bytesize
106
+ elsif bytesize < 2 ** 16
107
+ payload_len = 0b01111110
108
+ ex_payload_len = [ bytesize ] . pack ( 'n*' ) . bytes
109
+ else
110
+ payload_len = 0b01111111
111
+ ex_payload_len = [ bytesize ] . pack ( 'Q>' ) . bytes
112
+ end
113
+
114
+ frame << mask + payload_len
115
+ frame . push *ex_payload_len if ex_payload_len
116
+
117
+ frame . push *masking_key = 4 . times . map { rand ( 1 ..255 ) }
118
+ masked = [ ]
119
+ msg . bytes . each_with_index do |b , i |
120
+ masked << ( b ^ masking_key [ i % 4 ] )
121
+ end
122
+
123
+ frame . push *masked
124
+ @sock . print frame . pack 'c*'
125
+ end
126
+
127
+ def extract_data
128
+ first_group = @sock . getbyte
129
+ fin = first_group & 0b10000000 != 128
130
+ raise 'Unsupported' if fin
131
+ opcode = first_group & 0b00001111
132
+ raise "Unsupported: #{ opcode } " unless opcode == 1
133
+
134
+ second_group = @sock . getbyte
135
+ mask = second_group & 0b10000000 == 128
136
+ raise 'The server must not mask any frames' if mask
137
+ payload_len = second_group & 0b01111111
138
+ # TODO: Support other payload_lengths
139
+ if payload_len == 126
140
+ payload_len = @sock . read ( 2 ) . unpack ( 'n*' ) [ 0 ]
141
+ end
142
+
143
+ data = JSON . parse @sock . read payload_len
144
+ $stderr. puts '[>]' + data . inspect if SHOW_PROTOCOL
145
+ data
146
+ end
147
+ end
148
+
12
149
class WebSocketServer
13
150
def initialize s
14
151
@sock = s
0 commit comments