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 +7 -0
- data/.gitignore +11 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +78 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/shells/bash_common.rb +170 -0
- data/lib/shells/errors.rb +57 -0
- data/lib/shells/pf_sense_common.rb +400 -0
- data/lib/shells/pf_sense_serial_session.rb +55 -0
- data/lib/shells/pf_sense_ssh_session.rb +56 -0
- data/lib/shells/serial_session.rb +184 -0
- data/lib/shells/shell_base.rb +846 -0
- data/lib/shells/ssh_session.rb +232 -0
- data/lib/shells/version.rb +5 -0
- data/lib/shells.rb +37 -0
- data/shells.gemspec +32 -0
- metadata +160 -0
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
data/Gemfile
ADDED
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
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,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
|