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 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