chrome_remote_debug 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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