abalone 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|