shells 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|