shells 0.1.5

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
+ 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