evilcap-ruby-vnc 1.2.0

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.
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: []