ruby-vnc 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/net/vnc.rb CHANGED
@@ -1,306 +1,373 @@
1
1
  require 'socket'
2
2
  require 'yaml'
3
- require 'thread'
4
- require 'cipher/des'
3
+ require 'cipher/vncdes'
5
4
  require 'net/vnc/version'
6
5
 
7
6
  module Net
8
- #
9
- # The VNC class provides for simple rfb-protocol based control of
10
- # a VNC server. This can be used, eg, to automate applications.
11
- #
12
- # Sample usage:
13
- #
14
- # # launch xclock on localhost. note that there is an xterm in the top-left
15
- # Net::VNC.open 'localhost:0', :shared => true, :password => 'mypass' do |vnc|
16
- # vnc.pointer_move 10, 10
17
- # vnc.type 'xclock'
18
- # vnc.key_press :return
19
- # end
20
- #
21
- # = TODO
22
- #
23
- # * The server read loop seems a bit iffy. Not sure how best to do it.
24
- # * Should probably be changed to be more of a lower-level protocol wrapping thing, with the
25
- # actual VNCClient sitting on top of that. all it should do is read/write the packets over
26
- # the socket.
27
- #
28
- class VNC
29
- class PointerState
30
- attr_reader :x, :y, :button
31
-
32
- def initialize vnc
33
- @x = @y = @button = 0
34
- @vnc = vnc
35
- end
36
-
37
- # could have the same for x=, and y=
38
- def button= button
39
- @button = button
40
- refresh
41
- end
42
-
43
- def update x, y, button=@button
44
- @x, @y, @button = x, y, button
45
- refresh
46
- end
47
-
48
- def refresh
49
- packet = 0.chr * 6
50
- packet[0] = 5.chr
51
- packet[1] = button.chr
52
- packet[2, 2] = [x].pack 'n'
53
- packet[4, 2] = [y].pack 'n'
54
- @vnc.socket.write packet
55
- end
56
- end
57
-
58
- BASE_PORT = 5900
59
- CHALLENGE_SIZE = 16
60
- DEFAULT_OPTIONS = {
61
- :shared => false,
62
- :wait => 0.1
63
- }
64
-
65
- keys_file = File.dirname(__FILE__) + '/../../data/keys.yaml'
66
- KEY_MAP = YAML.load_file(keys_file).inject({}) { |h, (k, v)| h.update k.to_sym => v }
67
- def KEY_MAP.[] key
68
- super or raise ArgumentError.new('Invalid key name - %s' % key)
69
- end
70
-
71
- attr_reader :server, :display, :options, :socket, :pointer
72
-
73
- def initialize display=':0', options={}
74
- @server = 'localhost'
75
- if display =~ /^(.*)(:\d+)$/
76
- @server, display = $1, $2
77
- end
78
- @display = display[1..-1].to_i
79
- @options = DEFAULT_OPTIONS.merge options
80
- @clipboard = nil
81
- @pointer = PointerState.new self
82
- @mutex = Mutex.new
83
- connect
84
- @packet_reading_state = nil
85
- @packet_reading_thread = Thread.new { packet_reading_thread }
86
- end
87
-
88
- def self.open display=':0', options={}
89
- vnc = new display, options
90
- if block_given?
91
- begin
92
- yield vnc
93
- ensure
94
- vnc.close
95
- end
96
- else
97
- vnc
98
- end
99
- end
100
-
101
- def port
102
- BASE_PORT + @display
103
- end
104
-
105
- def connect
106
- @socket = TCPSocket.open server, port
107
- unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/
108
- raise 'invalid server response'
109
- end
110
- @server_version = $1
111
- socket.write "RFB 003.003\n"
112
- data = socket.read(4)
113
- auth = data.to_s.unpack('N')[0]
114
- case auth
115
- when 0, nil
116
- raise 'connection failed'
117
- when 1
118
- # ok...
119
- when 2
120
- password = @options[:password] or raise 'Need to authenticate but no password given'
121
- challenge = socket.read CHALLENGE_SIZE
122
- response = Cipher::DES.encrypt password, challenge
123
- socket.write response
124
- ok = socket.read(4).to_s.unpack('N')[0]
125
- raise 'Unable to authenticate - %p' % ok unless ok == 0
126
- else
127
- raise 'Unknown authentication scheme - %d' % auth
128
- end
129
-
130
- # ClientInitialisation
131
- socket.write((options[:shared] ? 1 : 0).chr)
132
-
133
- # ServerInitialisation
134
- # TODO: parse this.
135
- socket.read(20)
136
- data = socket.read(4)
137
- # read this many bytes in chunks of 20
138
- size = data.to_s.unpack('N')[0]
139
- while size > 0
140
- len = [20, size].min
141
- # this is the hostname, and other stuff i think...
142
- socket.read(len)
143
- size -= len
144
- end
145
- end
146
-
147
- # this types +text+ on the server
148
- def type text, options={}
149
- packet = 0.chr * 8
150
- packet[0] = 4.chr
151
- text.split(//).each do |char|
152
- packet[7] = char[0]
153
- packet[1] = 1.chr
154
- socket.write packet
155
- packet[1] = 0.chr
156
- socket.write packet
157
- end
158
- wait options
159
- end
160
-
161
- # this takes an array of keys, and successively holds each down then lifts them up in
162
- # reverse order.
163
- # FIXME: should wait. can't recurse in that case.
164
- def key_press(*args)
165
- options = Hash === args.last ? args.pop : {}
166
- keys = args
167
- raise ArgumentError, 'Must have at least one key argument' if keys.empty?
168
- begin
169
- key_down keys.first
170
- if keys.length == 1
171
- yield if block_given?
172
- else
173
- key_press(*(keys[1..-1] + [options]))
174
- end
175
- ensure
176
- key_up keys.first
177
- end
178
- end
179
-
180
- def get_key_code which
181
- if String === which
182
- if which.length != 1
183
- raise ArgumentError, 'can only get key_code of single character strings'
184
- end
185
- which[0]
186
- else
187
- KEY_MAP[which]
188
- end
189
- end
190
- private :get_key_code
191
-
192
- def key_down which, options={}
193
- packet = 0.chr * 8
194
- packet[0] = 4.chr
195
- key_code = get_key_code which
196
- packet[4, 4] = [key_code].pack('N')
197
- packet[1] = 1.chr
198
- socket.write packet
199
- wait options
200
- end
201
-
202
- def key_up which, options={}
203
- packet = 0.chr * 8
204
- packet[0] = 4.chr
205
- key_code = get_key_code which
206
- packet[4, 4] = [key_code].pack('N')
207
- packet[1] = 0.chr
208
- socket.write packet
209
- wait options
210
- end
211
-
212
- def pointer_move x, y, options={}
213
- # options[:relative]
214
- pointer.update x, y
215
- wait options
216
- end
217
-
218
- BUTTON_MAP = {
219
- :left => 0
220
- }
221
-
222
- def button_press button=:left, options={}
223
- begin
224
- button_down button, options
225
- yield if block_given?
226
- ensure
227
- button_up button, options
228
- end
229
- end
230
-
231
- def button_down which=:left, options={}
232
- button = BUTTON_MAP[which] || which
233
- raise ArgumentError, 'Invalid button - %p' % which unless (0..2) === button
234
- pointer.button |= 1 << button
235
- wait options
236
- end
237
-
238
- def button_up which=:left, options={}
239
- button = BUTTON_MAP[which] || which
240
- raise ArgumentError, 'Invalid button - %p' % which unless (0..2) === button
241
- pointer.button &= ~(1 << button)
242
- wait options
243
- end
244
-
245
- def wait options={}
246
- sleep options[:wait] || @options[:wait]
247
- end
248
-
249
- def close
250
- # destroy packet reading thread
251
- if @packet_reading_state == :loop
252
- @packet_reading_state = :stop
253
- while @packet_reading_state
254
- # do nothing
255
- end
256
- end
257
- socket.close
258
- end
259
-
260
- def clipboard
261
- if block_given?
262
- @clipboard = nil
263
- yield
264
- 60.times do
265
- clipboard = @mutex.synchronize { @clipboard }
266
- return clipboard if clipboard
267
- sleep 0.5
268
- end
269
- warn 'clipboard still empty after 30s'
270
- nil
271
- else
272
- @mutex.synchronize { @clipboard }
273
- end
274
- end
275
-
276
- private
277
-
278
- def read_packet type
279
- case type
280
- when 3 # ServerCutText
281
- socket.read 3 # discard padding bytes
282
- len = socket.read(4).unpack('N')[0]
283
- @mutex.synchronize { @clipboard = socket.read len }
284
- else
285
- raise NotImplementedError, 'unhandled server packet type - %d' % type
286
- end
287
- end
288
-
289
- def packet_reading_thread
290
- @packet_reading_state = :loop
291
- loop do
292
- begin
293
- break if @packet_reading_state != :loop
294
- next unless IO.select [socket], nil, nil, 2
295
- type = socket.read(1)[0]
296
- read_packet type
297
- rescue
298
- warn "exception in packet_reading_thread: #{$!.class}:#{$!}"
299
- break
300
- end
301
- end
302
- @packet_reading_state = nil
303
- end
304
- end
305
- end
7
+ #
8
+ # The VNC class provides for simple rfb-protocol based control of
9
+ # a VNC server. This can be used, eg, to automate applications.
10
+ #
11
+ # Sample usage:
12
+ #
13
+ # # launch xclock on localhost. note that there is an xterm in the top-left
14
+ #
15
+ # require 'net/vnc'
16
+ # Net::VNC.open 'localhost:0', :shared => true, :password => 'mypass' do |vnc|
17
+ # vnc.pointer_move 10, 10
18
+ # vnc.type 'xclock'
19
+ # vnc.key_press :return
20
+ # end
21
+ #
22
+ # = TODO
23
+ #
24
+ # * The server read loop seems a bit iffy. Not sure how best to do it.
25
+ # * Should probably be changed to be more of a lower-level protocol wrapping thing, with the
26
+ # actual VNCClient sitting on top of that. all it should do is read/write the packets over
27
+ # the socket.
28
+ #
29
+ class VNC
30
+ class PointerState
31
+ attr_reader :x, :y, :button
32
+
33
+ def initialize(vnc)
34
+ @x = @y = @button = 0
35
+ @vnc = vnc
36
+ end
37
+
38
+ # could have the same for x=, and y=
39
+ def button=(button)
40
+ @button = button
41
+ refresh
42
+ end
43
+
44
+ def update(x, y, button = @button)
45
+ @x = x
46
+ @y = y
47
+ @button = button
48
+ refresh
49
+ end
50
+
51
+ def refresh
52
+ packet = 0.chr * 6
53
+ packet[0] = 5.chr
54
+ packet[1] = button.chr
55
+ packet[2, 2] = [x].pack 'n'
56
+ packet[4, 2] = [y].pack 'n'
57
+ @vnc.socket.write packet
58
+ end
59
+ end
60
+
61
+ BASE_PORT = 5900
62
+ CHALLENGE_SIZE = 16
63
+ DEFAULT_OPTIONS = {
64
+ shared: false,
65
+ wait: 0.1,
66
+ pix_fmt: :BGRA,
67
+ encoding: :RAW
68
+ }
69
+
70
+ keys_file = File.dirname(__FILE__) + '/../../data/keys.yaml'
71
+ KEY_MAP = YAML.load_file(keys_file).inject({}) { |h, (k, v)| h.update k.to_sym => v }
72
+ def KEY_MAP.[](key)
73
+ super or raise(ArgumentError, 'Invalid key name - %s' % key)
74
+ end
75
+
76
+ attr_reader :server, :display, :options, :socket, :pointer, :desktop_name
77
+
78
+ def initialize(display = ':0', options = {})
79
+ @server = 'localhost'
80
+ if display =~ /^(.*)(:\d+)$/
81
+ @server = Regexp.last_match(1)
82
+ display = Regexp.last_match(2)
83
+ end
84
+ @display = display[1..-1].to_i
85
+ @desktop_name = nil
86
+ @options = DEFAULT_OPTIONS.merge options
87
+ @clipboard = nil
88
+ @fb = nil
89
+ @pointer = PointerState.new self
90
+ @mutex = Mutex.new
91
+ connect
92
+ @packet_reading_state = nil
93
+ @packet_reading_thread = Thread.new { packet_reading_thread }
94
+ end
95
+
96
+ def self.open(display = ':0', options = {})
97
+ vnc = new display, options
98
+ if block_given?
99
+ begin
100
+ yield vnc
101
+ ensure
102
+ vnc.close
103
+ end
104
+ else
105
+ vnc
106
+ end
107
+ end
108
+
109
+ def port
110
+ BASE_PORT + @display
111
+ end
112
+
113
+ def connect
114
+ @socket = TCPSocket.open(server, port)
115
+ raise 'invalid server response' unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/
116
+
117
+ @server_version = Regexp.last_match(1)
118
+ socket.write "RFB 003.003\n"
119
+ data = socket.read(4)
120
+ auth = data.to_s.unpack1('N')
121
+ case auth
122
+ when 0, nil
123
+ raise 'connection failed'
124
+ when 1
125
+ # ok...
126
+ when 2
127
+ password = @options[:password] or raise 'Need to authenticate but no password given'
128
+ challenge = socket.read CHALLENGE_SIZE
129
+ response = Cipher::VNCDES.new(password).encrypt(challenge)
130
+ socket.write response
131
+ ok = socket.read(4).to_s.unpack1('N')
132
+ raise 'Unable to authenticate - %p' % ok unless ok == 0
133
+ else
134
+ raise 'Unknown authentication scheme - %d' % auth
135
+ end
136
+
137
+ # ClientInitialisation
138
+ socket.write((options[:shared] ? 1 : 0).chr)
139
+
140
+ # ServerInitialisation
141
+ @framebuffer_width = socket.read(2).to_s.unpack1('n').to_i
142
+ @framebuffer_height = socket.read(2).to_s.unpack1('n').to_i
143
+
144
+ # TODO: parse this.
145
+ _pixel_format = socket.read(16)
146
+
147
+ # read the name in byte chunks of 20
148
+ name_length = socket.read(4).to_s.unpack1('N')
149
+ @desktop_name = [].tap do |it|
150
+ while name_length > 0
151
+ len = [20, name_length].min
152
+ it << socket.read(len)
153
+ name_length -= len
154
+ end
155
+ end.join
156
+
157
+ _load_frame_buffer
158
+ end
159
+
160
+ # this types +text+ on the server
161
+ def type(text, options = {})
162
+ packet = 0.chr * 8
163
+ packet[0] = 4.chr
164
+ text.split(//).each do |char|
165
+ packet[7] = char[0]
166
+ packet[1] = 1.chr
167
+ socket.write packet
168
+ packet[1] = 0.chr
169
+ socket.write packet
170
+ end
171
+ wait options
172
+ end
173
+
174
+ # this takes an array of keys, and successively holds each down then lifts them up in
175
+ # reverse order.
176
+ # FIXME: should wait. can't recurse in that case.
177
+ def key_press(*args)
178
+ options = args.last.is_a?(Hash) ? args.pop : {}
179
+ keys = args
180
+ raise ArgumentError, 'Must have at least one key argument' if keys.empty?
181
+
182
+ begin
183
+ key_down keys.first
184
+ if keys.length == 1
185
+ yield if block_given?
186
+ else
187
+ key_press(*(keys[1..-1] + [options]))
188
+ end
189
+ ensure
190
+ key_up keys.first
191
+ end
192
+ end
193
+
194
+ def get_key_code(which)
195
+ case which
196
+ when String
197
+ raise ArgumentError, 'can only get key_code of single character strings' if which.length != 1
198
+
199
+ which[0].ord
200
+ when Symbol
201
+ KEY_MAP[which]
202
+ when Integer
203
+ which
204
+ else
205
+ raise ArgumentError, "unsupported key value: #{which.inspect}"
206
+ end
207
+ end
208
+ private :get_key_code
306
209
 
210
+ def key_down(which, options = {})
211
+ packet = 0.chr * 8
212
+ packet[0] = 4.chr
213
+ key_code = get_key_code which
214
+ packet[4, 4] = [key_code].pack('N')
215
+ packet[1] = 1.chr
216
+ socket.write packet
217
+ wait options
218
+ end
219
+
220
+ def key_up(which, options = {})
221
+ packet = 0.chr * 8
222
+ packet[0] = 4.chr
223
+ key_code = get_key_code which
224
+ packet[4, 4] = [key_code].pack('N')
225
+ packet[1] = 0.chr
226
+ socket.write packet
227
+ wait options
228
+ end
229
+
230
+ def pointer_move(x, y, options = {})
231
+ # options[:relative]
232
+ pointer.update x, y
233
+ wait options
234
+ end
235
+
236
+ BUTTON_MAP = {
237
+ left: 0
238
+ }
239
+
240
+ def button_press(button = :left, options = {})
241
+ button_down button, options
242
+ yield if block_given?
243
+ ensure
244
+ button_up button, options
245
+ end
246
+
247
+ def button_down(which = :left, options = {})
248
+ button = BUTTON_MAP[which] || which
249
+ raise ArgumentError, 'Invalid button - %p' % which unless (0..2).include?(button)
250
+
251
+ pointer.button |= 1 << button
252
+ wait options
253
+ end
254
+
255
+ def button_up(which = :left, options = {})
256
+ button = BUTTON_MAP[which] || which
257
+ raise ArgumentError, 'Invalid button - %p' % which unless (0..2).include?(button)
258
+
259
+ pointer.button &= ~(1 << button)
260
+ wait options
261
+ end
262
+
263
+ # take screenshot as PNG image
264
+ # @param dest [String|IO|nil] destination file path, or IO-object, or nil
265
+ # @return [String] PNG binary data as string when dest is null
266
+ # [true] else case
267
+ def take_screenshot(dest = nil)
268
+ fb = _load_frame_buffer # on-demand loading
269
+ fb.save_pixel_data_as_png dest
270
+ end
271
+
272
+ def wait(options = {})
273
+ sleep options[:wait] || @options[:wait]
274
+ end
275
+
276
+ def close
277
+ # destroy packet reading thread
278
+ if @packet_reading_state == :loop
279
+ @packet_reading_state = :stop
280
+ while @packet_reading_state
281
+ # do nothing
282
+ end
283
+ end
284
+ socket.close
285
+ end
286
+
287
+ def reconnect
288
+ 60.times do
289
+ if @packet_reading_state.nil?
290
+ connect
291
+ @packet_reading_thread = Thread.new { packet_reading_thread }
292
+ return true
293
+ end
294
+ sleep 0.5
295
+ end
296
+ warn 'reconnect failed because packet reading state had not been stopped for 30 seconds.'
297
+ false
298
+ end
299
+
300
+ def clipboard
301
+ if block_given?
302
+ @clipboard = nil
303
+ yield
304
+ 60.times do
305
+ clipboard = @mutex.synchronize { @clipboard }
306
+ return clipboard if clipboard
307
+
308
+ sleep 0.5
309
+ end
310
+ warn 'clipboard still empty after 30s'
311
+ nil
312
+ else
313
+ @mutex.synchronize { @clipboard }
314
+ end
315
+ end
316
+
317
+ def clipboard=(text)
318
+ text = text.to_s.gsub(/\R/, "\n") # eol of ClientCutText's text is LF
319
+ byte_size = text.to_s.bytes.size
320
+ packet = 0.chr * (8 + byte_size)
321
+ packet[0] = 6.chr # message-type: 6 (ClientCutText)
322
+ packet[4, 4] = [byte_size].pack('N') # length
323
+ packet[8, byte_size] = text
324
+ socket.write(packet)
325
+ @clipboard = text
326
+ end
327
+
328
+ private
329
+
330
+ def read_packet(type)
331
+ case type
332
+ when 0 # ----------------------------------------------- FramebufferUpdate
333
+ @fb.handle_response type if @fb
334
+ when 1 # --------------------------------------------- SetColourMapEntries
335
+ @fb.handle_response type if @fb
336
+ when 2 # ------------------------------------------------------------ Bell
337
+ nil # not support
338
+ when 3 # --------------------------------------------------- ServerCutText
339
+ socket.read 3 # discard padding bytes
340
+ len = socket.read(4).unpack1('N')
341
+ @mutex.synchronize { @clipboard = socket.read len }
342
+ else
343
+ warn 'unhandled server packet type - %d' % type
344
+ end
345
+ end
346
+
347
+ def packet_reading_thread
348
+ @packet_reading_state = :loop
349
+ loop do
350
+ break if @packet_reading_state != :loop
351
+ next unless IO.select [socket], nil, nil, 2
352
+
353
+ type = socket.read(1)[0]
354
+ read_packet type.ord
355
+ rescue StandardError
356
+ warn "exception in packet_reading_thread: #{$!.class}:#{$!}\n#{$!.backtrace}"
357
+ break
358
+ end
359
+ @packet_reading_state = nil
360
+ end
361
+
362
+ def _load_frame_buffer
363
+ unless @fb
364
+ require 'net/rfb/frame_buffer'
365
+
366
+ @fb = Net::RFB::FrameBuffer.new @socket, @framebuffer_width, @framebuffer_height, @options[:pix_fmt],
367
+ @options[:encoding]
368
+ @fb.send_initial_data
369
+ end
370
+ @fb
371
+ end
372
+ end
373
+ end