chrome_remote_debug 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ require "net/http"
2
+ require "json"
3
+
4
+ module ChromeRemoteDebug
5
+ class Client
6
+ def initialize(host, port)
7
+ @host = host
8
+ @port = port
9
+ end
10
+
11
+ def pages
12
+ json = Net::HTTP.get(@host, "/json", @port)
13
+ JSON.parse(json).map {|page_spec| Page.new(page_spec)}
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require "json"
2
+
3
+ module ChromeRemoteDebug
4
+ class Command
5
+ @@id = 1
6
+
7
+ def initialize(method, params = {})
8
+ @method = method
9
+ @params = params
10
+ @id = @@id
11
+ @@id += 1
12
+ end
13
+
14
+ def id
15
+ @id
16
+ end
17
+
18
+ def to_json(*a)
19
+ { "id" => id, "method" => @method, "params" => @params }.to_json(*a)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ require "json"
2
+ require_relative "../../vendor/web-socket-ruby/lib/web_socket"
3
+
4
+
5
+ module ChromeRemoteDebug
6
+ class Page
7
+ def initialize(spec)
8
+ @spec = spec
9
+ end
10
+
11
+ def url
12
+ @spec["url"]
13
+ end
14
+
15
+ def favicon
16
+ @spec["faviconUrl"]
17
+ end
18
+
19
+ def title
20
+ @spec["title"]
21
+ end
22
+
23
+ def reload
24
+ ws = ::WebSocket.new(@spec["webSocketDebuggerUrl"])
25
+ ws.send(JSON.generate(Command.new("Page.reload")))
26
+ ws.close()
27
+ end
28
+
29
+ def navigate(url)
30
+ ws = ::WebSocket.new(@spec["webSocketDebuggerUrl"])
31
+ ws.send(JSON.generate(Command.new("Page.navigate", :url => url)))
32
+ ws.close()
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "chrome_remote_debug/command"
2
+ require_relative "chrome_remote_debug/page"
3
+ require_relative "chrome_remote_debug/client"
@@ -0,0 +1,34 @@
1
+ require_relative "spec_helper.rb"
2
+
3
+ describe ChromeRemoteDebug::Client do
4
+
5
+ describe "creation" do
6
+ it "should be created with host and port" do
7
+ expect { ChromeRemoteDebug::Client.new("127.0.0.1", 9222) }.not_to raise_error
8
+ end
9
+ end
10
+
11
+ describe "pages" do
12
+ before do
13
+ stub_request(:get, "127.0.0.1:9222/json").to_return({
14
+ :body => File.new("spec/fixtures/google_twitter_pages.txt")
15
+ })
16
+ @chrome = ChromeRemoteDebug::Client.new("127.0.0.1", 9222)
17
+ @pages = @chrome.pages
18
+ end
19
+
20
+ it "should list all of them" do
21
+ expect(@pages.count).to eq(2)
22
+ end
23
+
24
+ it "should return pages" do
25
+ @pages.all? {|page| expect(page).to be_instance_of(ChromeRemoteDebug::Page)}
26
+ end
27
+
28
+ it "should return google and twitter pages" do
29
+ expect(@pages[0].url).to match(/google/)
30
+ expect(@pages[1].url).to match(/twitter/)
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,72 @@
1
+ require_relative "spec_helper.rb"
2
+
3
+ describe ChromeRemoteDebug::Command do
4
+
5
+ describe "creation" do
6
+ it "should created with method" do
7
+ expect {
8
+ ChromeRemoteDebug::Command.new(
9
+ "method"
10
+ )
11
+ }.not_to raise_error
12
+ end
13
+
14
+ it "should be created with method and params" do
15
+ expect {
16
+ ChromeRemoteDebug::Command.new(
17
+ "method", :param1 => "1", :params2 => "2"
18
+ )
19
+ }.not_to raise_error
20
+ end
21
+ end
22
+
23
+ describe "id" do
24
+ before do
25
+ ChromeRemoteDebug::Command.class_variable_set :@@id, 1
26
+ @command = ChromeRemoteDebug::Command.new(
27
+ "method", :param1 => "1", :param2 => "2"
28
+ )
29
+ end
30
+
31
+ it "should start at 1" do
32
+ expect(@command.id).to eq(1)
33
+ end
34
+
35
+ it "should increase monotonically" do
36
+ command2 = ChromeRemoteDebug::Command.new("method")
37
+ command3 = ChromeRemoteDebug::Command.new("method")
38
+ expect(command2.id).to eq(2)
39
+ expect(command3.id).to eq(3)
40
+ end
41
+ end
42
+
43
+ describe "serialization" do
44
+ before do
45
+ ChromeRemoteDebug::Command.class_variable_set :@@id, 1
46
+ end
47
+
48
+ it "should be serializable to json" do
49
+ command = ChromeRemoteDebug::Command.new(
50
+ "method", :param1 => "1", :param2 => "2"
51
+ )
52
+ expect(command.to_json).to eq(
53
+ JSON.generate({
54
+ "id" => 1,
55
+ "method" => "method",
56
+ "params" => {
57
+ "param1" => "1",
58
+ "param2" => "2"
59
+ }
60
+ })
61
+ )
62
+ end
63
+
64
+ it "should consider params optional" do
65
+ command = ChromeRemoteDebug::Command.new("method")
66
+ expect(command.to_json).to eq(
67
+ JSON.generate({ "id" => 1, "method" => "method", "params" => {} })
68
+ )
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,88 @@
1
+ require_relative "spec_helper.rb"
2
+
3
+ describe ChromeRemoteDebug::Page do
4
+ before do
5
+ @page_spec = JSON.parse( IO.read("spec/fixtures/google_twitter_pages.txt"))[0]
6
+ end
7
+
8
+ describe "creation" do
9
+ it "should be created with spec" do
10
+ expect { ChromeRemoteDebug::Page.new(@page_spec) }.not_to raise_error
11
+ end
12
+ end
13
+
14
+ describe "attributes" do
15
+ before do
16
+ @page = ChromeRemoteDebug::Page.new(@page_spec)
17
+ end
18
+
19
+ it "should have an url attribute" do
20
+ expect(@page.url).to eq(@page_spec["url"])
21
+ end
22
+
23
+ it "should have a favicon attribute" do
24
+ expect(@page.favicon).to eq(@page_spec["faviconUrl"])
25
+ end
26
+
27
+ it "should have a title attribute" do
28
+ expect(@page.title).to eq(@page_spec["title"])
29
+ end
30
+ end
31
+
32
+ describe "reload" do
33
+ before do
34
+ @page = ChromeRemoteDebug::Page.new(@page_spec)
35
+ @websocket = double()
36
+ @websocket.stub(:send)
37
+ @websocket.stub(:close)
38
+ end
39
+
40
+ it "should connect to page websocket" do
41
+ WebSocket.should_receive(:new).with(@page_spec["webSocketDebuggerUrl"]).and_return(@websocket)
42
+ @page.reload
43
+ end
44
+
45
+ it "should send a reload command" do
46
+ WebSocket.stub(:new => @websocket)
47
+ @websocket.should_receive(:send).with(/reload/)
48
+ @page.reload
49
+ end
50
+
51
+ it "should disconnect" do
52
+ WebSocket.stub(:new => @websocket)
53
+ @websocket.should_receive(:send)
54
+ @websocket.should_receive(:close)
55
+ @page.reload
56
+ end
57
+ end
58
+
59
+ # TODO: remove duplication
60
+ describe "navigate" do
61
+ before do
62
+ @page = ChromeRemoteDebug::Page.new(@page_spec)
63
+ @websocket = double()
64
+ @websocket.stub(:send)
65
+ @websocket.stub(:close)
66
+ @url = "http://www.google.com"
67
+ end
68
+
69
+ it "should connect to page websocket" do
70
+ WebSocket.should_receive(:new).with(@page_spec["webSocketDebuggerUrl"]).and_return(@websocket)
71
+ @page.navigate(@url)
72
+ end
73
+
74
+ it "should send a navigate command" do
75
+ WebSocket.stub(:new => @websocket)
76
+ @websocket.should_receive(:send).with(/navigate/).with(/#{@url}/)
77
+ @page.navigate(@url)
78
+ end
79
+
80
+ it "should disconnect" do
81
+ WebSocket.stub(:new => @websocket)
82
+ @websocket.should_receive(:send)
83
+ @websocket.should_receive(:close)
84
+ @page.navigate(@url)
85
+ end
86
+ end
87
+
88
+ end
@@ -0,0 +1,15 @@
1
+ [ {
2
+ "devtoolsFrontendUrl": "/devtools/devtools.html?ws=localhost:9222/devtools/page/16_1",
3
+ "faviconUrl": "http://www.google.com/favicon.ico",
4
+ "thumbnailUrl": "/thumb/http://www.google.com/",
5
+ "title": "Google",
6
+ "url": "http://www.google.com/",
7
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/16_1"
8
+ }, {
9
+ "devtoolsFrontendUrl": "/devtools/devtools.html?ws=localhost:9222/devtools/page/18_1",
10
+ "faviconUrl": "http://twitter.com/favicons/favicon.ico",
11
+ "thumbnailUrl": "/thumb/http://twitter.com/",
12
+ "title": "Twitter",
13
+ "url": "http://twitter.com/",
14
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/18_1"
15
+ } ]
@@ -0,0 +1 @@
1
+ [ ]
@@ -0,0 +1,8 @@
1
+ [ {
2
+ "devtoolsFrontendUrl": "/devtools/devtools.html?ws=localhost:9222/devtools/page/14_1",
3
+ "faviconUrl": "",
4
+ "thumbnailUrl": "/thumb/chrome://newtab/",
5
+ "title": "New Tab",
6
+ "url": "chrome://newtab/",
7
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/14_1"
8
+ } ]
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format progress
@@ -0,0 +1,9 @@
1
+ require_relative "../lib/chrome_remote_debug.rb"
2
+ require "webmock/rspec"
3
+
4
+ # force expect syntax
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |c|
7
+ c.syntax = :expect
8
+ end
9
+ end
@@ -0,0 +1,588 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
4
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
5
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
6
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
7
+
8
+ require "base64"
9
+ require "socket"
10
+ require "uri"
11
+ require "digest/md5"
12
+ require "digest/sha1"
13
+ require "openssl"
14
+ require "stringio"
15
+
16
+
17
+ class WebSocket
18
+
19
+ class << self
20
+
21
+ attr_accessor(:debug)
22
+
23
+ end
24
+
25
+ class Error < RuntimeError
26
+
27
+ end
28
+
29
+ WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
30
+ OPCODE_CONTINUATION = 0x00
31
+ OPCODE_TEXT = 0x01
32
+ OPCODE_BINARY = 0x02
33
+ OPCODE_CLOSE = 0x08
34
+ OPCODE_PING = 0x09
35
+ OPCODE_PONG = 0x0a
36
+
37
+ def initialize(arg, params = {})
38
+ if params[:server] # server
39
+
40
+ @server = params[:server]
41
+ @socket = arg
42
+ line = gets()
43
+ if !line
44
+ raise(WebSocket::Error, "Client disconnected without sending anything.")
45
+ end
46
+ line = line.chomp()
47
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
48
+ raise(WebSocket::Error, "Invalid request: #{line}")
49
+ end
50
+ @path = $1
51
+ read_header()
52
+ if @header["sec-websocket-version"]
53
+ @web_socket_version = @header["sec-websocket-version"]
54
+ @key3 = nil
55
+ elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
56
+ @web_socket_version = "hixie-76"
57
+ @key3 = read(8)
58
+ else
59
+ @web_socket_version = "hixie-75"
60
+ @key3 = nil
61
+ end
62
+ if !@server.accepted_origin?(self.origin)
63
+ raise(WebSocket::Error,
64
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
65
+ "To accept this origin, write e.g. \n" +
66
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
67
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
68
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
69
+ end
70
+ @handshaked = false
71
+
72
+ else # client
73
+
74
+ @web_socket_version = "hixie-76"
75
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
76
+
77
+ if uri.scheme == "ws"
78
+ default_port = 80
79
+ elsif uri.scheme = "wss"
80
+ default_port = 443
81
+ else
82
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
83
+ end
84
+
85
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
86
+ host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
87
+ origin = params[:origin] || "http://#{uri.host}"
88
+ key1 = generate_key()
89
+ key2 = generate_key()
90
+ key3 = generate_key3()
91
+
92
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
93
+
94
+ if uri.scheme == "ws"
95
+ @socket = socket
96
+ else
97
+ @socket = ssl_handshake(socket)
98
+ end
99
+
100
+ write(
101
+ "GET #{@path} HTTP/1.1\r\n" +
102
+ "Upgrade: WebSocket\r\n" +
103
+ "Connection: Upgrade\r\n" +
104
+ "Host: #{host}\r\n" +
105
+ "Origin: #{origin}\r\n" +
106
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
107
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
108
+ "\r\n" +
109
+ "#{key3}")
110
+ flush()
111
+
112
+ line = gets().chomp()
113
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
114
+ read_header()
115
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
116
+ raise(WebSocket::Error,
117
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
118
+ end
119
+ reply_digest = read(16)
120
+ expected_digest = hixie_76_security_digest(key1, key2, key3)
121
+ if reply_digest != expected_digest
122
+ raise(WebSocket::Error,
123
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
124
+ end
125
+ @handshaked = true
126
+
127
+ end
128
+ @received = []
129
+ @buffer = ""
130
+ @closing_started = false
131
+ end
132
+
133
+ attr_reader(:server, :header, :path)
134
+
135
+ def handshake(status = nil, header = {})
136
+ if @handshaked
137
+ raise(WebSocket::Error, "handshake has already been done")
138
+ end
139
+ status ||= "101 Switching Protocols"
140
+ def_header = {}
141
+ case @web_socket_version
142
+ when "hixie-75"
143
+ def_header["WebSocket-Origin"] = self.origin
144
+ def_header["WebSocket-Location"] = self.location
145
+ extra_bytes = ""
146
+ when "hixie-76"
147
+ def_header["Sec-WebSocket-Origin"] = self.origin
148
+ def_header["Sec-WebSocket-Location"] = self.location
149
+ extra_bytes = hixie_76_security_digest(
150
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
151
+ else
152
+ def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
153
+ extra_bytes = ""
154
+ end
155
+ header = def_header.merge(header)
156
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
157
+ # Note that Upgrade and Connection must appear in this order.
158
+ write(
159
+ "HTTP/1.1 #{status}\r\n" +
160
+ "Upgrade: websocket\r\n" +
161
+ "Connection: Upgrade\r\n" +
162
+ "#{header_str}\r\n#{extra_bytes}")
163
+ flush()
164
+ @handshaked = true
165
+ end
166
+
167
+ def send(data)
168
+ if !@handshaked
169
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
170
+ end
171
+ case @web_socket_version
172
+ when "hixie-75", "hixie-76"
173
+ data = force_encoding(data.dup(), "ASCII-8BIT")
174
+ write("\x00#{data}\xff")
175
+ flush()
176
+ else
177
+ send_frame(OPCODE_TEXT, data, !@server)
178
+ end
179
+ end
180
+
181
+ def receive()
182
+ if !@handshaked
183
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
184
+ end
185
+ case @web_socket_version
186
+
187
+ when "hixie-75", "hixie-76"
188
+ packet = gets("\xff")
189
+ return nil if !packet
190
+ if packet =~ /\A\x00(.*)\xff\z/nm
191
+ return force_encoding($1, "UTF-8")
192
+ elsif packet == "\xff" && read(1) == "\x00" # closing
193
+ close(1005, "", :peer)
194
+ return nil
195
+ else
196
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
197
+ end
198
+
199
+ else
200
+ begin
201
+ bytes = read(2).unpack("C*")
202
+ fin = (bytes[0] & 0x80) != 0
203
+ opcode = bytes[0] & 0x0f
204
+ mask = (bytes[1] & 0x80) != 0
205
+ plength = bytes[1] & 0x7f
206
+ if plength == 126
207
+ bytes = read(2)
208
+ plength = bytes.unpack("n")[0]
209
+ elsif plength == 127
210
+ bytes = read(8)
211
+ (high, low) = bytes.unpack("NN")
212
+ plength = high * (2 ** 32) + low
213
+ end
214
+ if @server && !mask
215
+ # Masking is required.
216
+ @socket.close()
217
+ raise(WebSocket::Error, "received unmasked data")
218
+ end
219
+ mask_key = mask ? read(4).unpack("C*") : nil
220
+ payload = read(plength)
221
+ payload = apply_mask(payload, mask_key) if mask
222
+ case opcode
223
+ when OPCODE_TEXT
224
+ return force_encoding(payload, "UTF-8")
225
+ when OPCODE_BINARY
226
+ raise(WebSocket::Error, "received binary data, which is not supported")
227
+ when OPCODE_CLOSE
228
+ close(1005, "", :peer)
229
+ return nil
230
+ when OPCODE_PING
231
+ raise(WebSocket::Error, "received ping, which is not supported")
232
+ when OPCODE_PONG
233
+ else
234
+ raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
235
+ end
236
+ rescue EOFError
237
+ return nil
238
+ end
239
+
240
+ end
241
+ end
242
+
243
+ def tcp_socket
244
+ return @socket
245
+ end
246
+
247
+ def host
248
+ return @header["host"]
249
+ end
250
+
251
+ def origin
252
+ case @web_socket_version
253
+ when "7", "8"
254
+ name = "sec-websocket-origin"
255
+ else
256
+ name = "origin"
257
+ end
258
+ if @header[name]
259
+ return @header[name]
260
+ else
261
+ raise(WebSocket::Error, "%s header is missing" % name)
262
+ end
263
+ end
264
+
265
+ def location
266
+ return "ws://#{self.host}#{@path}"
267
+ end
268
+
269
+ # Does closing handshake.
270
+ def close(code = 1005, reason = "", origin = :self)
271
+ if !@closing_started
272
+ case @web_socket_version
273
+ when "hixie-75", "hixie-76"
274
+ write("\xff\x00")
275
+ else
276
+ if code == 1005
277
+ payload = ""
278
+ else
279
+ payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
280
+ end
281
+ send_frame(OPCODE_CLOSE, payload, false)
282
+ end
283
+ end
284
+ @socket.close() if origin == :peer
285
+ @closing_started = true
286
+ end
287
+
288
+ def close_socket()
289
+ @socket.close()
290
+ end
291
+
292
+ private
293
+
294
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
295
+
296
+ def read_header()
297
+ @header = {}
298
+ while line = gets()
299
+ line = line.chomp()
300
+ break if line.empty?
301
+ if !(line =~ /\A(\S+): (.*)\z/n)
302
+ raise(WebSocket::Error, "invalid request: #{line}")
303
+ end
304
+ @header[$1] = $2
305
+ @header[$1.downcase()] = $2
306
+ end
307
+ if !@header["upgrade"]
308
+ raise(WebSocket::Error, "Upgrade header is missing")
309
+ end
310
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
311
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
312
+ end
313
+ if !@header["connection"]
314
+ raise(WebSocket::Error, "Connection header is missing")
315
+ end
316
+ if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty?
317
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
318
+ end
319
+ end
320
+
321
+ def send_frame(opcode, payload, mask)
322
+ payload = force_encoding(payload.dup(), "ASCII-8BIT")
323
+ # Setting StringIO's encoding to ASCII-8BIT.
324
+ buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
325
+ write_byte(buffer, 0x80 | opcode)
326
+ masked_byte = mask ? 0x80 : 0x00
327
+ if payload.bytesize <= 125
328
+ write_byte(buffer, masked_byte | payload.bytesize)
329
+ elsif payload.bytesize < 2 ** 16
330
+ write_byte(buffer, masked_byte | 126)
331
+ buffer.write([payload.bytesize].pack("n"))
332
+ else
333
+ write_byte(buffer, masked_byte | 127)
334
+ buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
335
+ end
336
+ if mask
337
+ mask_key = Array.new(4){ rand(256) }
338
+ buffer.write(mask_key.pack("C*"))
339
+ payload = apply_mask(payload, mask_key)
340
+ end
341
+ buffer.write(payload)
342
+ write(buffer.string)
343
+ end
344
+
345
+ def gets(rs = $/)
346
+ line = @socket.gets(rs)
347
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
348
+ return line
349
+ end
350
+
351
+ def read(num_bytes)
352
+ str = @socket.read(num_bytes)
353
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
354
+ if str && str.bytesize == num_bytes
355
+ return str
356
+ else
357
+ raise(EOFError)
358
+ end
359
+ end
360
+
361
+ def write(data)
362
+ if WebSocket.debug
363
+ data.scan(/\G(.*?(\n|\z))/n) do
364
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
365
+ end
366
+ end
367
+ @socket.write(data)
368
+ end
369
+
370
+ def flush()
371
+ @socket.flush()
372
+ end
373
+
374
+ def write_byte(buffer, byte)
375
+ buffer.write([byte].pack("C"))
376
+ end
377
+
378
+ def security_digest(key)
379
+ return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
380
+ end
381
+
382
+ def hixie_76_security_digest(key1, key2, key3)
383
+ bytes1 = websocket_key_to_bytes(key1)
384
+ bytes2 = websocket_key_to_bytes(key2)
385
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
386
+ end
387
+
388
+ def apply_mask(payload, mask_key)
389
+ orig_bytes = payload.unpack("C*")
390
+ new_bytes = []
391
+ orig_bytes.each_with_index() do |b, i|
392
+ new_bytes.push(b ^ mask_key[i % 4])
393
+ end
394
+ return new_bytes.pack("C*")
395
+ end
396
+
397
+ def generate_key()
398
+ spaces = 1 + rand(12)
399
+ max = 0xffffffff / spaces
400
+ number = rand(max + 1)
401
+ key = (number * spaces).to_s()
402
+ (1 + rand(12)).times() do
403
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
404
+ pos = rand(key.size + 1)
405
+ key[pos...pos] = char
406
+ end
407
+ spaces.times() do
408
+ pos = 1 + rand(key.size - 1)
409
+ key[pos...pos] = " "
410
+ end
411
+ return key
412
+ end
413
+
414
+ def generate_key3()
415
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
416
+ end
417
+
418
+ def websocket_key_to_bytes(key)
419
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
420
+ return [num].pack("N")
421
+ end
422
+
423
+ def force_encoding(str, encoding)
424
+ if str.respond_to?(:force_encoding)
425
+ return str.force_encoding(encoding)
426
+ else
427
+ return str
428
+ end
429
+ end
430
+
431
+ def ssl_handshake(socket)
432
+ ssl_context = OpenSSL::SSL::SSLContext.new()
433
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
434
+ ssl_socket.sync_close = true
435
+ ssl_socket.connect()
436
+ return ssl_socket
437
+ end
438
+
439
+ end
440
+
441
+
442
+ class WebSocketServer
443
+
444
+ def initialize(params_or_uri, params = nil)
445
+ if params
446
+ uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
447
+ params[:port] ||= uri.port
448
+ params[:accepted_domains] ||= [uri.host]
449
+ else
450
+ params = params_or_uri
451
+ end
452
+ @port = params[:port] || 80
453
+ @accepted_domains = params[:accepted_domains]
454
+ if !@accepted_domains
455
+ raise(ArgumentError, "params[:accepted_domains] is required")
456
+ end
457
+ if params[:host]
458
+ @tcp_server = TCPServer.open(params[:host], @port)
459
+ else
460
+ @tcp_server = TCPServer.open(@port)
461
+ end
462
+ end
463
+
464
+ attr_reader(:tcp_server, :port, :accepted_domains)
465
+
466
+ def run(&block)
467
+ while true
468
+ Thread.start(accept()) do |s|
469
+ begin
470
+ ws = create_web_socket(s)
471
+ yield(ws) if ws
472
+ rescue => ex
473
+ print_backtrace(ex)
474
+ ensure
475
+ begin
476
+ ws.close_socket() if ws
477
+ rescue
478
+ end
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ def accept()
485
+ return @tcp_server.accept()
486
+ end
487
+
488
+ def accepted_origin?(origin)
489
+ domain = origin_to_domain(origin)
490
+ return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
491
+ end
492
+
493
+ def origin_to_domain(origin)
494
+ if origin == "null" || origin == "file://" # local file
495
+ return "null"
496
+ else
497
+ return URI.parse(origin).host
498
+ end
499
+ end
500
+
501
+ def create_web_socket(socket)
502
+ ch = socket.getc()
503
+ if ch == ?<
504
+ # This is Flash socket policy file request, not an actual Web Socket connection.
505
+ send_flash_socket_policy_file(socket)
506
+ return nil
507
+ else
508
+ socket.ungetc(ch) if ch
509
+ return WebSocket.new(socket, :server => self)
510
+ end
511
+ end
512
+
513
+ private
514
+
515
+ def print_backtrace(ex)
516
+ $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
517
+ for s in ex.backtrace[1..-1]
518
+ $stderr.printf(" %s\n", s)
519
+ end
520
+ end
521
+
522
+ # Handles Flash socket policy file request sent when web-socket-js is used:
523
+ # http://github.com/gimite/web-socket-js/tree/master
524
+ def send_flash_socket_policy_file(socket)
525
+ socket.puts('<?xml version="1.0"?>')
526
+ socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
527
+ '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
528
+ socket.puts('<cross-domain-policy>')
529
+ for domain in @accepted_domains
530
+ next if domain == "file://"
531
+ socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
532
+ end
533
+ socket.puts('</cross-domain-policy>')
534
+ socket.close()
535
+ end
536
+
537
+ end
538
+
539
+
540
+ if __FILE__ == $0
541
+ Thread.abort_on_exception = true
542
+
543
+ if ARGV[0] == "server" && ARGV.size == 3
544
+
545
+ server = WebSocketServer.new(
546
+ :accepted_domains => [ARGV[1]],
547
+ :port => ARGV[2].to_i())
548
+ puts("Server is running at port %d" % server.port)
549
+ server.run() do |ws|
550
+ puts("Connection accepted")
551
+ puts("Path: #{ws.path}, Origin: #{ws.origin}")
552
+ if ws.path == "/"
553
+ ws.handshake()
554
+ while data = ws.receive()
555
+ printf("Received: %p\n", data)
556
+ ws.send(data)
557
+ printf("Sent: %p\n", data)
558
+ end
559
+ else
560
+ ws.handshake("404 Not Found")
561
+ end
562
+ puts("Connection closed")
563
+ end
564
+
565
+ elsif ARGV[0] == "client" && ARGV.size == 2
566
+
567
+ client = WebSocket.new(ARGV[1])
568
+ puts("Connected")
569
+ Thread.new() do
570
+ while data = client.receive()
571
+ printf("Received: %p\n", data)
572
+ end
573
+ end
574
+ $stdin.each_line() do |line|
575
+ data = line.chomp()
576
+ client.send(data)
577
+ printf("Sent: %p\n", data)
578
+ end
579
+
580
+ else
581
+
582
+ $stderr.puts("Usage:")
583
+ $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
584
+ $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
585
+ exit(1)
586
+
587
+ end
588
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chrome_remote_debug
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Federico Galassi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &78620210 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *78620210
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &78619950 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *78619950
36
+ - !ruby/object:Gem::Dependency
37
+ name: webmock
38
+ requirement: &78619730 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *78619730
47
+ - !ruby/object:Gem::Dependency
48
+ name: json
49
+ requirement: &78619510 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *78619510
58
+ description: Ruby client library for the Google Chrome Remote Debugging Protocol.
59
+ email:
60
+ - federico.galassi@cleancode.it
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - lib/chrome_remote_debug.rb
66
+ - lib/chrome_remote_debug/command.rb
67
+ - lib/chrome_remote_debug/page.rb
68
+ - lib/chrome_remote_debug/client.rb
69
+ - vendor/web-socket-ruby/lib/web_socket.rb
70
+ - spec/chrome_remote_debug_command_spec.rb
71
+ - spec/chrome_remote_debug_page_spec.rb
72
+ - spec/spec_helper.rb
73
+ - spec/fixtures/no_pages.txt
74
+ - spec/fixtures/google_twitter_pages.txt
75
+ - spec/fixtures/one_blank_page.txt
76
+ - spec/chrome_remote_debug_client_spec.rb
77
+ - spec/spec.opts
78
+ homepage: http://github.com/fgalassi/chrome-remote-debug
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.16
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Ruby client library for the Google Chrome Remote Debugging Protocol.
102
+ test_files:
103
+ - spec/chrome_remote_debug_command_spec.rb
104
+ - spec/chrome_remote_debug_page_spec.rb
105
+ - spec/spec_helper.rb
106
+ - spec/fixtures/no_pages.txt
107
+ - spec/fixtures/google_twitter_pages.txt
108
+ - spec/fixtures/one_blank_page.txt
109
+ - spec/chrome_remote_debug_client_spec.rb
110
+ - spec/spec.opts