abalone 0.0.1 → 0.1.0
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 +4 -4
- data/README.md +120 -0
- data/bin/abalone +43 -11
- data/bin/em.rb +50 -0
- data/lib/abalone.rb +88 -8
- data/public/index.html +2 -2
- data/public/js/abalone.js +22 -5
- data/views/index.erb +44 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1de9af9802ec0b416470cec7b62a3cd7b3dfa053
|
4
|
+
data.tar.gz: ea85d95749bb862e9b3f1edd9e69fe4cc73781f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7d724d615b6e25db425b2d8aaaae18521053dce1c4bfbaee14b1fa6f7b26670ea603d6920e4408e15168103abd364bdd4c1b09c9a072e581bf422350bef5ce3
|
7
|
+
data.tar.gz: d3b76310ff77bb549dea12f1e333d8e390d19ef9e98faef383e8ae7cac3ba3b5400278dad667a7363435828adddd586b3aad5e85f7c001a257375a143084dc7a
|
data/README.md
CHANGED
@@ -1,5 +1,125 @@
|
|
1
1
|
# abalone
|
2
2
|
A simple Sinatra & hterm based web terminal.
|
3
3
|
|
4
|
+
#### Table of Contents
|
5
|
+
|
6
|
+
1. [Overview](#overview)
|
7
|
+
1. [Configuration](#configuration)
|
8
|
+
1. [Limitations](#limitations)
|
9
|
+
|
10
|
+
## Overview
|
11
|
+
|
4
12
|
Simply exposes a login shell to a web browser. This is currently
|
5
13
|
nowhere near to production quality, so don't actually use it.
|
14
|
+
|
15
|
+
It supports three methods for providing a shell:
|
16
|
+
|
17
|
+
1. Simply running the `login` binary and logging in as a system user. (default)
|
18
|
+
1. Using SSH to connect to localhost or a remote machine. This can be configured
|
19
|
+
with credentials to automatically connect, or it can request a username and
|
20
|
+
password from the end user.
|
21
|
+
1. Running custom command, which can be configured with arbitrary parameters.
|
22
|
+
|
23
|
+
## Configuration
|
24
|
+
|
25
|
+
Abalone defaults to loading configuration from `/etc/abalone/config.yaml`. You
|
26
|
+
can pass the path to another config file at the command line. In that file, you
|
27
|
+
can set one of `:command` or `:ssh`.
|
28
|
+
|
29
|
+
### Configuring SSH
|
30
|
+
|
31
|
+
The following parameters may be used to configure SSH login. The `:host` setting
|
32
|
+
is required. `:user` and `:cert` are optional. If `:user` is not set, then the
|
33
|
+
user will be prompted for a login name, and if `:cert` is not set then
|
34
|
+
the user will be prompted to log in with a password. If the SSH server is running
|
35
|
+
on a non-standard port, you may specify that with the `:port` setting.
|
36
|
+
|
37
|
+
``` Yaml
|
38
|
+
---
|
39
|
+
:ssh:
|
40
|
+
:host: shellserver.example.com
|
41
|
+
:user: centos
|
42
|
+
:cert: /etc/abalone/centos.pem
|
43
|
+
```
|
44
|
+
|
45
|
+
### Configuring a custom command
|
46
|
+
|
47
|
+
A custom command can be configured in several ways. If you just want to run a
|
48
|
+
command without providing any options, the config file would look like:
|
49
|
+
|
50
|
+
``` Yaml
|
51
|
+
---
|
52
|
+
:command: /usr/local/bin/run-container
|
53
|
+
```
|
54
|
+
|
55
|
+
#### Simple options
|
56
|
+
|
57
|
+
You can also allow the user to pass in a arbitrary options. These must be
|
58
|
+
whitelisted. You can simply list allowed options in an Array:
|
59
|
+
|
60
|
+
``` Yaml
|
61
|
+
---
|
62
|
+
:command: /usr/local/bin/run-container
|
63
|
+
:params: [ 'username', 'image' ]
|
64
|
+
```
|
65
|
+
|
66
|
+
The options will be passed to the command in this way, ignoring the option
|
67
|
+
that was not whitelisted:
|
68
|
+
|
69
|
+
* http://localhost:9000/?username=bob&image=testing&invalid=value
|
70
|
+
* `/usr/local/bin/run-container --username bob --image testing`
|
71
|
+
|
72
|
+
#### Customized options
|
73
|
+
|
74
|
+
Finally, you can fully customize the options which may be passed in, including
|
75
|
+
remapping them to command line arguments and filtering accepted values. In this
|
76
|
+
case, `:params` must be a Hash.
|
77
|
+
|
78
|
+
``` Yaml
|
79
|
+
---
|
80
|
+
:command: /usr/local/bin/run-container
|
81
|
+
:params:
|
82
|
+
username:
|
83
|
+
type:
|
84
|
+
:values: ['demo', 'testing']
|
85
|
+
image:
|
86
|
+
:map: '--create-image'
|
87
|
+
:values: [ 'ubuntu', 'rhel', /centos[5,6,7]/ ]
|
88
|
+
```
|
89
|
+
|
90
|
+
Notice that `username` has nothing on the right side. It will be treated exactly
|
91
|
+
the same as `username` in the *Simple options* array above.
|
92
|
+
|
93
|
+
The `image` parameter is more complex though. It has two keys specified. Both are
|
94
|
+
optional. If `:map` is set, then its value will be used when running the command.
|
95
|
+
The `:values` key can be used to specify a list of valid values. Note that these
|
96
|
+
can be specified as Strings or regular expressions.
|
97
|
+
|
98
|
+
The options in this case will be passed to the command like:
|
99
|
+
|
100
|
+
* An option is mapped to a different command line argument:
|
101
|
+
* http://localhost:9000/?username=bob&image=centos6
|
102
|
+
* `/usr/local/bin/run-container --username bob --create-image centos6`
|
103
|
+
* An option without a `:map` is passed directly through to the command:
|
104
|
+
* http://localhost:9000/?username=bob&type=demo
|
105
|
+
* `/usr/local/bin/run-container --username bob --type demo`
|
106
|
+
* Image name that doesn't pass validation is ignored:
|
107
|
+
* http://localhost:9000/?username=bob&image=testing
|
108
|
+
* `/usr/local/bin/run-container --username bob`
|
109
|
+
* Invalid options and values are ignored:
|
110
|
+
* http://localhost:9000/?username=bob&image=testing&invalid=value
|
111
|
+
* `/usr/local/bin/run-container --username bob`
|
112
|
+
|
113
|
+
## Limitations
|
114
|
+
|
115
|
+
This is super early in development and has not yet been battle tested.
|
116
|
+
|
117
|
+
## Disclaimer
|
118
|
+
|
119
|
+
I take no liability for the use of this tool.
|
120
|
+
|
121
|
+
Contact
|
122
|
+
-------
|
123
|
+
|
124
|
+
binford2k@gmail.com
|
125
|
+
|
data/bin/abalone
CHANGED
@@ -3,36 +3,36 @@
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'optparse'
|
5
5
|
require 'abalone'
|
6
|
+
require 'yaml'
|
6
7
|
|
7
|
-
|
8
|
-
options = {
|
8
|
+
defaults = {
|
9
9
|
:port => 9000,
|
10
10
|
:host => '0.0.0.0',
|
11
11
|
:bind => '0.0.0.0',
|
12
12
|
}
|
13
|
-
logfile
|
14
|
-
loglevel
|
15
|
-
|
13
|
+
logfile = $stderr
|
14
|
+
loglevel = Logger::WARN
|
15
|
+
configfile = '/etc/abalone/config.yaml'
|
16
|
+
options = {}
|
16
17
|
optparse = OptionParser.new { |opts|
|
17
18
|
opts.banner = "Usage : abalone [-p <port>] [-l [logfile]] [-d]
|
18
19
|
-- Runs the Abalone web shell.
|
19
20
|
|
20
21
|
"
|
21
22
|
|
22
|
-
opts.on("-
|
23
|
-
|
23
|
+
opts.on("-c CONFIGFILE", "--config CONFIGFILE", "Load configuration from a file. (/etc/abalone/config.yaml)") do
|
24
|
+
configfile = arg
|
24
25
|
end
|
25
26
|
|
26
|
-
opts.on("
|
27
|
-
|
28
|
-
options[:disabled_lint_checks] = arg
|
27
|
+
opts.on("-d", "--debug", "Display or log debugging messages") do
|
28
|
+
loglevel = Logger::DEBUG
|
29
29
|
end
|
30
30
|
|
31
31
|
opts.on("-l [LOGFILE]", "--logfile [LOGFILE]", "Path to logfile. Defaults to no logging, or /var/log/abalone if no filename is passed.") do |arg|
|
32
32
|
logfile = arg || '/var/log/abalone'
|
33
33
|
end
|
34
34
|
|
35
|
-
opts.on("-p PORT", "--port", "Port to listen on. Defaults to 9000.") do |arg|
|
35
|
+
opts.on("-p PORT", "--port PORT", "Port to listen on. Defaults to 9000.") do |arg|
|
36
36
|
options[:port] = arg
|
37
37
|
end
|
38
38
|
|
@@ -51,6 +51,38 @@ logger = Logger.new(logfile)
|
|
51
51
|
logger.level = loglevel
|
52
52
|
options[:logger] = logger
|
53
53
|
|
54
|
+
config = YAML.load_file(configfile) rescue {}
|
55
|
+
options = defaults.merge(config.merge(options))
|
56
|
+
|
57
|
+
if options[:params].class == Hash
|
58
|
+
options[:params].each do |param, data|
|
59
|
+
next if data.nil?
|
60
|
+
next unless data.include? :values
|
61
|
+
|
62
|
+
data[:values].collect! do |value|
|
63
|
+
case value
|
64
|
+
when Regexp
|
65
|
+
value
|
66
|
+
when String
|
67
|
+
# if a string encapsulated regex, replace with that regex
|
68
|
+
value =~ /^\/(.*)\/$/ ? Regexp.new($1) : value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
raise 'Specify only one of a login command or SSH settings' if options.include? :command and options.include? :ssh
|
75
|
+
|
76
|
+
if options.include? :command
|
77
|
+
raise ":params must be an Array or Hash, not #{options[:params].class}" unless [Array, Hash].include? options[:params].class
|
78
|
+
raise ":command should be a String or an Array, not a #{options[:command].class}." unless [Array, String].include? options[:command].class
|
79
|
+
end
|
80
|
+
|
81
|
+
if options.include? :ssh
|
82
|
+
raise "SSH configuration should be a Hash, not a #{options[:ssh].class}." unless options[:ssh].class == Hash
|
83
|
+
raise "SSH configuration must include the host" unless options[:ssh].include? :host
|
84
|
+
end
|
85
|
+
|
54
86
|
puts
|
55
87
|
puts
|
56
88
|
puts "Starting Abalone Web Shell. Browse to http://localhost:#{options[:port]}"
|
data/bin/em.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require "eventmachine"
|
5
|
+
|
6
|
+
|
7
|
+
module ProcessWatcher
|
8
|
+
def receive_data(data)
|
9
|
+
puts "Got: #{data}"
|
10
|
+
end
|
11
|
+
def process_exited
|
12
|
+
puts 'the forked child died!'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
pid = fork{
|
17
|
+
sleep 1
|
18
|
+
puts 'booger'
|
19
|
+
sleep 1
|
20
|
+
puts 'oogles'
|
21
|
+
}
|
22
|
+
|
23
|
+
EM.kqueue=true
|
24
|
+
EventMachine.run {
|
25
|
+
EventMachine.watch_process(pid, ProcessWatcher)
|
26
|
+
EventMachine.add_timer(5){ Process.kill('TERM', pid) }
|
27
|
+
}
|
28
|
+
|
29
|
+
#EM.run{
|
30
|
+
# EM.system('sh', proc { |process|
|
31
|
+
# process.send_data("echo hello\n")
|
32
|
+
# process.send_data("exit\n")
|
33
|
+
# sleep 5
|
34
|
+
# EM.stop
|
35
|
+
# },
|
36
|
+
# proc{ |out,status|
|
37
|
+
# puts(out)
|
38
|
+
# })
|
39
|
+
#}
|
40
|
+
|
41
|
+
#EM.run do
|
42
|
+
# EM.add_periodic_timer(1) { puts 'sec' }
|
43
|
+
# EM.add_timer(1.01) { puts '1 second passed by' }
|
44
|
+
# EM.add_timer(2.01) { puts '2 seconds passed by'; }
|
45
|
+
# EM.add_timer(5.01) do
|
46
|
+
# puts '5 seconds passed by: stopping...'
|
47
|
+
# EM.stop
|
48
|
+
# end
|
49
|
+
#end
|
50
|
+
|
data/lib/abalone.rb
CHANGED
@@ -10,21 +10,39 @@ class Abalone < Sinatra::Base
|
|
10
10
|
set :logging, true
|
11
11
|
set :strict, true
|
12
12
|
set :public_folder, 'public'
|
13
|
+
set :views, 'views'
|
13
14
|
|
14
15
|
before {
|
15
16
|
env["rack.logger"] = settings.logger if settings.logger
|
16
17
|
}
|
17
18
|
|
18
|
-
get '
|
19
|
+
get '/?:user?' do
|
19
20
|
if !request.websocket?
|
20
|
-
redirect '/index.html'
|
21
|
+
#redirect '/index.html'
|
22
|
+
@requestUsername = (settings.respond_to?(:ssh) and ! settings.ssh.include?(:user)) rescue false
|
23
|
+
erb :index
|
21
24
|
else
|
22
25
|
request.websocket do |ws|
|
26
|
+
|
23
27
|
ws.onopen do
|
24
28
|
warn("websocket opened")
|
25
|
-
reader, @writer, @pid = PTY.spawn(
|
29
|
+
reader, @writer, @pid = PTY.spawn(*shell_command)
|
26
30
|
@writer.winsize = [24,80]
|
27
31
|
|
32
|
+
# reader.sync = true
|
33
|
+
# EM.add_periodic_timer(0.05) do
|
34
|
+
# begin
|
35
|
+
# PTY.check(@pid, true)
|
36
|
+
# data = reader.read_nonblock(512) # we read non-blocking to stream data as quickly as we can
|
37
|
+
# ws.send({'event' => 'output', 'data' => data}.to_json)
|
38
|
+
# rescue IO::WaitReadable
|
39
|
+
# # nop
|
40
|
+
# rescue PTY::ChildExited => e
|
41
|
+
# puts "Terminal has exited!"
|
42
|
+
# ws.send({'event' => 'logout'}.to_json)
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
|
28
46
|
# there must be some form of event driven pty interaction, EM or some gem maybe?
|
29
47
|
reader.sync = true
|
30
48
|
@term = Thread.new do
|
@@ -41,7 +59,7 @@ class Abalone < Sinatra::Base
|
|
41
59
|
Thread.exit
|
42
60
|
end
|
43
61
|
ws.send({'event' => 'output', 'data' => data}.to_json)
|
44
|
-
sleep(0.
|
62
|
+
sleep(0.05)
|
45
63
|
end
|
46
64
|
end
|
47
65
|
|
@@ -93,11 +111,73 @@ class Abalone < Sinatra::Base
|
|
93
111
|
@term.join
|
94
112
|
end
|
95
113
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
114
|
+
def sanitized(params)
|
115
|
+
params.reject do |key,val|
|
116
|
+
['captures','splat'].include? key
|
99
117
|
end
|
100
|
-
|
118
|
+
end
|
119
|
+
|
120
|
+
def allowed(param, value)
|
121
|
+
return false unless settings.params.include? param
|
122
|
+
|
123
|
+
config = settings.params[param]
|
124
|
+
return true if config.nil?
|
125
|
+
return true unless config.include? :values
|
126
|
+
|
127
|
+
config[:values].each do |pattern|
|
128
|
+
case pattern
|
129
|
+
when String
|
130
|
+
return true if value == pattern
|
131
|
+
when Regexp
|
132
|
+
return true if pattern.match(value)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
def shell_command()
|
140
|
+
if settings.respond_to? :command
|
141
|
+
return settings.command unless settings.respond_to? :params
|
142
|
+
|
143
|
+
command = settings.command
|
144
|
+
command = command.split if command.class == String
|
145
|
+
|
146
|
+
sanitized(params).each do |param, value|
|
147
|
+
next unless allowed(param, value)
|
148
|
+
|
149
|
+
config = settings.params[param]
|
150
|
+
case config
|
151
|
+
when nil
|
152
|
+
command << "--#{param}" << value
|
153
|
+
when Hash
|
154
|
+
command << (config[:map] || "--#{param}")
|
155
|
+
command << value
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
return command
|
160
|
+
end
|
161
|
+
|
162
|
+
if settings.respond_to? :ssh
|
163
|
+
config = settings.ssh.dup
|
164
|
+
config[:user] ||= params['user'] # if not in the config file, it must come from the user
|
165
|
+
|
166
|
+
if config[:user].nil?
|
167
|
+
warn "SSH configuration must include the user"
|
168
|
+
return ['echo', 'no username provided']
|
169
|
+
end
|
170
|
+
|
171
|
+
command = ['ssh', config[:host] ]
|
172
|
+
command << '-l' << config[:user] if config.include? :user
|
173
|
+
command << '-p' << config[:port] if config.include? :port
|
174
|
+
command << '-i' << config[:cert] if config.include? :cert
|
175
|
+
|
176
|
+
return command
|
177
|
+
end
|
178
|
+
|
179
|
+
# default just to running login
|
180
|
+
'login'
|
101
181
|
end
|
102
182
|
end
|
103
183
|
end
|
data/public/index.html
CHANGED
@@ -36,8 +36,8 @@
|
|
36
36
|
</style>
|
37
37
|
</head>
|
38
38
|
|
39
|
-
<body>
|
40
|
-
<div id="overlay"><input type="button" onclick="javascript:
|
39
|
+
<body onload="connect(boogers);">
|
40
|
+
<div id="overlay"><input type="button" onclick="javascript:connect();" value="reconnect" /></div>
|
41
41
|
<div id="terminal"></div>
|
42
42
|
</body>
|
43
43
|
|
data/public/js/abalone.js
CHANGED
@@ -1,11 +1,28 @@
|
|
1
1
|
var term;
|
2
2
|
var buf = '';
|
3
|
-
var
|
4
|
-
var socket = new WebSocket(protocol + location.host)
|
3
|
+
var socket;
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
function connect(userPrompt) {
|
6
|
+
var protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
7
|
+
|
8
|
+
if(socket) {
|
9
|
+
socket.onclose = null;
|
10
|
+
socket.close();
|
11
|
+
document.getElementById("overlay").style.display = "none";
|
12
|
+
}
|
13
|
+
|
14
|
+
if(userPrompt && location.pathname == '/') {
|
15
|
+
var username = prompt('What username would you like to connect with?');
|
16
|
+
socket = new WebSocket(protocol + location.host + '/' + username + location.search);
|
17
|
+
}
|
18
|
+
else {
|
19
|
+
socket = new WebSocket(protocol + location.host + location.pathname + location.search);
|
20
|
+
}
|
21
|
+
|
22
|
+
socket.onopen = function() { connected(); };
|
23
|
+
socket.onclose = function() { disconnected(); }
|
24
|
+
socket.onmessage = function(m) { messageHandler(m.data); };
|
25
|
+
}
|
9
26
|
|
10
27
|
function connected() {
|
11
28
|
console.log('Client connected');
|
data/views/index.erb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Abalone Web Shell</title>
|
6
|
+
<script src="js/hterm_all.js"></script>
|
7
|
+
<script src="js/abalone.js"></script>
|
8
|
+
<style>
|
9
|
+
html,
|
10
|
+
body {
|
11
|
+
height: 100%;
|
12
|
+
width: 100%;
|
13
|
+
margin: 0px;
|
14
|
+
}
|
15
|
+
#overlay {
|
16
|
+
position: absolute;
|
17
|
+
z-index: 1000;
|
18
|
+
height: 100%;
|
19
|
+
width: 100%;
|
20
|
+
background-color: rgba(0,0,0,0.75);
|
21
|
+
display: none;
|
22
|
+
}
|
23
|
+
#overlay input {
|
24
|
+
display: block;
|
25
|
+
margin: auto;
|
26
|
+
position: relative;
|
27
|
+
top: 50%;
|
28
|
+
transform: translateY(-50%);
|
29
|
+
}
|
30
|
+
#terminal {
|
31
|
+
display: block;
|
32
|
+
position: relative;
|
33
|
+
width: 100%;
|
34
|
+
height: 100%;
|
35
|
+
}
|
36
|
+
</style>
|
37
|
+
</head>
|
38
|
+
|
39
|
+
<body onload="connect(<%= @requestUsername %>);">
|
40
|
+
<div id="overlay"><input type="button" onclick="javascript:connect(<%= @requestUsername %>);" value="reconnect" /></div>
|
41
|
+
<div id="terminal"></div>
|
42
|
+
</body>
|
43
|
+
|
44
|
+
</html>
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abalone
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Ford
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -53,6 +53,8 @@ files:
|
|
53
53
|
- LICENSE
|
54
54
|
- bin/abalone
|
55
55
|
- lib/abalone.rb
|
56
|
+
- bin/em.rb
|
57
|
+
- views/index.erb
|
56
58
|
- public/index.html
|
57
59
|
- public/js/abalone.js
|
58
60
|
- public/js/hterm_all.js
|