evilcap-ruby-vnc 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c6cb5b602176bf4af4687a2771a19b5d24e902e6137be88b572b0f3f98ee960
4
+ data.tar.gz: f9d1482b9dfd3454ffaa01c3c9fb0c7aba39102d29ee449fe2e84eec09894e76
5
+ SHA512:
6
+ metadata.gz: bc3bbf48ff15d28609e84b9a789fb5a767679a263bc74417e9836d4653fef1a7aebe4fe2a72b02ccc4a7f14fbe83233b6c3c3e1c698f9e4d85fb30f13f2a54be
7
+ data.tar.gz: b7d0d3538d01d2f93630d2ac158174e623c404cd0efb876a8650d5a2756e44df42266d3f20833c67935d1d87eea44cbbf5c430bc644ae9bd2bf6d9dd1b9111d8
data/COPYING ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007-2010 Charles Lowe
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
data/Changelog.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ == Unreleased:
2
+
3
+ - Replaced DES-algorithm with Ruby's built-in OpenSSL wrapper instead
4
+ - Parse framebuffer width/height and hostname from ServerInitialisation
5
+ - Added a project Gemfile
6
+
7
+ == 1.1.0 / 2012-06-03
8
+
9
+ - Fixes to support ruby 1.9 (jedi4ever & codemonkeyjohn).
10
+
11
+ == 1.0.1 / 2011-09-15
12
+
13
+ - Split out gemspec into separate file and use for Rakefile.
14
+ - Add homepage and rubyforge project to gemspec.
15
+
16
+ == 1.0.0 / 2008-08-29
17
+
18
+ - First public release
19
+
data/README.rdoc ADDED
@@ -0,0 +1,33 @@
1
+ = Introduction
2
+
3
+ The ruby-vnc library provides for simple rfb-protocol based control of a
4
+ VNC server. This can be used, eg, to automate applications (sometimes there
5
+ is no better way), or script some sort of interaction.
6
+
7
+ Being VNC based gives it the advantage of being able to be run against
8
+ servers on any platform.
9
+
10
+ The primary documentation is for the Net::VNC class.
11
+
12
+ = Running the tests
13
+
14
+ * Boot up the two VNC servers with `docker-compose up`.
15
+
16
+ * Run the test-suite with `bundle exec rake spec`.
17
+
18
+ = Resources
19
+
20
+ * {The Remote Framebuffer Protocol (RFC6143)}[https://tools.ietf.org/html/rfc6143]
21
+
22
+ * https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst
23
+
24
+ = Thanks
25
+
26
+ Code borrows a lot from Tim Waugh's excellent rfbplaymacro. So far all it
27
+ really offers on top of that is access to the host clipboard, and the ease
28
+ with which it can be scripted, ie taking conditional actions based on the
29
+ contents thereof.
30
+
31
+ = P.S.
32
+
33
+ This gem forked from [https://github.com/aquasync/ruby-vnc] for normally reads and additions. This is my frist gem publishing, so be patient.
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake'
3
+
4
+ task :default => :spec
5
+
6
+ desc 'Run all specs'
7
+ begin
8
+ require 'rspec/core/rake_task'
9
+ RSpec::Core::RakeTask.new(:spec)
10
+ rescue LoadError
11
+ end
12
+
13
+ desc 'Run all specs and generate html spec document'
14
+ namespace :spec do
15
+ RSpec::Core::RakeTask.new :html do |t|
16
+ t.rspec_opts = ['--format html --out spec.html']
17
+ end
18
+ end
19
+
20
+ require 'rdoc/task'
21
+
22
+ Rake::RDocTask.new do |t|
23
+ t.rdoc_dir = 'doc'
24
+ t.rdoc_files.include 'lib/**/*.rb'
25
+ t.rdoc_files.include 'README'
26
+ t.title = 'evilcap-ruby-vnc documentation'
27
+ t.options += %w[--line-numbers --inline-source --tab-width 2]
28
+ t.main = 'README'
29
+ end
30
+
31
+ require 'rubygems/package_task'
32
+
33
+ spec = eval File.read('evilcap-ruby-vnc.gemspec')
34
+ Gem::PackageTask.new(spec) do |pkg|
35
+ pkg.need_tar = false
36
+ pkg.need_zip = false
37
+ pkg.package_dir = 'build'
38
+ end
39
+
data/data/keys.yaml ADDED
@@ -0,0 +1,128 @@
1
+ backspace: 0xff08
2
+ tab: 0xff09
3
+ linefeed: 0xff0a
4
+ clear: 0xff0b
5
+ return: 0xff0d
6
+ pause: 0xff13
7
+ scroll_lock: 0xff14
8
+ sys_req: 0xff15
9
+ escape: 0xff1b
10
+ delete: 0xffff
11
+ home: 0xff50
12
+ left: 0xff51
13
+ up: 0xff52
14
+ right: 0xff53
15
+ down: 0xff54
16
+ prior: 0xff55
17
+ page_up: 0xff55
18
+ next: 0xff56
19
+ page_down: 0xff56
20
+ end: 0xff57
21
+ begin: 0xff58
22
+ select: 0xff60
23
+ print: 0xff61
24
+ execute: 0xff62
25
+ insert: 0xff63
26
+ undo: 0xff65
27
+ redo: 0xff66
28
+ menu: 0xff67
29
+ find: 0xff68
30
+ cancel: 0xff69
31
+ help: 0xff6a
32
+ break: 0xff6b
33
+ mode_switch: 0xff7e
34
+ script_switch: 0xff7e
35
+ num_lock: 0xff7f
36
+ kp_space: 0xff80
37
+ kp_tab: 0xff89
38
+ kp_enter: 0xff8d
39
+ kp_f1: 0xff91
40
+ kp_f2: 0xff92
41
+ kp_f3: 0xff93
42
+ kp_f4: 0xff94
43
+ kp_home: 0xff95
44
+ kp_left: 0xff96
45
+ kp_up: 0xff97
46
+ kp_right: 0xff98
47
+ kp_down: 0xff99
48
+ kp_prior: 0xff9a
49
+ kp_page_up: 0xff9a
50
+ kp_next: 0xff9b
51
+ kp_page_down: 0xff9b
52
+ kp_end: 0xff9c
53
+ kp_begin: 0xff9d
54
+ kp_insert: 0xff9e
55
+ kp_delete: 0xff9f
56
+ kp_equal: 0xffbd
57
+ kp_multiply: 0xffaa
58
+ kp_add: 0xffab
59
+ kp_separator: 0xffac
60
+ kp_subtract: 0xffad
61
+ kp_decimal: 0xffae
62
+ kp_divide: 0xffaf
63
+ kp_0: 0xffb0
64
+ kp_1: 0xffb1
65
+ kp_2: 0xffb2
66
+ kp_3: 0xffb3
67
+ kp_4: 0xffb4
68
+ kp_5: 0xffb5
69
+ kp_6: 0xffb6
70
+ kp_7: 0xffb7
71
+ kp_8: 0xffb8
72
+ kp_9: 0xffb9
73
+ f1: 0xffbe
74
+ f2: 0xffbf
75
+ f3: 0xffc0
76
+ f4: 0xffc1
77
+ f5: 0xffc2
78
+ f6: 0xffc3
79
+ f7: 0xffc4
80
+ f8: 0xffc5
81
+ f9: 0xffc6
82
+ f10: 0xffc7
83
+ f11: 0xffc8
84
+ f12: 0xffc9
85
+ f13: 0xffca
86
+ f14: 0xffcb
87
+ f15: 0xffcc
88
+ f16: 0xffcd
89
+ f17: 0xffce
90
+ f18: 0xffcf
91
+ f19: 0xffd0
92
+ f20: 0xffd1
93
+ f21: 0xffd2
94
+ f22: 0xffd3
95
+ f23: 0xffd4
96
+ f24: 0xffd5
97
+ f25: 0xffd6
98
+ f26: 0xffd7
99
+ f27: 0xffd8
100
+ f28: 0xffd9
101
+ f29: 0xffda
102
+ f30: 0xffdb
103
+ f31: 0xffdc
104
+ f32: 0xffdd
105
+ f33: 0xffde
106
+ f34: 0xffdf
107
+ f35: 0xffe0
108
+ left_shift: 0xffe1
109
+ right_shift: 0xffe2
110
+ left_control: 0xffe3
111
+ right_control: 0xffe4
112
+ caps_lock: 0xffe5
113
+ shift_lock: 0xffe6
114
+ left_meta: 0xffe7
115
+ right_meta: 0xffe8
116
+ left_alt: 0xffe9
117
+ right_alt: 0xffea
118
+ left_super: 0xffeb
119
+ right_super: 0xffec
120
+ left_hyper: 0xffed
121
+ right_hyper: 0xffee
122
+ # some convenience aliases
123
+ shift: 0xffe1
124
+ control: 0xffe3
125
+ meta: 0xffe7
126
+ alt: 0xffe9
127
+ super: 0xffeb
128
+ hyper: 0xffed
@@ -0,0 +1,55 @@
1
+ require 'openssl'
2
+
3
+ # MIT-licensed code by Andrew Dorofeyev
4
+ # from https://github.com/d-theus/vncrec-ruby
5
+
6
+ # The server sends a random 16-byte challenge:
7
+ #
8
+ # +--------------+--------------+-------------+
9
+ # | No. of bytes | Type [Value] | Description |
10
+ # +--------------+--------------+-------------+
11
+ # | 16 | U8 | challenge |
12
+ # +--------------+--------------+-------------+
13
+ #
14
+ # The client encrypts the challenge with DES (ECB), using a password supplied
15
+ # by the user as the key. To form the key, the password is truncated
16
+ # to eight characters, or padded with null bytes on the right.
17
+ # Actually, each byte is also reversed. Challenge string is split
18
+ # in two chunks of 8 bytes, which are encrypted separately and clashed together
19
+ # again. The client then sends the resulting 16-byte response:
20
+ #
21
+ # +--------------+--------------+-------------+
22
+ # | No. of bytes | Type [Value] | Description |
23
+ # +--------------+--------------+-------------+
24
+ # | 16 | U8 | response |
25
+ # +--------------+--------------+-------------+
26
+ #
27
+ # The protocol continues with the SecurityResult message.
28
+
29
+ module Cipher
30
+ class VNCDES
31
+ attr_reader :key
32
+
33
+ def initialize(key)
34
+ @key = normalized(key[0..7])
35
+ self
36
+ end
37
+
38
+ def encrypt(challenge)
39
+ chunks = [challenge.slice(0, 8), challenge.slice(8, 8)]
40
+ cipher = OpenSSL::Cipher::DES.new(:ECB)
41
+ cipher.encrypt
42
+ cipher.key = self.key
43
+ chunks.reduce('') { |a, e| cipher.reset; a << cipher.update(e) }.force_encoding('UTF-8')
44
+ end
45
+
46
+ private
47
+
48
+ def normalized(key)
49
+ rev = ->(n) { (0...8).reduce(0) { |a, e| a + 2**e * n[7 - e] } }
50
+ inv = key.each_byte.map { |b| rev[b].chr }.join
51
+ inv.ljust(8, "\x00")
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,175 @@
1
+ require 'vncrec'
2
+ require 'chunky_png'
3
+
4
+ module Net::RFB
5
+
6
+ # Manage FrameBuffer pixel data for RFB protocol
7
+ # This is a little wrapper for the `Proxy` class in vncrec-ruby https://github.com/d-theus/vncrec-ruby
8
+ class FrameBuffer
9
+ class VNCRecAuthStub
10
+ def initialize(io, *options)
11
+ end
12
+ end
13
+
14
+ # @param io [IO, #read, #sysread, #syswrite, #read_nonblock] string stream from VNC server.
15
+ # @param w [Integer] width of the screen area
16
+ # @param h [Integer] height of the screen area
17
+ # @param bpp [Symbol] bits per pixel (BGR8 or BGRA)
18
+ # @param encodings [Array<Symbol>] encoding (RAW or HEXTILE or ZRLE) default: RAW
19
+ def initialize(io, w, h, bpp, encodings=nil)
20
+ @cb_mutex = Monitor.new
21
+ @cb_cv = @cb_mutex.new_cond
22
+
23
+ @encodings = encodings
24
+
25
+ @vnc_rec_pix_fmt = convert_to_vnc_rec_pix_fmt bpp
26
+
27
+ @proxy = VNCRec::RFB::Proxy.new(io, nil, nil, nil, [VNCRecAuthStub, nil])
28
+ @proxy.prepare_framebuffer w, h, @vnc_rec_pix_fmt[:bpp]
29
+ end
30
+
31
+ def send_initial_data
32
+ # set encoding
33
+ unless self.set_encodings @encodings
34
+ raise 'Error while setting encoding'
35
+ end
36
+
37
+ # set pixel format
38
+ self.set_pixel_format @vnc_rec_pix_fmt
39
+
40
+ # request all pixel data
41
+ self.request_update_fb incremental: false
42
+ end
43
+
44
+ # raw pixel data of screen
45
+ def pixel_data
46
+ @proxy.data
47
+ end
48
+
49
+ # 32bit RGBA pixel data of screen
50
+ def rgba_pixel_data
51
+ px = self.pixel_data
52
+ raise 'Error in get raw pixel_data.' unless px
53
+ self.class.convert_raw_pixel_data_to_rgba px, @vnc_rec_pix_fmt[:string]
54
+ end
55
+
56
+ # convert raw pixel data to 32bit RGBA values according to VNC pixel format
57
+ # @param px [String] binary pixel data
58
+ # @param pix_fmt [String] pixel format (bgra, bgr8)
59
+ # @return [Array<Integer>] array of 32bit pixel data
60
+ def self.convert_raw_pixel_data_to_rgba(px, pix_fmt)
61
+ # see https://github.com/d-theus/vncrec-ruby/blob/master/lib/vncrec/constants.rb
62
+ case pix_fmt.to_s
63
+ when 'bgra'
64
+ # convert 32bit BGRA -> 32bit RGBA
65
+ px = px.unpack("V*")
66
+ px.map! { |p| (p << 8) | 0xff }
67
+ when 'bgr8'
68
+ # convert 8bit BGR -> 32bit RGBA
69
+ px = px.unpack("C*")
70
+ px.map! do |p|
71
+ r = (p & 0b00000111)
72
+ g = (p & 0b00111000) >> 3
73
+ b = (p & 0b11000000) >> 6
74
+ ((r * 36) << 24) | ((g * 36) << 16) | ((b * 85) << 8) | 0xff
75
+ end
76
+ else
77
+ raise "unknown pixel format #{pix_fmt.inspect}"
78
+ end
79
+ end
80
+
81
+ # Set a way that server should use to represent pixel data
82
+ # @param [Symbol|String] pixel format:
83
+ # * :BGR8
84
+ # * :BGRA
85
+ def set_pixel_format(format)
86
+ @proxy.set_pixel_format convert_to_vnc_rec_pix_fmt(format)
87
+ end
88
+
89
+ # Set way of encoding video frames.
90
+ # @param encodings [Symbol|String] list of encoding of video data used to transfer.
91
+ # * :RAW
92
+ # * :HEXTILE
93
+ # * :ZRLE
94
+ def set_encodings(*encodings)
95
+ @proxy.set_encodings [encodings].flatten.compact.map{|sym| VNCRec::const_get "ENC_#{sym}"}
96
+ end
97
+
98
+ # Send request for update framebuffer.
99
+ # if block given, called it with pixel data after the response received.
100
+ # @param [Boolean] incremental incremental, request just difference
101
+ # between previous and current framebuffer state.
102
+ # @param x [Integer]
103
+ # @param y [Integer]
104
+ # @param w [Integer]
105
+ # @param h [Integer]
106
+ # @param wait_for_response [Boolean] if true, wait for a FramebufferUpdate response
107
+ def request_update_fb(incremental: true, x: nil, y: nil, w: nil, h: nil, wait_for_response: false)
108
+ @cb_mutex.synchronize do
109
+ @proxy.fb_update_request incremental ? 1 : 0, x||0, y||0, w||@proxy.w, h||@proxy.h
110
+
111
+ if wait_for_response
112
+ @cb_cv.wait
113
+ end
114
+ end
115
+ end
116
+
117
+ def handle_response(t)
118
+ case t
119
+ when 0 # ----------------------------------------------- FramebufferUpdate
120
+ ret = handle_fb_update
121
+ @cb_mutex.synchronize do
122
+ @cb_cv.broadcast
123
+ end
124
+ return ret
125
+ when 1 # --------------------------------------------- SetColourMapEntries
126
+ return handle_set_colormap_entries
127
+ end
128
+ end
129
+
130
+ # save current screen pixel data as PNG image
131
+ # @param dest [String|IO|nil] destination file path, or IO-object, or nil
132
+ # @return [String] PNG binary data as string when dest is null
133
+ # [true] else case
134
+ def save_pixel_data_as_png(dest=nil)
135
+ self.request_update_fb(wait_for_response: true)
136
+
137
+ image = ChunkyPNG::Image.new(@proxy.w, @proxy.h, rgba_pixel_data)
138
+
139
+ if dest.is_a? IO
140
+ # write to IO-object
141
+ image.write dest
142
+ elsif dest.is_a?(String) || dest.is_a?(Pathname)
143
+ # write to file
144
+ image.save dest.to_s
145
+ elsif dest.nil?
146
+ # return binary data as string
147
+ return image.to_blob
148
+ else
149
+ raise ArgumentError, "Unsupported destination type #{dest.inspect}"
150
+ end
151
+ true
152
+ end
153
+
154
+ private
155
+
156
+ # convert pixel_format symbol to VNCRec::PIX_FMT_XXX symbol.
157
+ # @param pix_fmt [Symbol|String] bits per pixel (BGR8 or BGRA)
158
+ def convert_to_vnc_rec_pix_fmt(pix_fmt)
159
+ return pix_fmt if pix_fmt.is_a?(Hash)
160
+ pf = pix_fmt.to_s.prepend('PIX_FMT_').upcase.to_sym
161
+ raise ArgumentError, "Unsupported pixel_format '#{pix_fmt}', now supported values are: BGR8, BGRA" unless VNCRec.const_defined? pf
162
+ VNCRec.const_get(pf)
163
+ end
164
+
165
+ # Receives data and applies diffs(if incremental) to the @data
166
+ def handle_fb_update
167
+ @proxy.handle_fb_update
168
+ end
169
+
170
+ # @return [Array] palette
171
+ def handle_set_colormap_entries
172
+ @proxy.handle_colormap_update
173
+ end
174
+ end
175
+ end
data/lib/net/vnc.rb ADDED
@@ -0,0 +1,371 @@
1
+ require 'socket'
2
+ require 'yaml'
3
+ require 'thread'
4
+ require 'cipher/vncdes'
5
+ require 'net/vnc/version'
6
+
7
+ 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
+ #
16
+ # require 'net/vnc'
17
+ # Net::VNC.open 'localhost:0', :shared => true, :password => 'mypass' do |vnc|
18
+ # vnc.pointer_move 10, 10
19
+ # vnc.type 'xclock'
20
+ # vnc.key_press :return
21
+ # end
22
+ #
23
+ # = TODO
24
+ #
25
+ # * The server read loop seems a bit iffy. Not sure how best to do it.
26
+ # * Should probably be changed to be more of a lower-level protocol wrapping thing, with the
27
+ # actual VNCClient sitting on top of that. all it should do is read/write the packets over
28
+ # the socket.
29
+ #
30
+ class VNC
31
+ class PointerState
32
+ attr_reader :x, :y, :button
33
+
34
+ def initialize vnc
35
+ @x = @y = @button = 0
36
+ @vnc = vnc
37
+ end
38
+
39
+ # could have the same for x=, and y=
40
+ def button= button
41
+ @button = button
42
+ refresh
43
+ end
44
+
45
+ def update x, y, button=@button
46
+ @x, @y, @button = x, y, button
47
+ refresh
48
+ end
49
+
50
+ def refresh
51
+ packet = 0.chr * 6
52
+ packet[0] = 5.chr
53
+ packet[1] = button.chr
54
+ packet[2, 2] = [x].pack 'n'
55
+ packet[4, 2] = [y].pack 'n'
56
+ @vnc.socket.write packet
57
+ end
58
+ end
59
+
60
+ BASE_PORT = 5900
61
+ CHALLENGE_SIZE = 16
62
+ DEFAULT_OPTIONS = {
63
+ :shared => false,
64
+ :wait => 0.1,
65
+ :pix_fmt => :BGRA,
66
+ :encoding => :RAW
67
+ }
68
+
69
+ keys_file = File.dirname(__FILE__) + '/../../data/keys.yaml'
70
+ KEY_MAP = YAML.load_file(keys_file).inject({}) { |h, (k, v)| h.update k.to_sym => v }
71
+ def KEY_MAP.[] key
72
+ super or raise ArgumentError.new('Invalid key name - %s' % key)
73
+ end
74
+
75
+ attr_reader :server, :display, :options, :socket, :pointer, :desktop_name
76
+
77
+ def initialize display=':0', options={}
78
+ @server = 'localhost'
79
+ if display =~ /^(.*)(:\d+)$/
80
+ @server, display = $1, $2
81
+ end
82
+ @display = display[1..-1].to_i
83
+ @desktop_name = nil
84
+ @options = DEFAULT_OPTIONS.merge options
85
+ @clipboard = nil
86
+ @fb = nil
87
+ @pointer = PointerState.new self
88
+ @mutex = Mutex.new
89
+ connect
90
+ @packet_reading_state = nil
91
+ @packet_reading_thread = Thread.new { packet_reading_thread }
92
+ end
93
+
94
+ def self.open display=':0', options={}
95
+ vnc = new display, options
96
+ if block_given?
97
+ begin
98
+ yield vnc
99
+ ensure
100
+ vnc.close
101
+ end
102
+ else
103
+ vnc
104
+ end
105
+ end
106
+
107
+ def port
108
+ BASE_PORT + @display
109
+ end
110
+
111
+ def connect
112
+ @socket = TCPSocket.open(server, port)
113
+ unless socket.read(12) =~ /^RFB (\d{3}.\d{3})\n$/
114
+ raise 'invalid server response'
115
+ end
116
+ @server_version = $1
117
+ socket.write "RFB 003.003\n"
118
+ data = socket.read(4)
119
+ auth = data.to_s.unpack('N')[0]
120
+ case auth
121
+ when 0, nil
122
+ raise 'connection failed'
123
+ when 1
124
+ # ok...
125
+ when 2
126
+ password = @options[:password] or raise 'Need to authenticate but no password given'
127
+ challenge = socket.read CHALLENGE_SIZE
128
+ response = Cipher::VNCDES.new(password).encrypt(challenge)
129
+ socket.write response
130
+ ok = socket.read(4).to_s.unpack('N')[0]
131
+ raise 'Unable to authenticate - %p' % ok unless ok == 0
132
+ else
133
+ raise 'Unknown authentication scheme - %d' % auth
134
+ end
135
+
136
+ # ClientInitialisation
137
+ socket.write((options[:shared] ? 1 : 0).chr)
138
+
139
+ # ServerInitialisation
140
+ @framebuffer_width = socket.read(2).to_s.unpack('n')[0].to_i
141
+ @framebuffer_height = socket.read(2).to_s.unpack('n')[0].to_i
142
+
143
+ # TODO: parse this.
144
+ pixel_format = socket.read(16)
145
+
146
+ # read the name in byte chunks of 20
147
+ name_length = socket.read(4).to_s.unpack('N')[0]
148
+ @desktop_name = [].tap do |it|
149
+ while name_length > 0
150
+ len = [20, name_length].min
151
+ it << socket.read(len)
152
+ name_length -= len
153
+ end
154
+ end.join
155
+
156
+ _load_frame_buffer
157
+ end
158
+
159
+ # this types +text+ on the server
160
+ def type text, options={}
161
+ packet = 0.chr * 8
162
+ packet[0] = 4.chr
163
+ text.split(//).each do |char|
164
+ packet[7] = char[0]
165
+ packet[1] = 1.chr
166
+ socket.write packet
167
+ packet[1] = 0.chr
168
+ socket.write packet
169
+ end
170
+ wait options
171
+ end
172
+
173
+ # this takes an array of keys, and successively holds each down then lifts them up in
174
+ # reverse order.
175
+ # FIXME: should wait. can't recurse in that case.
176
+ def key_press(*args)
177
+ options = Hash === args.last ? args.pop : {}
178
+ keys = args
179
+ raise ArgumentError, 'Must have at least one key argument' if keys.empty?
180
+ begin
181
+ key_down keys.first
182
+ if keys.length == 1
183
+ yield if block_given?
184
+ else
185
+ key_press(*(keys[1..-1] + [options]))
186
+ end
187
+ ensure
188
+ key_up keys.first
189
+ end
190
+ end
191
+
192
+ def get_key_code(which)
193
+ case which
194
+ when String
195
+ if which.length != 1
196
+ raise ArgumentError, 'can only get key_code of single character strings'
197
+ end
198
+ which[0].ord
199
+ when Symbol
200
+ KEY_MAP[which]
201
+ when Integer
202
+ which
203
+ else
204
+ raise ArgumentError, "unsupported key value: #{which.inspect}"
205
+ end
206
+ end
207
+ private :get_key_code
208
+
209
+ def key_down which, options={}
210
+ packet = 0.chr * 8
211
+ packet[0] = 4.chr
212
+ key_code = get_key_code which
213
+ packet[4, 4] = [key_code].pack('N')
214
+ packet[1] = 1.chr
215
+ socket.write packet
216
+ wait options
217
+ end
218
+
219
+ def key_up which, options={}
220
+ packet = 0.chr * 8
221
+ packet[0] = 4.chr
222
+ key_code = get_key_code which
223
+ packet[4, 4] = [key_code].pack('N')
224
+ packet[1] = 0.chr
225
+ socket.write packet
226
+ wait options
227
+ end
228
+
229
+ def pointer_move x, y, options={}
230
+ # options[:relative]
231
+ pointer.update x, y
232
+ wait options
233
+ end
234
+
235
+ BUTTON_MAP = {
236
+ :left => 0
237
+ }
238
+
239
+ def button_press button=:left, options={}
240
+ begin
241
+ button_down button, options
242
+ yield if block_given?
243
+ ensure
244
+ button_up button, options
245
+ end
246
+ end
247
+
248
+ def button_down which=:left, options={}
249
+ button = BUTTON_MAP[which] || which
250
+ raise ArgumentError, 'Invalid button - %p' % which unless (0..2) === button
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) === button
258
+ pointer.button &= ~(1 << button)
259
+ wait options
260
+ end
261
+
262
+ # take screenshot as PNG image
263
+ # @param dest [String|IO|nil] destination file path, or IO-object, or nil
264
+ # @return [String] PNG binary data as string when dest is null
265
+ # [true] else case
266
+ def take_screenshot(dest=nil)
267
+ fb = _load_frame_buffer # on-demand loading
268
+ fb.save_pixel_data_as_png dest
269
+ end
270
+
271
+ def wait options={}
272
+ sleep options[:wait] || @options[:wait]
273
+ end
274
+
275
+ def close
276
+ # destroy packet reading thread
277
+ if @packet_reading_state == :loop
278
+ @packet_reading_state = :stop
279
+ while @packet_reading_state
280
+ # do nothing
281
+ end
282
+ end
283
+ socket.close
284
+ end
285
+
286
+ def reconnect
287
+ 60.times do
288
+ if @packet_reading_state.nil?
289
+ connect
290
+ @packet_reading_thread = Thread.new { packet_reading_thread }
291
+ return true
292
+ end
293
+ sleep 0.5
294
+ end
295
+ warn 'reconnect failed because packet reading state had not been stopped for 30 seconds.'
296
+ false
297
+ end
298
+
299
+ def clipboard
300
+ if block_given?
301
+ @clipboard = nil
302
+ yield
303
+ 60.times do
304
+ clipboard = @mutex.synchronize { @clipboard }
305
+ return clipboard if clipboard
306
+ sleep 0.5
307
+ end
308
+ warn 'clipboard still empty after 30s'
309
+ nil
310
+ else
311
+ @mutex.synchronize { @clipboard }
312
+ end
313
+ end
314
+
315
+ def clipboard= text
316
+ text = text.to_s.gsub(/\R/, "\n") # eol of ClientCutText's text is LF
317
+ byte_size = text.to_s.bytes.size
318
+ packet = 0.chr * (8 + byte_size)
319
+ packet[0] = 6.chr # message-type: 6 (ClientCutText)
320
+ packet[4, 4] = [byte_size].pack('N') # length
321
+ packet[8, byte_size] = text
322
+ socket.write(packet)
323
+ @clipboard = text
324
+ end
325
+
326
+ private
327
+
328
+ def read_packet type
329
+ case type
330
+ when 0 # ----------------------------------------------- FramebufferUpdate
331
+ @fb.handle_response type if @fb
332
+ when 1 # --------------------------------------------- SetColourMapEntries
333
+ @fb.handle_response type if @fb
334
+ when 2 # ------------------------------------------------------------ Bell
335
+ nil # not support
336
+ when 3 # --------------------------------------------------- ServerCutText
337
+ socket.read 3 # discard padding bytes
338
+ len = socket.read(4).unpack('N')[0]
339
+ @mutex.synchronize { @clipboard = socket.read len }
340
+ else
341
+ warn 'unhandled server packet type - %d' % type
342
+ end
343
+ end
344
+
345
+ def packet_reading_thread
346
+ @packet_reading_state = :loop
347
+ loop do
348
+ begin
349
+ break if @packet_reading_state != :loop
350
+ next unless IO.select [socket], nil, nil, 2
351
+ type = socket.read(1)[0]
352
+ read_packet type.ord
353
+ rescue
354
+ warn "exception in packet_reading_thread: #{$!.class}:#{$!}\n#{$!.backtrace}"
355
+ break
356
+ end
357
+ end
358
+ @packet_reading_state = nil
359
+ end
360
+
361
+ def _load_frame_buffer
362
+ unless @fb
363
+ require 'net/rfb/frame_buffer'
364
+
365
+ @fb = Net::RFB::FrameBuffer.new @socket, @framebuffer_width, @framebuffer_height, @options[:pix_fmt], @options[:encoding]
366
+ @fb.send_initial_data
367
+ end
368
+ @fb
369
+ end
370
+ end
371
+ end
@@ -0,0 +1,6 @@
1
+ module Net
2
+ class VNC
3
+ VERSION = '1.2.0'.freeze
4
+ end
5
+ end
6
+
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+ require 'cipher/vncdes'
3
+
4
+ RSpec.describe Cipher::VNCDES do
5
+ it 'should pad key with zeroes if key is shorter than 8 characters' do
6
+ key = Cipher::VNCDES.new('test').key
7
+
8
+ expect(key.size).to eq 8
9
+ expect(key[4..7]).to eq(0.chr * 4)
10
+ end
11
+
12
+ it 'should cut the key if the key is longer than 8 characters' do
13
+ expect(Cipher::VNCDES.new('iamdefinitelylongerthan8characters').key.size).to eq 8
14
+ end
15
+
16
+ it 'should correctly encrypt keys' do
17
+ encrypted_string = Cipher::VNCDES.new('matzisnicesowearenice').encrypt("\x9D\xBBU\n\x05b\x96L \b'&\x18\xCE(\xD8")
18
+ expect(encrypted_string.encoding.to_s).to eq 'UTF-8'
19
+ expect(encrypted_string.size).to eq 16
20
+ expect(encrypted_string).to eq "2\x95\xA7\xAE\xD4A\xF3\xDCt\x82d\e\xAE\x8A\xB9c"
21
+ end
22
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+ require 'net/vnc'
3
+
4
+ =begin
5
+ class SocketMock
6
+ class MockIOError < IOError
7
+ end
8
+
9
+ def initialize
10
+ # this can be used to make detailed assertions
11
+ @trace = []
12
+ @read_buf = ''
13
+ @write_buf = ''
14
+ end
15
+
16
+ def read len
17
+ @trace << [:read, len]
18
+ if @read_buf.length < len
19
+ msg = 'bad socket read sequence - read(%d) but only %d byte(s) available' % [len, @read_buf.length]
20
+ raise MockIOError, msg
21
+ end
22
+ @read_buf.slice! 0, len
23
+ end
24
+
25
+ def write data
26
+ @trace << [:write, data]
27
+ @write_buf << data
28
+ end
29
+ end
30
+
31
+ class VNCServerSocketMock < SocketMock
32
+ TICK_TIME = 0.1
33
+
34
+ def initialize(&block)
35
+ super
36
+
37
+ @pending_read = nil
38
+ obj = self
39
+ @t = Thread.new { block.call obj; @pending_read = nil }
40
+ 100.times do |i|
41
+ break if @pending_read
42
+ if i == 99
43
+ msg = 'blah'
44
+ raise MockIOError, msg
45
+ end
46
+ sleep TICK_TIME
47
+ end
48
+ end
49
+
50
+ def run
51
+ yield
52
+ 100.times do |i|
53
+ break unless @pending_read
54
+ if i == 99
55
+ msg = 'missing socket write sequence'
56
+ raise MockIOError, msg
57
+ end
58
+ sleep TICK_TIME
59
+ end
60
+ raise 'wrote to much' if @write_buf.length != 0
61
+ raise 'did not read enough' if @read_buf.length != 0
62
+ end
63
+
64
+ def read len
65
+ @trace << [:read, len]
66
+ 100.times do |i|
67
+ break if @read_buf.length >= len
68
+ if i == 99
69
+ msg = 'timeout during socket read sequence - read(%d) but only %d byte(s) available' % [len, @read_buf.length]
70
+ raise MockIOError, msg
71
+ end
72
+ sleep TICK_TIME
73
+ end
74
+ @read_buf.slice! 0, len
75
+ end
76
+
77
+ def write data
78
+ unless @read_buf.empty?
79
+ raise MockIOError, 'tried to write with non empty read buffer - (%p, %p)' % [@read_buf, data]
80
+ end
81
+ super
82
+ if !@pending_read
83
+ raise MockIOError, "wrote to socket but server is not expecting it"
84
+ end
85
+ if @write_buf.length >= @pending_read
86
+ @pending_read = @write_buf.slice!(0, @pending_read)
87
+ sleep TICK_TIME while @pending_read.is_a? String
88
+ end
89
+ end
90
+
91
+ def provide_data data
92
+ @read_buf << data
93
+ end
94
+
95
+ def expect_data len
96
+ @pending_read = len
97
+ sleep TICK_TIME while @pending_read.is_a? Fixnum
98
+ @pending_read
99
+ end
100
+ end
101
+
102
+ describe 'Net::VNC' do
103
+ VNC = Net::VNC
104
+
105
+ it 'should do something' do
106
+ =begin
107
+ socket_mock.should_receive(:read).once.ordered.with(12).and_return("RFB 003.003\n")
108
+ socket_mock.should_receive(:write).once.ordered.with(/^RFB (\d{3}.\d{3})\n$/)
109
+ socket_mock.should_receive(:read).once.ordered.with(4).and_return([1].pack('N'))
110
+ socket_mock.should_receive(:write).once.ordered.with("\000")
111
+ socket_mock.should_receive(:read).once.ordered.with(20).and_return('')
112
+ socket_mock.should_receive(:read).once.ordered.with(4).and_return([0].pack('N'))
113
+ #m = mock('my mock')
114
+ #m.should_receive(:test1).ordered.once.with('argument').and_return(1)
115
+ #m.should_receive(:test2).ordered.once.with('argument').and_return(2)
116
+ #p m.test1('argument')
117
+ #p m.test2('argument')
118
+ vnc = VNC.open('192.168.0.1:0')
119
+ #=end
120
+
121
+ server = VNCServerSocketMock.new do |s|
122
+ s.provide_data "RFB 003.003\n"
123
+ p :read => s.expect_data(12)
124
+ s.provide_data [1].pack('N')
125
+ p :read => s.expect_data(1)
126
+ s.provide_data ' ' * 20
127
+ s.provide_data [0].pack('N')
128
+ end
129
+ server.run do
130
+ TCPSocket.should_receive(:open).with('192.168.0.1', 5900).and_return(server)
131
+ vnc = VNC.open('192.168.0.1:0')
132
+ end
133
+ end
134
+ end
135
+ =end
136
+
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'net/vnc'
3
+
4
+ RSpec.describe Net::VNC do
5
+ NO_AUTH_SERVER_DISPLAY = ':1'
6
+ WITH_AUTH_SERVER_DISPLAY = ':2'
7
+
8
+ context 'no auth' do
9
+ it 'should connect with no password' do
10
+ Net::VNC.open(NO_AUTH_SERVER_DISPLAY) do |vnc|
11
+ vnc.pointer_move(10, 15)
12
+ expect(vnc.pointer.x).to eq 10
13
+ expect(vnc.pointer.y).to eq 15
14
+
15
+ vnc.pointer_move(20, 25)
16
+ expect(vnc.pointer.x).to eq 20
17
+ expect(vnc.pointer.y).to eq 25
18
+ end
19
+ end
20
+
21
+ it 'should connect with password even though it is not needed' do
22
+ Net::VNC.open(NO_AUTH_SERVER_DISPLAY, password: 'password') do |vnc|
23
+ vnc.pointer_move(10, 15)
24
+ expect(vnc.pointer.x).to eq 10
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'with auth' do
30
+ it 'should connect with a password' do
31
+ Net::VNC.open(WITH_AUTH_SERVER_DISPLAY, password: 'matzisnicesowearenice') do |vnc|
32
+ vnc.pointer_move(10, 15)
33
+ expect(vnc.pointer.x).to eq 10
34
+ expect(vnc.pointer.y).to eq 15
35
+ end
36
+ end
37
+
38
+ it 'should give error with a wrong password' do
39
+ expect { Net::VNC.open(WITH_AUTH_SERVER_DISPLAY, password: 'wrongPasssword') }.to raise_error(RuntimeError, 'Unable to authenticate - 1')
40
+ end
41
+
42
+ it 'should give error with no password' do
43
+ expect { Net::VNC.open(WITH_AUTH_SERVER_DISPLAY) }.to raise_error(RuntimeError, 'Need to authenticate but no password given')
44
+ end
45
+ end
46
+
47
+ context 'screenshotting' do
48
+ def verify_screenshot(input)
49
+ image_size = ImageSize.path(input)
50
+ expect(image_size.format).to eq :png
51
+ expect(image_size.width).to eq 1366
52
+ expect(image_size.height).to eq 768
53
+ end
54
+
55
+ it 'should allow you to take a screenshot with a path' do
56
+ Tempfile.open('ruby-vnc-spec') do |screenshotfile|
57
+ Net::VNC.open(NO_AUTH_SERVER_DISPLAY) do |vnc|
58
+ vnc.pointer_move(10, 15)
59
+ vnc.take_screenshot(screenshotfile.path)
60
+ end
61
+ verify_screenshot(screenshotfile.path)
62
+ end
63
+ end
64
+
65
+ it 'should allow you to take a screenshot with a blob' do
66
+ Tempfile.open('ruby-vnc-spec-blob') do |screenshotfile|
67
+ vnc = Net::VNC.open(NO_AUTH_SERVER_DISPLAY)
68
+ begin
69
+ vnc.pointer_move(10, 15)
70
+ blob = vnc.take_screenshot(nil)
71
+ screenshotfile.write(blob)
72
+ ensure
73
+ vnc.close
74
+ end
75
+
76
+ verify_screenshot(screenshotfile.path)
77
+ end
78
+ end
79
+
80
+ it 'should allow you to take a screenshot with a IO-object' do
81
+ screenshotfile = File.new("out.png", "w")
82
+
83
+ begin
84
+ Net::VNC.open(NO_AUTH_SERVER_DISPLAY) do |vnc|
85
+ vnc.pointer_move(10, 15)
86
+ vnc.take_screenshot(screenshotfile)
87
+ end
88
+ verify_screenshot(screenshotfile)
89
+ ensure
90
+ screenshotfile.close
91
+ File.delete(screenshotfile)
92
+ end
93
+ end
94
+ end
95
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: evilcap-ruby-vnc
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Charles Lowe
8
+ - Egor Topolnyak
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-07-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: vncrec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 1.0.6
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 1.0.6
28
+ - !ruby/object:Gem::Dependency
29
+ name: chunky_png
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 1.3.0
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.3.0
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '12.3'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '12.3'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.7'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.7'
70
+ - !ruby/object:Gem::Dependency
71
+ name: simplecov
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.16'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.16'
84
+ - !ruby/object:Gem::Dependency
85
+ name: image_size
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '2.0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '2.0'
98
+ description: A library which implements the client VNC protocol to control VNC servers.
99
+ email: topolnyak012@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files:
103
+ - README.rdoc
104
+ - Changelog.rdoc
105
+ files:
106
+ - COPYING
107
+ - Changelog.rdoc
108
+ - README.rdoc
109
+ - Rakefile
110
+ - data/keys.yaml
111
+ - lib/cipher/vncdes.rb
112
+ - lib/net/rfb/frame_buffer.rb
113
+ - lib/net/vnc.rb
114
+ - lib/net/vnc/version.rb
115
+ - spec/cipher_vncdes_spec.rb
116
+ - spec/net_vnc_spec.rb
117
+ - spec/real_net_vnc_spec.rb
118
+ homepage: https://github.com/evilcap/ruby-vnc
119
+ licenses:
120
+ - MIT
121
+ metadata: {}
122
+ post_install_message:
123
+ rdoc_options:
124
+ - "--main"
125
+ - README.rdoc
126
+ - "--title"
127
+ - evilcap-ruby-vnc documentation
128
+ - "--tab-width"
129
+ - '2'
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project: evilcap-ruby-vnc
144
+ rubygems_version: 2.7.6
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Evilcap Ruby VNC library.
148
+ test_files: []