shells 0.1.5

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
+ SHA1:
3
+ metadata.gz: 13762830a779f79e8479b3d5c41fe66962ed63b9
4
+ data.tar.gz: 24241daef4637de27a2905a03daee0aba6e85bfe
5
+ SHA512:
6
+ metadata.gz: b2e6a7496728f5cd40741c5eeb8a83675a48a2a364b703e420e67d929ff9b1039d6eb2080e3d993141fd9be6ef8796fa2c66e7fce965e19965eb592a0fba219e
7
+ data.tar.gz: c6377f55371051f7df873a78a0befe1e7c7a79c6a32ad3e3eac2493a95b423a7b48b84e2fc3694a14bbd31833e54f2bd983bcbfdc309fc74d459995e5050eced
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ test/config.yml
11
+
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shells.gemspec
4
+ gemspec
5
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 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.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Shells
2
+
3
+ This gem is a collection of shell classes that can be used to interact with various devices.
4
+ It started as a secure shell to interact with SSH hosts, then it received a shell to access pfSense devices.
5
+ A natural progression had me adding a serial shell and another shell to access pfSense devices over serial.
6
+
7
+ If you can't tell, it was primarily developed to interact with pfSense devices, but also tends to work well
8
+ for interacting with other devices and hosts as well.
9
+
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'shells'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install shells
26
+
27
+
28
+ ## Usage
29
+
30
+ Any of the various "Session" classes can be used to interact with a device or host.
31
+
32
+ ```ruby
33
+ Shells::SshSession.new(host: 'my.device.name.or.ip', user: 'somebody', password: 'secret') do |sh|
34
+ sh.exec "cd /usr/local/bin"
35
+ user_bin_files = sh.exec("ls -A1").split("\n")
36
+ @app_is_installed = user_bin_files.include?("my_app")
37
+ end
38
+ ```
39
+
40
+ Every session constructor works the same way. The shell is connected to, the prompt is set, and then the block
41
+ of code passed to the constructor is executed on the shell. After the code block completes, the session is finalized
42
+ and the shell is closed.
43
+
44
+ The `Shells` module is designed to allow you to forego the `.new` as well. So `Shells::SshSession(...)` is the same as
45
+ `Shells::SshSession.new(...)`.
46
+
47
+ In most cases you will be sending commands to the shell using the `.exec` method of the shell passed to the code block.
48
+ The `.exec` method returns the output of the command and then you can process the results. You can also request that
49
+ the `.exec` method retrieves the exit code from the command as well, but this will only work in some shells.
50
+
51
+ ```ruby
52
+ Shells::SshSession(host: 'my.device.name.or.ip', user: 'somebody', password: 'secret') do |sh|
53
+ # By default shells do not retrieve exit codes or raise on non-zero exit codes.
54
+ # These parameters can be set in the options list for the constructor as well to change the
55
+ # default behavior.
56
+
57
+ # This command will execute the command and then retrieve the exit code.
58
+ # We then perform an action based on the exit code.
59
+ sh.exec "some command", retrieve_exit_code: true
60
+ raise 'Some Error' if sh.last_exit_code != 0
61
+
62
+ # This command will execute the command then automatically raise an error if the exit code
63
+ # is non-zero. The error raised is a Shells::NonZeroExitCode exception which happens to have
64
+ # an exit_code property for your rescue code to examine.
65
+ sh.exec "some command", retrieve_exit_code: true, on_non_zero_exit_code: :raise
66
+ end
67
+ ```
68
+
69
+
70
+ ## Contributing
71
+
72
+ Bug reports and pull requests are welcome on GitHub at https://github.com/barkerest/shells.
73
+
74
+
75
+ ## License
76
+
77
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
78
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "shells"
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(__FILE__)
data/bin/setup ADDED
@@ -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,170 @@
1
+ require 'base64'
2
+
3
+ module Shells
4
+ ##
5
+ # Provides some common functionality for bash-like shells.
6
+ module BashCommon
7
+
8
+ ##
9
+ # Reads from a file on the device.
10
+ def read_file(path, use_method = nil)
11
+ if use_method
12
+ use_method = use_method.to_sym
13
+ raise ArgumentError, "use_method (#{use_method.inspect}) is not a valid method." unless file_methods.include?(use_method)
14
+ raise Shells::ShellError, "The #{use_method} binary is not available with this shell." unless which(use_method)
15
+ send "read_file_#{use_method}", path
16
+ elsif default_file_method
17
+ return send "read_file_#{default_file_method}", path
18
+ else
19
+ raise Shells::ShellError, 'No supported binary to encode/decode files.'
20
+ end
21
+ end
22
+
23
+ ##
24
+ # Writes to a file on the device.
25
+ def write_file(path, data, use_method = nil)
26
+ if use_method
27
+ use_method = use_method.to_sym
28
+ raise ArgumentError, "use_method (#{use_method.inspect}) is not a valid method." unless file_methods.include?(use_method)
29
+ raise Shells::ShellError, "The #{use_method} binary is not available with this shell." unless which(use_method)
30
+ send "write_file_#{use_method}", path, data
31
+ elsif default_file_method
32
+ return send "write_file_#{default_file_method}", path, data
33
+ else
34
+ raise Shells::ShellError, 'No supported binary to encode/decode files.'
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ ##
41
+ # Gets an exit code by echoing the $? variable from the environment.
42
+ #
43
+ # This can be overridden by specifying either a string command or a Proc
44
+ # for the :override_get_exit_code option in the shell's options.
45
+ def get_exit_code #:nodoc:
46
+ cmd = options[:override_get_exit_code] || 'echo $?'
47
+ if cmd.respond_to?(:call)
48
+ cmd.call(self)
49
+ else
50
+ debug 'Retrieving exit code from last command...'
51
+ push_buffer
52
+ send_data cmd + line_ending
53
+ wait_for_prompt nil, 1
54
+ ret = command_output(cmd).strip.to_i
55
+ pop_discard_buffer
56
+ debug 'Exit code: ' + ret.to_s
57
+ ret
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Gets the path to a program, or nil if not found.
63
+ def which(program)
64
+ ret = exec("which #{program} 2>/dev/null").strip
65
+ ret == '' ? nil : ret
66
+ end
67
+
68
+ private
69
+
70
+ def file_methods
71
+ @file_methods ||= [
72
+ :base64,
73
+ :openssl
74
+ ]
75
+ end
76
+
77
+ def default_file_method
78
+ # Find the first method that should work.
79
+ unless instance_variable_defined?(:@default_file_method)
80
+ @default_file_method = file_methods.find { |meth| which(meth) }
81
+ end
82
+ @default_file_method
83
+ end
84
+
85
+ def with_b64_file(path, data, &block)
86
+ data = Base64.encode64(data)
87
+
88
+ max_cmd_length = 2048
89
+
90
+ # Send 1 line at a time (this will be SLOW for large files).
91
+ lines = data.gsub("\r\n", "\n").split("\n")
92
+
93
+ # Construct a temporary filename.
94
+ b64path = path + '.b64'
95
+ if exec_for_code("[ -f #{b64path.inspect} ]") == 0
96
+ # File exists.
97
+ cnt = 2
98
+ while exec_for_code("[ -f #{(b64path + cnt.to_s).inspect} ]") == 0
99
+ cnt += 1
100
+ end
101
+ b64path += cnt.to_s
102
+ end
103
+
104
+ debug "Writing #{lines.count} lines to #{b64path}..."
105
+
106
+ # Create/overwrite file with the first line.
107
+ first_line = lines.delete_at 0
108
+ exec "echo #{first_line} > #{b64path.inspect}"
109
+
110
+ # Create a queue.
111
+ cmds = []
112
+ lines.each do |line|
113
+ cmds << "echo #{line} >> #{b64path.inspect}"
114
+ end
115
+
116
+ # Process the queue sending as many at a time as possible.
117
+ while cmds.any?
118
+ cmd = cmds.delete(cmds.first)
119
+ while cmds.any? && cmd.length + cmds.first.length + 4 <= max_cmd_length
120
+ cmd += ' && ' + cmds.delete(cmds.first)
121
+ end
122
+ exec cmd
123
+ end
124
+
125
+ ret = block.call(b64path)
126
+
127
+ exec "rm #{b64path.inspect}"
128
+
129
+ ret
130
+ end
131
+
132
+ def write_file_base64(path, data)
133
+ with_b64_file path, data do |b64path|
134
+ exec_for_code "base64 -d #{b64path.inspect} > #{path.inspect}", command_timeout: 30
135
+ end
136
+ end
137
+
138
+ def write_file_openssl(path, data)
139
+ with_b64_file path, data do |b64path|
140
+ exec_for_code "openssl base64 -d < #{b64path.inspect} > #{path.inspect}"
141
+ end
142
+ end
143
+
144
+ def write_file_perl(path, data)
145
+ with_b64_file path, data do |b64path|
146
+ exec_for_code "perl -MMIME::Base64 -ne 'print decode_base64($_)' < #{b64path.inspect} > #{path.inspect}"
147
+ end
148
+ end
149
+
150
+ def read_file_base64(path)
151
+ data = exec "base64 -w 0 #{path.inspect}", retrieve_exit_code: true, on_non_zero_exit_code: :ignore, command_timeout: 30
152
+ return nil if last_exit_code != 0
153
+ Base64.decode64 data
154
+ end
155
+
156
+ def read_file_openssl(path)
157
+ data = exec "openssl base64 < #{path.inspect}", retrieve_exit_code: true, on_non_zero_exit_code: :ignore, command_timeout: 30
158
+ return nil if last_exit_code != 0
159
+ Base64.decode64 data
160
+ end
161
+
162
+ def read_file_perl(path)
163
+ data = exec "perl -MMIME::Base64 -ne 'print encode_base64($_)' < #{path.inspect}", retrieve_exit_code: true, on_non_zero_exit_code: :ignore, command_timeout: 30
164
+ return nil if last_exit_code != 0
165
+ Base64.decode64 data
166
+ end
167
+
168
+
169
+ end
170
+ end
@@ -0,0 +1,57 @@
1
+ module Shells
2
+ ##
3
+ # An error occurring within the SecureShell class aside from argument errors.
4
+ class ShellError < StandardError
5
+
6
+ end
7
+
8
+ ##
9
+ # An error raised when a provided option is invalid.
10
+ class InvalidOption < ShellError
11
+
12
+ end
13
+
14
+ ##
15
+ # An error raised when a command requiring a session is attempted after the session has been completed.
16
+ class SessionCompleted < ShellError
17
+
18
+ end
19
+
20
+ ##
21
+ # An error raised when a command exits with a non-zero status.
22
+ class NonZeroExitCode < ShellError
23
+ ##
24
+ # The exit code triggering the error.
25
+ attr_accessor :exit_code
26
+
27
+ ##
28
+ # Creates a new non-zero exit code error.
29
+ def initialize(exit_code)
30
+ self.exit_code = exit_code
31
+ end
32
+
33
+ def message # :nodoc:
34
+ "The exit code was #{exit_code}."
35
+ end
36
+ end
37
+
38
+ ##
39
+ # An error raised when a session is waiting for output for too long.
40
+ class SilenceTimeout < ShellError
41
+
42
+ end
43
+
44
+ ##
45
+ # An error raise when a session is waiting for a command to finish for too long.
46
+ class CommandTimeout < ShellError
47
+
48
+ end
49
+
50
+ ##
51
+ # An error raised when the session fails to set the prompt in the shell.
52
+ class FailedToSetPrompt < ShellError
53
+
54
+ end
55
+
56
+
57
+ end