barkest_ssh 1.1.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 11900aa2618a5ff0ca84feb5c426e729d9b802f5
4
+ data.tar.gz: 4bc6cf36fde39a99e637bfc6a1089035c9836e4c
5
+ SHA512:
6
+ metadata.gz: fb5b9bede7905f66967d275e1bb9feb8aaceaefd48759a8c2f32ffab550956dc54357990cb283e1eb42dd617aa0c7d1f0246f5cb600d68171643c9d1014f55a2
7
+ data.tar.gz: 21daaa9703b71cbad298aeadb38d61903014daa75a2a4e9b375f1f1e6c0f2cf3d14922e351766ebe27e20970b02d28ac93f1c61f10fa179e72a3a5bbd6714e1e
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea
11
+ **/.DS_Store
12
+ **/.byebug*
13
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in barkest_ssh.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Beau Barker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,130 @@
1
+ # BarkestSsh
2
+
3
+ The BarkestSsh gem is a very simple wrapper around Net::SSH. Using the BarkestSsh::SecureShell class you can execute a
4
+ shell session on a local or remote host. Primarily targeted at `bash` shells, this gem may have trouble interacting
5
+ with some hardware due to some assumptions it makes.
6
+
7
+ For instance, it expects to be able to set the `PS1` variable to use a custom prompt so it knows when command execution
8
+ has officially completed. A possible workaround if the device has a static shell is to set the prompt option to match
9
+ the static shell. If the device has a dynamic shell, but a static final sequence (ie - '>', '$', or '#') then setting
10
+ the prompt option to that value may also work. However, there may be an issue with false positives in this situation.
11
+
12
+ This gem was primarily developed to be added into [Barker EST](http://www.barkerest.com/) web applications (hence the name).
13
+
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'barkest_ssh'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install barkest_ssh
30
+
31
+ ## Usage
32
+
33
+ All of the work is done in the BarkestSsh::SecureShell constructor. Provide a code block to BarkestSsh::SecureShell.new
34
+ with the actions you want to execute remotely.
35
+
36
+ ```ruby
37
+ BarkestSsh::SecureShell.new(
38
+ host: '10.10.10.10',
39
+ user: 'somebody',
40
+ password: 'super-secret'
41
+ ) do |shell|
42
+ shell.exec('cd /usr/local/bin')
43
+ user_bin_files = shell.exec('ls -A1').split('\n')
44
+ @app_is_installed = user_bin_files.include?('my_app')
45
+ end
46
+ ```
47
+
48
+
49
+ The following methods are available within the code block:
50
+
51
+ * __`exec(command, options = {}, &block)`__
52
+
53
+ This is the core method that will probably be used most often. The command is executed and the text sent
54
+ to STDOUT and STDERR is returned as a single string for you to process. The options parameter can set the
55
+ `:on_non_zero_exit_code` option to :default, :ignore, or :raise_error. If a block is provided, the block
56
+ will be called anytime data is received. The block will receive data and type parameters. Type will indicate
57
+ if the data was sent to STDOUT or STDERR. If the block returns a value, that value will be sent to the shell.
58
+ This can be used to interact with the shell.
59
+
60
+ * __`exec_ignore(command, &block)`__
61
+
62
+ Wrapper for `exec` that sets :on_non_zero_exit_code to :ignore.
63
+
64
+ * __`exec_raise(command, &block)`__
65
+
66
+ Wrapper for `exec` that sets :on_non_zero_exit_code to :raise_error.
67
+
68
+ * __`sudo_exec(command, options = {}, &block)`__
69
+
70
+ Wrapper for `exec` that attempts to elevate the command to run as root. This will only work if the user
71
+ that the shell is connected with is a sudoer on the target host. Also, it requires that the target host
72
+ uses the `sudo` command and `bash` shell.
73
+
74
+ * __`sudo_exec_ignore(command, &block)`__
75
+
76
+ Wrapper for `sudo_exec` that sets :on_non_zero_exit_code to :ignore.
77
+
78
+ * __`sudo_exec_raise(command, &block)`__
79
+
80
+ Wrapper for `sudo_exec` that sets :on_non_zero_exit_code to :raise_error.
81
+
82
+ * __`last_exit_code`__
83
+
84
+ Gets the exit code from the last command, if the shell is configured to retrieve the exit codes.
85
+
86
+ * __`stdout`__
87
+
88
+ Gets the output to STDOUT for the shell session.
89
+
90
+ * __`stderr`__
91
+
92
+ Gets the output to STDERR for the shell session.
93
+
94
+ * __`combined_output`__
95
+
96
+ Gets the output to both STDOUT and STDERR combined together for the shell session.
97
+
98
+ * __`upload(local_file, remote_file)`__
99
+
100
+ Uses a SFTP channel to upload a file to the host. The first time a SFTP method is used a second
101
+ SSH connection is made to the host and a SFTP channel is created.
102
+
103
+ * __`download(remote_file, local_file)`__
104
+
105
+ Uses a SFTP channel to download a file from the host. The first time a SFTP method is used a second
106
+ SSH connection is made to the host and a SFTP channel is created.
107
+
108
+ * __`read_file(remote_file)`__
109
+
110
+ Uses a SFTP channel to download a file from the host and returns the contents as a string.
111
+ The first time a SFTP method is used a second SSH connection is made to the host and a SFTP channel
112
+ is created.
113
+
114
+ * __`write_file(remote_file, data)`__
115
+
116
+ Uses a SFTP channel to upload a file to the host from an in-memory string.
117
+ The first time a SFTP method is used a second SSH connection is made to the host and a SFTP channel
118
+ is created.
119
+
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/barkerest/barkest_ssh.
124
+
125
+
126
+ ## License
127
+
128
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
129
+
130
+ Copyright (c) 2016 [Beau Barker](mailto:beau@barkerest.com)
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'barkest_ssh/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "barkest_ssh"
8
+ spec.version = BarkestSsh::VERSION
9
+ spec.authors = ["Beau Barker"]
10
+ spec.email = ["beau@barkerest.com"]
11
+
12
+ spec.summary = "Provides a very simple interface to Net::SSH."
13
+ spec.homepage = "http://www.barkerest.com/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'net-ssh', '~> 3.0.2'
22
+ spec.add_dependency 'net-sftp', '~> 2.1.2'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.12'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "barkest_ssh"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ require 'barkest_ssh/version'
2
+ require 'barkest_ssh/secure_shell'
3
+
4
+ module BarkestSsh
5
+ # Your code goes here...
6
+ end
7
+
@@ -0,0 +1,592 @@
1
+
2
+ require 'net/ssh'
3
+ require 'net/sftp'
4
+
5
+ module BarkestSsh
6
+
7
+ ##
8
+ # The SecureShell class is used to run an SSH session with a local or remote host.
9
+ #
10
+ # This is a wrapper for Net::SSH that starts a shell session and executes a block.
11
+ # All of the output from the session is cached for later use as needed.
12
+ #
13
+ class SecureShell
14
+
15
+ ##
16
+ # An error occurring within the SecureShell class aside from argument errors.
17
+ ShellError = Class.new(StandardError)
18
+
19
+ ##
20
+ # An exception raised when a command requiring a connection is attempted after the connection has been closed.
21
+ ConnectionClosed = Class.new(ShellError)
22
+
23
+ ##
24
+ # An exception raised when the SSH session fails to request a PTY.
25
+ FailedToRequestPTY = Class.new(ShellError)
26
+
27
+ ##
28
+ # An exception raised when the SSH session fails to start a shell.
29
+ FailedToStartShell = Class.new(ShellError)
30
+
31
+ ##
32
+ # An exception raised when the SSH shell session fails to execute.
33
+ #
34
+ FailedToExecute = Class.new(ShellError)
35
+
36
+ ##
37
+ # An exception raised when the shell session is silent too long.
38
+ LongSilence = Class.new(ShellError)
39
+
40
+ ##
41
+ # A command exited with a non-zero status.
42
+ NonZeroExitCode = Class.new(ShellError)
43
+
44
+ ##
45
+ # Creates a SecureShell session and executes the provided block.
46
+ #
47
+ # You must provide a code block to run within the shell session, the session is closed before this returns.
48
+ #
49
+ # Valid options:
50
+ # * +host+
51
+ # The name or IP address of the host to connect to. Defaults to 'localhost'.
52
+ # * +port+
53
+ # The port on the host to connect to. Defaults to 22.
54
+ # * +user+
55
+ # The user to login with.
56
+ # * +password+
57
+ # The password to login with.
58
+ # * +prompt+
59
+ # The prompt used to determine when processes finish execution.
60
+ # Defaults to '~~#', but if that doesn't work for some reason because it is valid output from one or more
61
+ # commands, you can change it to something else. It must be unique and cannot contain certain characters.
62
+ # The characters you should avoid are !, $, \, /, ", and ' because no attempt is made to escape them and the
63
+ # resulting prompt can very easily become something else entirely. If they are provided, they will be
64
+ # replaced to protect the shell from getting stuck.
65
+ # * +silence_wait+
66
+ # The number of seconds to wait when the shell is not sending back data to send a newline. This can help
67
+ # battle background tasks burying the prompt, but it might not play nice with long-running foreground tasks.
68
+ # The default is 5 seconds, if you notice problems, set this to a higher value, or 0 to disable.
69
+ # During extended silence, the first time this value elapses, the shell will send the newline, the second time
70
+ # the shell will error out.
71
+ # * +replace_cr+
72
+ # The string to replace stand-alone CR characters with. The default is an empty string (ie - remove them).
73
+ # You may also want to replace with a LF character instead, which is the behavior taken when a CR+ LF sequence
74
+ # is encountered. A space followed by a standalone CR is treated differently since these seem to occur when
75
+ # the terminal ouput wraps. In these cases, the SPACE + CR sequence is simply removed.
76
+ # * +retrieve_exit_code+
77
+ # Version 1.1.10 introduces support for grabbing the exit code from the last command and then performing an
78
+ # action. The default value is true, but if you set this to false then the shell will not retrieve the exit
79
+ # codes automatically.
80
+ # * +on_non_zero_exit_code+
81
+ # If the exit code is non-zero, the default behavior (to remain compatible with prior versions) is to
82
+ # ignore the exit code. You can also set this to :raise_error to raise the NonZeroExitCode error.
83
+ # * +filter_password+
84
+ # As a convenience, if this is set to true (the default), then any text matching the configured password
85
+ # will be replaced with a series of asterisks in the output.
86
+ #
87
+ # SecureShell.new(
88
+ # host: '10.10.10.10',
89
+ # user: 'somebody',
90
+ # password: 'super-secret'
91
+ # ) do |shell|
92
+ # shell.exec('cd /usr/local/bin')
93
+ # user_bin_files = shell.exec('ls -A1').split('\n')
94
+ # @app_is_installed = user_bin_files.include?('my_app')
95
+ # end
96
+ #
97
+ def initialize(options = {}, &block)
98
+ options ||= {}
99
+ @options = {
100
+ host: options[:host] || 'localhost',
101
+ port: options[:port] || 22,
102
+ user: options[:user],
103
+ password: options[:password],
104
+ prompt: (options[:prompt].to_s.strip == '') ? '~~#' : options[:prompt],
105
+ silence_wait: (options[:silence_wait] || 5),
106
+ replace_cr: options[:replace_cr].to_s,
107
+ retrieve_exit_code: options[:retrieve_exit_code].nil? ? true : options[:retrieve_exit_code],
108
+ on_non_zero_exit_code: options[:on_non_zero_exit_code] ? options[:on_non_zero_exit_code].to_s.to_sym : :ignore,
109
+ filter_password: options[:filter_password].nil? ? true : options[:filter_password],
110
+ }
111
+
112
+ raise ArgumentError.new('Missing block.') unless block_given?
113
+ raise ArgumentError.new('Missing host.') if @options[:host].to_s.strip == ''
114
+ raise ArgumentError.new('Missing user.') if @options[:user].to_s.strip == ''
115
+ raise ArgumentError.new('Missing password.') if @options[:password].to_s.strip == ''
116
+ raise ArgumentError.new('Missing prompt.') if @options[:prompt].to_s.strip == ''
117
+ raise ArgumentError.new('Invalid option for on_non_zero_exit_code.') unless [:ignore, :raise_error].include?(@options[:on_non_zero_exit_code])
118
+
119
+ @options[:prompt] = @options[:prompt]
120
+ .gsub('!', '#')
121
+ .gsub('$', '#')
122
+ .gsub('\\', '.')
123
+ .gsub('/', '.')
124
+ .gsub('"', '-')
125
+ .gsub('\'', '-')
126
+
127
+ executed = false
128
+
129
+ @last_exit_code = 0
130
+ @sftp = nil
131
+ Net::SSH.start(
132
+ @options[:host],
133
+ @options[:user],
134
+ password: @options[:password],
135
+ port: @options[:port],
136
+ non_interactive: true,
137
+ ) do |ssh|
138
+ @ssh = ssh
139
+ ssh.open_channel do |ssh_channel|
140
+ ssh_channel.request_pty do |pty_channel, pty_success|
141
+ raise FailedToRequestPTY.new('Failed to request PTY.') unless pty_success
142
+
143
+ pty_channel.send_channel_request('shell') do |_, shell_success|
144
+ raise FailedToStartShell.new('Failed to start shell.') unless shell_success
145
+
146
+ # cache the channel pointer and start buffering the input.
147
+ @channel = pty_channel
148
+ buffer_input
149
+
150
+ # give the shell a chance to catch up and initialize fully.
151
+ sleep 0.25
152
+
153
+ # set the shell prompt so that we can determine when processes end.
154
+ # does not work with background processes since we are looking for
155
+ # the shell to send us this when it is ready for more input.
156
+ # a background process can easily bury the prompt and then we are stuck in a loop.
157
+ exec "PS1=\"#{@options[:prompt]}\""
158
+
159
+ block.call(self)
160
+
161
+ executed = true
162
+
163
+ # send the exit command and remove the channel pointer.
164
+ quit
165
+ @channel = nil
166
+ end
167
+ end
168
+ ssh_channel.wait
169
+ end
170
+ end
171
+
172
+ @ssh = nil
173
+
174
+ if @sftp
175
+ @sftp.session.close
176
+ @sftp = nil
177
+ end
178
+
179
+ # remove the cached user and password.
180
+ options.delete(:user)
181
+ options.delete(:password)
182
+
183
+ raise FailedToExecute.new('Failed to execute shell.') unless executed
184
+ end
185
+
186
+ ##
187
+ # Gets the last exit code.
188
+ def last_exit_code
189
+ @last_exit_code || 0
190
+ end
191
+
192
+ ##
193
+ # Wrapper for +exec+ that will ignore non-zero exit codes.
194
+ def exec_ignore(command, &block)
195
+ exec command, on_non_zero_exit_code: :ignore, &block
196
+ end
197
+
198
+ ##
199
+ # Wrapper for +exec+ that will raise an error on non-zero exit codes.
200
+ def exec_raise(command, &block)
201
+ exec command, on_non_zero_exit_code: :raise_error, &block
202
+ end
203
+
204
+ ##
205
+ # Executes a command during the shell session.
206
+ #
207
+ # If called outside of the +new+ block, this will raise an error.
208
+ #
209
+ # The +command+ is the command to execute in the shell.
210
+ #
211
+ # The +options+ parameter can include the following keys.
212
+ # * The :on_non_zero_exit_code option can be :default, :ignore, or :raise_error.
213
+ #
214
+ # If provided, the +block+ is a chunk of code that will be processed every time the
215
+ # shell receives output from the program. If the block returns a string, the string
216
+ # will be sent to the shell. This can be used to monitor processes or monitor and
217
+ # interact with processes. The +block+ is optional.
218
+ #
219
+ # shell.exec('sudo -p "password:" nginx restart') do |data,type|
220
+ # return 'super-secret' if /password:$/.match(data)
221
+ # nil
222
+ # end
223
+ #
224
+ def exec(command, options={}, &block)
225
+ raise ConnectionClosed.new('Connection is closed.') unless @channel
226
+
227
+ options = {
228
+ on_non_zero_exit_code: :default
229
+ }.merge(options || {})
230
+
231
+ options[:on_non_zero_exit_code] = @options[:on_non_zero_exit_code] if options[:on_non_zero_exit_code] == :default
232
+
233
+ push_buffer # store the current buffer and start a fresh buffer
234
+
235
+ # buffer while also passing data to the supplied block.
236
+ if block_given?
237
+ buffer_input( &block )
238
+ end
239
+
240
+ # send the command and wait for the prompt to return.
241
+ @channel.send_data command + "\n"
242
+ wait_for_prompt
243
+
244
+ # return buffering to normal.
245
+ if block_given?
246
+ buffer_input
247
+ end
248
+
249
+ # get the output from the command, minus the trailing prompt.
250
+ ret = command_output(command)
251
+
252
+ # restore the original buffer and merge the output from the command.
253
+ pop_merge_buffer
254
+
255
+ if @options[:retrieve_exit_code]
256
+ # get the exit code for the command.
257
+ push_buffer
258
+ retrieve_command = 'echo $?'
259
+ @channel.send_data retrieve_command + "\n"
260
+ wait_for_prompt
261
+ @last_exit_code = command_output(retrieve_command).strip.to_i
262
+ # restore the original buffer and discard the output from this command.
263
+ pop_discard_buffer
264
+
265
+ # if we are expected to raise an error, do so.
266
+ if options[:on_non_zero_exit_code] == :raise_error
267
+ raise NonZeroExitCode.new("Exit code was #{@last_exit_code}.") unless @last_exit_code == 0
268
+ end
269
+ end
270
+
271
+ ret
272
+ end
273
+
274
+ ##
275
+ # Wrapper for +sudo_exec+ that will ignore non-zero exit codes.
276
+ def sudo_exec_ignore(command, &block)
277
+ sudo_exec command, on_non_zero_exit_code: :ignore, &block
278
+ end
279
+
280
+ ##
281
+ # Wrapper for +sudo_exec+ that will raise an error on non-zero exit codes.
282
+ def sudo_exec_raise(command, &block)
283
+ sudo_exec command, on_non_zero_exit_code: :raise_error, &block
284
+ end
285
+
286
+ ##
287
+ # Executes a command using +sudo+ during the shell session.
288
+ #
289
+ # This is a wrapper around +exec+ that attempts to run the command as root.
290
+ # It provides the configured user's password if/when prompted.
291
+ #
292
+ # See +exec+ for more information.
293
+ def sudo_exec(command, options = {}, &block)
294
+ sudo_prompt = '[sp:'
295
+ sudo_match = /(\r|\n)\[sp\:$/
296
+ sudo_strip = /\[sp\:\n/
297
+ ret = exec("sudo -p \"#{sudo_prompt}\" bash -c \"#{command.gsub('"', '\\"')}\"", options) do |data,type|
298
+ test_data = data.to_s
299
+ desired_length = sudo_prompt.length + 1 # prefix a NL before the prompt.
300
+
301
+ # pull from the current stdout to get the full test data, but only if we received some new data.
302
+ if test_data.length > 0 && test_data.length < desired_length
303
+ test_data = stdout[-desired_length..-1].to_s
304
+ end
305
+
306
+ if sudo_match.match(test_data)
307
+ @options[:password]
308
+ else
309
+ if block
310
+ block.call(data, type)
311
+ else
312
+ nil
313
+ end
314
+ end
315
+ end
316
+ # remove the sudo prompts.
317
+ ret.gsub(sudo_strip, '')
318
+ end
319
+
320
+ ##
321
+ # Uses SFTP to upload a single file to the host.
322
+ def upload(local_file, remote_file)
323
+ raise ConnectionClosed.new('Connection is closed.') unless @ssh
324
+ sftp.upload!(local_file, remote_file)
325
+ end
326
+
327
+ ##
328
+ # Uses SFTP to download a single file from the host.
329
+ def download(remote_file, local_file)
330
+ raise ConnectionClosed.new('Connection is closed.') unless @ssh
331
+ sftp.download!(remote_file, local_file)
332
+ end
333
+
334
+ ##
335
+ # Uses SFTP to read the contents of a single file.
336
+ #
337
+ # Returns the contents of the file.
338
+ def read_file(remote_file)
339
+ raise ConnectionClosed.new('Connection is closed.') unless @ssh
340
+ sftp.download!(remote_file)
341
+ end
342
+
343
+ ##
344
+ # Uses SFTP to write data to a single file.
345
+ def write_file(remote_file, data)
346
+ raise ConnectionClosed.new('Connection is closed.') unless @ssh
347
+ sftp.file.open(remote_file, 'w') do |f|
348
+ f.write data
349
+ end
350
+ end
351
+
352
+ ##
353
+ # Gets the standard output from the session.
354
+ #
355
+ # The prompts are stripped from the standard ouput as they are encountered.
356
+ # So this will be a list of commands with their output.
357
+ #
358
+ # All line endings are converted to LF characters, so you will not
359
+ # encounter or need to search for CRLF or CR sequences.
360
+ #
361
+ def stdout
362
+ @stdout || ''
363
+ end
364
+
365
+ ##
366
+ # Gets the error output from the session.
367
+ #
368
+ # All line endings are converted to LF characters, so you will not
369
+ # encounter or need to search for CRLF or CR sequences.
370
+ #
371
+ def stderr
372
+ @stderr || ''
373
+ end
374
+
375
+ ##
376
+ # Gets both the standard output and error output from the session.
377
+ #
378
+ # The prompts will be included in the combined output.
379
+ # There is no attempt to differentiate error output from standard output.
380
+ #
381
+ # This is essentially the definitive log for the session.
382
+ #
383
+ # All line endings are converted to LF characters, so you will not
384
+ # encounter or need to search for CRLF or CR sequences.
385
+ #
386
+ def combined_output
387
+ @stdcomb || ''
388
+ end
389
+
390
+ private
391
+
392
+ def quit
393
+ raise ConnectionClosed.new('Connection is closed.') unless @channel
394
+ @channel.send_data("exit\n")
395
+ @channel.wait
396
+ end
397
+
398
+ def command_output(command)
399
+ # get everyting except for the ending prompt.
400
+ ret = combined_output[0...-(@options[:prompt].length)]
401
+ # return the output from the command starting with the second line.
402
+ # the first line is the command sent to the shell.
403
+ # We also check for those rare times when a prompt manages to sneak in, trimming them off the front as well.
404
+ result_cmd,_,result_data = ret.partition("\n")
405
+ cmd_with_prompt = @options[:prompt] + command
406
+ until result_cmd == command || result_cmd == cmd_with_prompt || result_data.to_s.strip == ''
407
+ result_cmd,_,result_data = result_data.partition("\n")
408
+ end
409
+ result_data
410
+ end
411
+
412
+ def stdout_hist
413
+ @stdout_hist ||= []
414
+ end
415
+
416
+ def stderr_hist
417
+ @stderr_hist ||= []
418
+ end
419
+
420
+ def stdcomb_hist
421
+ @stdcom_hist ||= []
422
+ end
423
+
424
+ def prompted?
425
+ @prompted ||= false
426
+ end
427
+
428
+ def reset_prompted
429
+ @prompted = false
430
+ end
431
+
432
+ def set_prompted
433
+ @prompted = true
434
+ end
435
+
436
+ def push_buffer
437
+ # push the buffer so we can get the output of a command.
438
+ stdout_hist.push stdout
439
+ stderr_hist.push stderr
440
+ stdcomb_hist.push combined_output
441
+ @stdout = ''
442
+ @stderr = ''
443
+ @stdcomb = ''
444
+ end
445
+
446
+ def pop_merge_buffer
447
+ # almost a standard pop, however we want to merge history with current.
448
+ if (hist = stdout_hist.pop)
449
+ @stdout = hist + stdout
450
+ end
451
+ if (hist = stderr_hist.pop)
452
+ @stderr = hist + stderr
453
+ end
454
+ if (hist = stdcomb_hist.pop)
455
+ @stdcomb = hist + combined_output
456
+ end
457
+ end
458
+
459
+ def pop_discard_buffer
460
+ # a standard pop discarding current data and retrieving the history.
461
+ if (hist = stdout_hist.pop)
462
+ @stdout = hist
463
+ end
464
+ if (hist = stderr_hist.pop)
465
+ @stderr = hist
466
+ end
467
+ if (hist = stdcomb_hist.pop)
468
+ @stdcomb = hist
469
+ end
470
+ end
471
+
472
+ def append_stdout(data, &block)
473
+ # Combined output gets the prompts,
474
+ # but stdout will be without prompts.
475
+ # CRLF are converted to LF and CR are removed.
476
+ # The " \r" sequence appears to be a line continuation sequence for the shell, so it get's removed.
477
+ # All remaining CR are replaced with LF.
478
+ data = data.gsub("\r\n", "\n").gsub(" \r", '').gsub("\r", @options[:replace_cr])
479
+
480
+ for_stdout = if data[-(@options[:prompt].length)..-1] == @options[:prompt]
481
+ set_prompted
482
+ data[0...-(@options[:prompt].length)]
483
+ else
484
+ data
485
+ end
486
+
487
+ @stdout = @stdout.to_s + for_stdout
488
+ @stdcomb = @stdcomb.to_s + data
489
+
490
+ if block_given?
491
+ result = block.call(for_stdout, :stdout)
492
+ if result && result.is_a?(String)
493
+ @channel.send_data(result + "\n") if @channel
494
+ end
495
+ end
496
+ end
497
+
498
+ def append_stderr(data, &block)
499
+ data = data.gsub("\r\n", "\n").gsub(" \r", '').gsub("\r", @options[:replace_cr])
500
+
501
+ @stderr = @stderr.to_s + data
502
+ @stdcomb = @stdcomb.to_s + data
503
+
504
+ if block_given?
505
+ result = block.call(data, :stderr)
506
+ if result && result.is_a?(String)
507
+ @channel.send_data(result + "\n") if @channel
508
+ end
509
+ end
510
+ end
511
+
512
+ def buffer_input(&block)
513
+ raise ConnectionClosed.new('Connection is closed.') unless @channel
514
+ block ||= Proc.new { }
515
+
516
+ @last_input = Time.now
517
+
518
+ @channel.on_data do |_, data|
519
+ append_stdout strip_ansi_escape(sterilize(data)), &block
520
+ end
521
+
522
+ @channel.on_extended_data do |_, type, data|
523
+ if type == 1
524
+ append_stderr strip_ansi_escape(sterilize(data)), &block
525
+ end
526
+ end
527
+
528
+ end
529
+
530
+ def wait_for_prompt
531
+ raise ConnectionClosed.new('Connection is closed.') unless @channel
532
+
533
+ wait_timeout = @options[:silence_wait].to_s.to_i
534
+ @last_input ||= Time.now
535
+ sent_nl_at = nil
536
+ sent_nl_times = 0
537
+
538
+ @channel.connection.loop do
539
+ # cache the last input, this way if something is received it doesn't screw with us.
540
+ last_input = @last_input
541
+
542
+ # do we need to nudge the shell?
543
+ if wait_timeout > 0 && (Time.now - last_input) > wait_timeout
544
+
545
+ # have we nudged the shell more than twice?
546
+ if sent_nl_times > 2
547
+ raise LongSilence.new('No input from shell for extended period.')
548
+ else
549
+
550
+ # reset the timer and increment the counter if the timer hasn't budged since we last nudged it.
551
+ sent_nl_times = (sent_nl_at.nil? || sent_nl_at < last_input) ? 1 : (sent_nl_times + 1)
552
+ sent_nl_at = Time.now
553
+
554
+ # and send the NL to nudge along the shell.
555
+ @channel.send_data "\n"
556
+ @last_input = sent_nl_at
557
+ end
558
+ end
559
+
560
+ !prompted?
561
+ end
562
+
563
+ reset_prompted
564
+ end
565
+
566
+ def sterilize(data)
567
+ if @options[:filter_password]
568
+ spwd = '*' * @options[:password].length
569
+ data.gsub(@options[:password], spwd)
570
+ else
571
+ data
572
+ end
573
+ end
574
+
575
+ def strip_ansi_escape(data)
576
+ data
577
+ .gsub(/\e\[(\d+;?)*[ABCDEFGHfu]/, "\n") # any of the "set cursor position" CSI commands.
578
+ .gsub(/\e\[=?(\d+;?)*[A-Za-z]/,'') # \e[#;#;#A or \e[=#;#;#A basically all the CSI commands except ...
579
+ .gsub(/\e\[(\d+;"[^"]+";?)+p/, '') # \e[#;"A"p
580
+ .gsub(/\e[NOc]./,'?') # any of the alternate character set commands.
581
+ .gsub(/\e[P_\]^X][^\e\a]*(\a|(\e\\))/,'') # any string command
582
+ .gsub(/[\x00\x08\x0B\x0C\x0E-\x1F]/, '') # any non-printable characters (notice \x0A (LF) and \x0D (CR) are left as is).
583
+ .gsub("\t", ' ') # turn tabs into spaces.
584
+ end
585
+
586
+ def sftp
587
+ raise ConnectionClosed.new('Connection is closed.') unless @ssh
588
+ @sftp ||= ::Net::SFTP.start(@options[:host], @options[:user], password: @options[:password])
589
+ end
590
+
591
+ end
592
+ end
@@ -0,0 +1,3 @@
1
+ module BarkestSsh
2
+ VERSION = '1.1.13'
3
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: barkest_ssh
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.13
5
+ platform: ruby
6
+ authors:
7
+ - Beau Barker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-sftp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ description:
70
+ email:
71
+ - beau@barkerest.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - barkest_ssh.gemspec
82
+ - bin/console
83
+ - bin/setup
84
+ - lib/barkest_ssh.rb
85
+ - lib/barkest_ssh/secure_shell.rb
86
+ - lib/barkest_ssh/version.rb
87
+ homepage: http://www.barkerest.com/
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 2.4.5.1
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Provides a very simple interface to Net::SSH.
111
+ test_files: []