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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 84a963023ccdf4286214f6e0bfcf7392c12289e6
4
- data.tar.gz: 887a90254e4836b4dab02a1e4efc8b4e0dcb6914
3
+ metadata.gz: 1de9af9802ec0b416470cec7b62a3cd7b3dfa053
4
+ data.tar.gz: ea85d95749bb862e9b3f1edd9e69fe4cc73781f7
5
5
  SHA512:
6
- metadata.gz: 9b628557aadd49c273a684830203c2190be6945fc9f5bf081b0a5dcd0a1d750ab0a9b1266c107605e3dcbff094929f51bac3b1152fbfc85b6dcc1e7353f0ecdc
7
- data.tar.gz: 3d530f9fa812d1ab3a4fc753de0df5a763997c70655dd2487a05b83e0cae5f1948b7ea0010c2aceaac6b2aeced8a4790507e3d6535f9058fd76e4c337463ca54
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
+
@@ -3,36 +3,36 @@
3
3
  require 'rubygems'
4
4
  require 'optparse'
5
5
  require 'abalone'
6
+ require 'yaml'
6
7
 
7
- gemroot = File.expand_path(File.join(File.dirname(__FILE__), '..'))
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 = $stderr
14
- loglevel = Logger::WARN
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("-d", "--debug", "Display or log debugging messages") do
23
- loglevel = Logger::DEBUG
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("--disable DISABLED_CHECKS", "Lint checks to disable. Either comma-separated list or filename.") do |arg|
27
- puts "Disabling #{arg}"
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]}"
@@ -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
+
@@ -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 '/' do
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(login_binary)
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.01)
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 login_binary()
97
- [ '/bin/login', '/usr/bin/login' ].each do |path|
98
- return path if File.executable? path
114
+ def sanitized(params)
115
+ params.reject do |key,val|
116
+ ['captures','splat'].include? key
99
117
  end
100
- raise 'No login binary found.'
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
@@ -36,8 +36,8 @@
36
36
  </style>
37
37
  </head>
38
38
 
39
- <body>
40
- <div id="overlay"><input type="button" onclick="javascript:location.reload();" value="reconnect" /></div>
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
 
@@ -1,11 +1,28 @@
1
1
  var term;
2
2
  var buf = '';
3
- var protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
4
- var socket = new WebSocket(protocol + location.host)
3
+ var socket;
5
4
 
6
- socket.onopen = function() { connected(); };
7
- socket.onclose = function() { disconnected(); }
8
- socket.onmessage = function(m) { messageHandler(m.data); };
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');
@@ -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.1
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-14 00:00:00.000000000 Z
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