abalone 0.3.4 → 0.4.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 +87 -0
- data/bin/abalone +2 -1
- data/lib/abalone.rb +42 -136
- data/lib/abalone/buffer.rb +22 -0
- data/lib/abalone/terminal.rb +188 -0
- data/public/css/gear.full.png +0 -0
- data/public/css/gear.png +0 -0
- data/public/css/launcher.css +43 -0
- data/public/css/terminal.css +95 -0
- data/public/demo.html +161 -0
- data/public/js/abalone.js +29 -0
- data/public/js/launcher.js +112 -0
- data/views/index.erb +17 -39
- metadata +10 -3
- data/public/index.html +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc6db47f9f103f87a1e38c945833d736b4171008
|
4
|
+
data.tar.gz: 4fb8dce6cf6e70ba7e7ec011f7c250d8f540771f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b724a5188a06ab85f0495f2e4151ad27e0877887a34b569fa023f9feae31eae48850ef124be654cb18cec0eeacd34de57738622b54d1d39dc4dbc73aa0499a6
|
7
|
+
data.tar.gz: 211f4bc11c85e4e9cce72598fde669f1713515ba67b11850e017c9d9783b60d28c76f29833c1bd1cf93ad22415429ea4a36de227a72f6e9d02f6ac6364b5aabf
|
data/README.md
CHANGED
@@ -7,6 +7,7 @@ A simple Sinatra & hterm based web terminal.
|
|
7
7
|
1. [Configuration](#configuration)
|
8
8
|
1. [SSH](#configuring-ssh)
|
9
9
|
1. [Custom Login Command](#configuring-a-custom-command)
|
10
|
+
1. [jQuery plugin](#jquery-plugin)
|
10
11
|
1. [Limitations](#limitations)
|
11
12
|
|
12
13
|
## Overview
|
@@ -42,6 +43,10 @@ can set several options:
|
|
42
43
|
* File to display before login. This does not interpret special characters the way `getty` does.
|
43
44
|
* `true`, `false`, or filename to display.
|
44
45
|
* Default value: `false`, or `/etc/issue.net` if set to `true`.
|
46
|
+
* `:welcome`
|
47
|
+
* A message to display prior to starting a session. This is on the overlay with the
|
48
|
+
*Start Session* button. Pass a string of text, or a filename. HTML will be interpreted.
|
49
|
+
* Default value: unset
|
45
50
|
* `:logfile`
|
46
51
|
* The path of a file to log to.
|
47
52
|
* Default value: Log only to `STDERR`. If you pass `-l` at the command line
|
@@ -51,6 +56,15 @@ can set several options:
|
|
51
56
|
end of that time. For example, set it to 300 for shells that last for up to
|
52
57
|
five minutes.
|
53
58
|
* Default value: unset.
|
59
|
+
* `:ttl`
|
60
|
+
* The number of seconds a session should last after disconnecting. If you reconnect
|
61
|
+
within this grace period, you'll be reconnected to your session without interruption.
|
62
|
+
This cannot yet restore the secondary terminal buffer, so if you're running something
|
63
|
+
like Vim, you may have to run `clear` or `reset` after exiting to get your console
|
64
|
+
sane again.
|
65
|
+
* Note that `:timeout` takes precedence, so if your session times out, even during
|
66
|
+
the `:ttl` grace period, it will be killed.
|
67
|
+
* Default value: unset.
|
54
68
|
* One of [`:command`](#configuring-a-custom-command) or [`:ssh`](#configuring-ssh), exclusive.
|
55
69
|
* The login method to use. Abalone can use `login`, SSH, or a custom command
|
56
70
|
to start a shell. See configuration instructions below.
|
@@ -140,6 +154,79 @@ The options in this case will be passed to the command like:
|
|
140
154
|
* http://localhost:9000/?username=bob&image=testing&invalid=value
|
141
155
|
* `/usr/local/bin/run-container --username bob`
|
142
156
|
|
157
|
+
## jQuery Plugin
|
158
|
+
|
159
|
+
Abalone comes with a build in jQuery plugin that makes it very easy to use. You
|
160
|
+
can attach the launcher to any element. If it's a `block` element, then a launcher
|
161
|
+
button will be injected inside, and if it's `inline` then it will directly trigger
|
162
|
+
the terminal.
|
163
|
+
|
164
|
+
See a demo of the launcher in action after installation by starting the server and
|
165
|
+
browsing to [http://localhost:9000/demo.html](http://localhost:9000/demo.html).
|
166
|
+
Adjust the URL and port as needed.
|
167
|
+
|
168
|
+
The minimum external dependencies are jQuery and jQuery UI:
|
169
|
+
|
170
|
+
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
171
|
+
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
|
172
|
+
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
173
|
+
|
174
|
+
To load and initialize the launcher, you'll need to load the CSS and Javascript
|
175
|
+
from a running Abalone instance like below. Notice the full URL, including the
|
176
|
+
port number. Alternatively, you can pull those from the repository and host them
|
177
|
+
along with the rest of your HTML.
|
178
|
+
|
179
|
+
<link rel="stylesheet" href="http://localhost:9000/css/launcher.css">
|
180
|
+
<script src="http://localhost:9000/js/launcher.js"></script>
|
181
|
+
|
182
|
+
Then you'll simply declare one or more launchers on any element you choose. Note
|
183
|
+
that you must pass in the server parameter. This should be the location of your
|
184
|
+
Abalone server, including the port it's running on.
|
185
|
+
|
186
|
+
<script>
|
187
|
+
$(document).ready(function() {
|
188
|
+
|
189
|
+
$('pre.popup').AbaloneLauncher({
|
190
|
+
label: "Try out a popup!",
|
191
|
+
title: "Isn't this neat?",
|
192
|
+
server: "http://localhost:9000",
|
193
|
+
});
|
194
|
+
|
195
|
+
$('pre.inline').AbaloneLauncher({
|
196
|
+
label: "Try it out inline!",
|
197
|
+
target: "inline",
|
198
|
+
server: "http://localhost:9000",
|
199
|
+
});
|
200
|
+
|
201
|
+
$('pre.targeted').AbaloneLauncher({
|
202
|
+
label: "Try it out!",
|
203
|
+
target: "#abalone-shell",
|
204
|
+
location: "se",
|
205
|
+
server: "http://localhost:9000",
|
206
|
+
});
|
207
|
+
|
208
|
+
$('a#launcher').AbaloneLauncher({
|
209
|
+
server: "http://localhost:9000",
|
210
|
+
params: { "type": "demo", "uuid": generateUUID() },
|
211
|
+
});
|
212
|
+
|
213
|
+
});
|
214
|
+
</script>
|
215
|
+
|
216
|
+
### Configuration Options
|
217
|
+
|
218
|
+
| Option | Valid values | Default |
|
219
|
+
|------------|-------------------------------------------------------|-----------------------|
|
220
|
+
| `location` | `ne`, `se`, `sw`, `nw` | `ne` |
|
221
|
+
| `label` | *String* | *Launch* |
|
222
|
+
| `title` | *String* | *Abalone Web Shell* |
|
223
|
+
| `target` | `popup`, `inline`, `tab`, CSS selector of a container | `popup` |
|
224
|
+
| `params` | parameters to be passed to the server | `{}` |
|
225
|
+
| `server` | URL to the Abalone server, including port | `null` (**required**) |
|
226
|
+
| `height` | *Integer* | `480` |
|
227
|
+
| `width` | *Integer* | `640` |
|
228
|
+
|
229
|
+
|
143
230
|
## Limitations
|
144
231
|
|
145
232
|
This is super early in development and has not yet been battle tested.
|
data/bin/abalone
CHANGED
data/lib/abalone.rb
CHANGED
@@ -3,15 +3,15 @@ require 'logger'
|
|
3
3
|
require 'sinatra/base'
|
4
4
|
require 'sinatra-websocket'
|
5
5
|
|
6
|
-
require 'pty'
|
7
|
-
require 'io/console'
|
8
|
-
|
9
6
|
class Abalone < Sinatra::Base
|
10
7
|
set :logging, true
|
11
8
|
set :strict, true
|
12
9
|
set :protection, :except => :frame_options
|
13
10
|
set :public_folder, "#{settings.root}/../public"
|
14
11
|
set :views, "#{settings.root}/../views"
|
12
|
+
set :active, {}
|
13
|
+
|
14
|
+
enable :sessions
|
15
15
|
|
16
16
|
before {
|
17
17
|
env["rack.logger"] = settings.logger if settings.logger
|
@@ -21,7 +21,6 @@ class Abalone < Sinatra::Base
|
|
21
21
|
puts "Caught SIGINT. Terminating active sessions (#{Process.pid}) now."
|
22
22
|
exit!
|
23
23
|
end
|
24
|
-
|
25
24
|
}
|
26
25
|
|
27
26
|
# super low cost heartbeat response.
|
@@ -30,97 +29,50 @@ class Abalone < Sinatra::Base
|
|
30
29
|
'alive'
|
31
30
|
end
|
32
31
|
|
32
|
+
get '/api/reset' do
|
33
|
+
if @terminal = settings.active[session.id]
|
34
|
+
@terminal.terminate!
|
35
|
+
end
|
36
|
+
redirect to('/')
|
37
|
+
end
|
38
|
+
|
33
39
|
get '/?:user?' do
|
34
40
|
if !request.websocket?
|
35
|
-
#redirect '/index.html'
|
36
41
|
@requestUsername = (settings.respond_to?(:ssh) and ! settings.ssh.include?(:user)) rescue false
|
37
42
|
@autoconnect = settings.autoconnect
|
43
|
+
if settings.respond_to?(:welcome)
|
44
|
+
begin
|
45
|
+
@welcome = File.file?(settings.welcome) ? File.read(settings.welcome) : settings.welcome
|
46
|
+
rescue => e
|
47
|
+
warn e.message
|
48
|
+
end
|
49
|
+
end
|
38
50
|
erb :index
|
39
51
|
else
|
40
52
|
request.websocket do |ws|
|
41
|
-
|
53
|
+
uid = session.id
|
42
54
|
ws.onopen do
|
43
55
|
warn("websocket opened")
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
#
|
56
|
-
#
|
57
|
-
# PTY.check(@pid, true)
|
58
|
-
# data = reader.read_nonblock(512) # we read non-blocking to stream data as quickly as we can
|
59
|
-
# ws.send({'event' => 'output', 'data' => data}.to_json)
|
60
|
-
# rescue IO::WaitReadable
|
61
|
-
# # nop
|
62
|
-
# rescue PTY::ChildExited => e
|
63
|
-
# puts "Terminal has exited!"
|
64
|
-
# ws.send({'event' => 'logout'}.to_json)
|
65
|
-
# end
|
66
|
-
# end
|
67
|
-
|
68
|
-
# there must be some form of event driven pty interaction, EM or some gem maybe?
|
69
|
-
reader.sync = true
|
70
|
-
@term = Thread.new do
|
71
|
-
carry = []
|
72
|
-
loop do
|
73
|
-
begin
|
74
|
-
PTY.check(@pid, true)
|
75
|
-
output = reader.read_nonblock(512).unpack('C*') # we read non-blocking to stream data as quickly as we can
|
76
|
-
last_low = output.rindex { |x| x < 128 } # find the last low bit
|
77
|
-
trailing = last_low +1
|
78
|
-
|
79
|
-
# use inclusive slices here
|
80
|
-
data = (carry + output[0..last_low]).pack('C*').force_encoding('UTF-8') # repack into a string up until the last low bit
|
81
|
-
carry = output[trailing..-1] # save the any remaining high bits and partial chars for next go-round
|
82
|
-
|
83
|
-
ws.send({'data' => data}.to_json)
|
84
|
-
|
85
|
-
rescue IO::WaitReadable
|
86
|
-
IO.select([reader])
|
87
|
-
retry
|
88
|
-
|
89
|
-
rescue PTY::ChildExited => e
|
90
|
-
warn('Terminal has exited!')
|
91
|
-
ws.close_connection
|
92
|
-
|
93
|
-
@timer.terminate rescue nil
|
94
|
-
@timer.join rescue nil
|
95
|
-
Thread.exit
|
96
|
-
end
|
97
|
-
|
98
|
-
sleep(0.05)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
if settings.respond_to? :timeout
|
103
|
-
@timer = Thread.new do
|
104
|
-
expiration = Time.now + settings.timeout
|
105
|
-
loop do
|
106
|
-
remaining = expiration - Time.now
|
107
|
-
stop_term if remaining < 0
|
108
|
-
|
109
|
-
time = {
|
110
|
-
'event' => 'time',
|
111
|
-
'data' => Time.at(remaining).utc.strftime("%H:%M:%S"),
|
112
|
-
}
|
113
|
-
ws.send(time.to_json)
|
114
|
-
sleep 1
|
115
|
-
end
|
56
|
+
# expire all dead terminals
|
57
|
+
settings.active.delete_if {|uid, term| ! term.alive? }
|
58
|
+
|
59
|
+
if @terminal = settings.active[uid]
|
60
|
+
@terminal.reconnect(ws)
|
61
|
+
else
|
62
|
+
warn "Starting a new session for #{uid}."
|
63
|
+
@terminal = Abalone::Terminal.new(settings, ws, sanitized(params))
|
64
|
+
|
65
|
+
if settings.respond_to? :ttl
|
66
|
+
settings.active[uid] = @terminal
|
67
|
+
logger.debug "Saving session #{uid}."
|
68
|
+
logger.debug "Active sessions: #{settings.active.keys.inspect}"
|
116
69
|
end
|
117
70
|
end
|
118
|
-
|
119
71
|
end
|
120
72
|
|
121
73
|
ws.onclose do
|
122
74
|
warn('websocket closed')
|
123
|
-
|
75
|
+
@terminal.stop! if @terminal
|
124
76
|
end
|
125
77
|
|
126
78
|
ws.onmessage do |message|
|
@@ -129,24 +81,27 @@ class Abalone < Sinatra::Base
|
|
129
81
|
begin
|
130
82
|
case message['event']
|
131
83
|
when 'input'
|
132
|
-
@
|
84
|
+
@terminal.write(message['data'])
|
85
|
+
|
86
|
+
when 'modes'
|
87
|
+
@terminal.modes = message['data']
|
133
88
|
|
134
89
|
when 'resize'
|
135
90
|
row = message['row']
|
136
91
|
col = message['col']
|
137
|
-
@
|
92
|
+
@terminal.resize(row, col)
|
138
93
|
|
139
94
|
when 'logout', 'disconnect'
|
140
95
|
warn("Client exited.")
|
141
|
-
|
96
|
+
@terminal.stop!
|
142
97
|
|
143
98
|
else
|
144
99
|
warn("Unrecognized message: #{message.inspect}")
|
145
100
|
end
|
146
101
|
rescue Errno::EIO => e
|
147
|
-
|
148
|
-
|
149
|
-
|
102
|
+
warn "Remote terminal closed."
|
103
|
+
warn e.message
|
104
|
+
@terminal.stop!
|
150
105
|
|
151
106
|
end
|
152
107
|
|
@@ -160,16 +115,10 @@ class Abalone < Sinatra::Base
|
|
160
115
|
end
|
161
116
|
|
162
117
|
helpers do
|
163
|
-
def stop_term()
|
164
|
-
Process.kill('TERM', @pid) rescue nil
|
165
|
-
sleep 1
|
166
|
-
Process.kill('KILL', @pid) rescue nil
|
167
|
-
@term.join rescue nil
|
168
|
-
end
|
169
118
|
|
170
119
|
def sanitized(params)
|
171
120
|
params.reject do |key,val|
|
172
|
-
['captures','splat'].include? key
|
121
|
+
['captures','splat'].include?(key) or not allowed(key, val)
|
173
122
|
end
|
174
123
|
end
|
175
124
|
|
@@ -192,48 +141,5 @@ class Abalone < Sinatra::Base
|
|
192
141
|
false
|
193
142
|
end
|
194
143
|
|
195
|
-
def shell_command()
|
196
|
-
if settings.respond_to? :command
|
197
|
-
return settings.command unless settings.respond_to? :params
|
198
|
-
|
199
|
-
command = settings.command
|
200
|
-
command = command.split if command.class == String
|
201
|
-
|
202
|
-
sanitized(params).each do |param, value|
|
203
|
-
next unless allowed(param, value)
|
204
|
-
|
205
|
-
config = settings.params[param]
|
206
|
-
case config
|
207
|
-
when nil
|
208
|
-
command << "--#{param}" << value
|
209
|
-
when Hash
|
210
|
-
command << (config[:map] || "--#{param}")
|
211
|
-
command << value
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
return command
|
216
|
-
end
|
217
|
-
|
218
|
-
if settings.respond_to? :ssh
|
219
|
-
config = settings.ssh.dup
|
220
|
-
config[:user] ||= params['user'] # if not in the config file, it must come from the user
|
221
|
-
|
222
|
-
if config[:user].nil?
|
223
|
-
warn "SSH configuration must include the user"
|
224
|
-
return ['echo', 'no username provided']
|
225
|
-
end
|
226
|
-
|
227
|
-
command = ['ssh', config[:host] ]
|
228
|
-
command << '-l' << config[:user] if config.include? :user
|
229
|
-
command << '-p' << config[:port] if config.include? :port
|
230
|
-
command << '-i' << config[:cert] if config.include? :cert
|
231
|
-
|
232
|
-
return command
|
233
|
-
end
|
234
|
-
|
235
|
-
# default just to running login
|
236
|
-
'login'
|
237
|
-
end
|
238
144
|
end
|
239
145
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'json'
|
2
|
+
class Abalone::Buffer
|
3
|
+
def initialize
|
4
|
+
@buffer = ''
|
5
|
+
end
|
6
|
+
|
7
|
+
def send(message)
|
8
|
+
@buffer << JSON.parse(message)['data']
|
9
|
+
rescue
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def close_connection
|
14
|
+
# nop
|
15
|
+
end
|
16
|
+
|
17
|
+
def replay
|
18
|
+
retval = @buffer
|
19
|
+
@buffer = ''
|
20
|
+
retval
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'pty'
|
2
|
+
require 'io/console'
|
3
|
+
require 'abalone/buffer'
|
4
|
+
|
5
|
+
class Abalone::Terminal
|
6
|
+
def initialize(settings, ws, params)
|
7
|
+
@settings = settings
|
8
|
+
@ws = ws
|
9
|
+
@params = params
|
10
|
+
@modes = nil
|
11
|
+
@buffer = Abalone::Buffer.new
|
12
|
+
|
13
|
+
ENV['TERM'] ||= 'xterm-256color' # make sure we've got a somewhat sane environment
|
14
|
+
|
15
|
+
if settings.respond_to?(:bannerfile)
|
16
|
+
@ws.send({'data' => File.read(settings.bannerfile).encode(crlf_newline: true)}.to_json)
|
17
|
+
@ws.send({'data' => "\r\n\r\n"}.to_json)
|
18
|
+
end
|
19
|
+
|
20
|
+
reader, @writer, @pid = PTY.spawn(*shell_command)
|
21
|
+
@writer.winsize = [24,80]
|
22
|
+
|
23
|
+
# there must be some form of event driven pty interaction, EM or some gem maybe?
|
24
|
+
reader.sync = true
|
25
|
+
@term = Thread.new do
|
26
|
+
carry = []
|
27
|
+
loop do
|
28
|
+
begin
|
29
|
+
PTY.check(@pid, true)
|
30
|
+
output = reader.read_nonblock(512).unpack('C*') # we read non-blocking to stream data as quickly as we can
|
31
|
+
last_low = output.rindex { |x| x < 128 } # find the last low bit
|
32
|
+
trailing = last_low +1
|
33
|
+
|
34
|
+
# use inclusive slices here
|
35
|
+
data = (carry + output[0..last_low]).pack('C*').force_encoding('UTF-8') # repack into a string up until the last low bit
|
36
|
+
carry = output[trailing..-1] # save the any remaining high bits and partial chars for next go-round
|
37
|
+
|
38
|
+
@ws.send({'data' => data}.to_json)
|
39
|
+
|
40
|
+
rescue IO::WaitReadable
|
41
|
+
IO.select([reader])
|
42
|
+
retry
|
43
|
+
|
44
|
+
rescue PTY::ChildExited => e
|
45
|
+
warn('Terminal has exited!')
|
46
|
+
@ws.close_connection
|
47
|
+
|
48
|
+
@timer.terminate rescue nil
|
49
|
+
@timer.join rescue nil
|
50
|
+
Thread.exit
|
51
|
+
end
|
52
|
+
|
53
|
+
sleep(0.05)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if @settings.respond_to? :timeout
|
58
|
+
@timer = Thread.new do
|
59
|
+
expiration = Time.now + @settings.timeout
|
60
|
+
loop do
|
61
|
+
remaining = expiration - Time.now
|
62
|
+
if remaining < 0
|
63
|
+
terminate!
|
64
|
+
Thread.exit
|
65
|
+
end
|
66
|
+
|
67
|
+
format = (remaining > 3600) ? "%H:%M:%S" : "%M:%S"
|
68
|
+
time = {
|
69
|
+
'event' => 'time',
|
70
|
+
'data' => Time.at(remaining).utc.strftime(format),
|
71
|
+
}
|
72
|
+
@ws.send(time.to_json)
|
73
|
+
sleep 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def alive?
|
80
|
+
@term.alive?
|
81
|
+
end
|
82
|
+
|
83
|
+
def reconnect(ws)
|
84
|
+
if @ttl # stop the countdown
|
85
|
+
warn "Stopping timeout"
|
86
|
+
@ttl.terminate rescue nil
|
87
|
+
@ttl.join rescue nil
|
88
|
+
@ttl = nil
|
89
|
+
end
|
90
|
+
@ws.close_connection if @ws
|
91
|
+
@ws = ws
|
92
|
+
|
93
|
+
sleep 0.25 # allow the terminal to finish initialization before we blast it.
|
94
|
+
|
95
|
+
if @modes
|
96
|
+
@ws.send({
|
97
|
+
'event' => 'modes',
|
98
|
+
'data' => @modes
|
99
|
+
}.to_json)
|
100
|
+
end
|
101
|
+
@ws.send({'data' => @buffer.replay}.to_json)
|
102
|
+
@writer.write "\cl" # ctrl-l forces a screen redraw
|
103
|
+
end
|
104
|
+
|
105
|
+
def write(message)
|
106
|
+
@writer.write message
|
107
|
+
end
|
108
|
+
|
109
|
+
def modes=(message)
|
110
|
+
raise 'Invalid modes data type' unless message.is_a? Hash
|
111
|
+
@modes = message.select do |key, val|
|
112
|
+
['cursorBlink', 'cursorVisible', 'bracketedPaste', 'applicationCursor'].include? key
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def stop!
|
117
|
+
if @settings.respond_to? :ttl
|
118
|
+
@ws = @buffer
|
119
|
+
@ttl = Thread.new do
|
120
|
+
warn "Providing a shutdown grace period of #{@settings.ttl} seconds."
|
121
|
+
sleep @settings.ttl
|
122
|
+
terminate!
|
123
|
+
end
|
124
|
+
else
|
125
|
+
terminate!
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def terminate!
|
130
|
+
warn "Terminating session."
|
131
|
+
Process.kill('TERM', @pid) rescue nil
|
132
|
+
sleep 1
|
133
|
+
Process.kill('KILL', @pid) rescue nil
|
134
|
+
|
135
|
+
[@ttl, @timer, @term].each do |thread|
|
136
|
+
thread.terminate rescue nil
|
137
|
+
thread.join rescue nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def resize(rows, cols)
|
142
|
+
@writer.winsize = [rows, cols]
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
def shell_command
|
147
|
+
if @settings.respond_to? :command
|
148
|
+
return @settings.command unless @settings.respond_to? :params
|
149
|
+
|
150
|
+
command = @settings.command
|
151
|
+
command = command.split if command.class == String
|
152
|
+
|
153
|
+
@params.each do |param, value|
|
154
|
+
config = @settings.params[param]
|
155
|
+
case config
|
156
|
+
when nil
|
157
|
+
command << "--#{param}" << value
|
158
|
+
when Hash
|
159
|
+
command << (config[:map] || "--#{param}")
|
160
|
+
command << value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
return command
|
165
|
+
end
|
166
|
+
|
167
|
+
if @settings.respond_to? :ssh
|
168
|
+
config = @settings.ssh.dup
|
169
|
+
config[:user] ||= @params['user'] # if not in the config file, it must come from the user
|
170
|
+
|
171
|
+
if config[:user].nil?
|
172
|
+
warn "SSH configuration must include the user"
|
173
|
+
return ['echo', 'no username provided']
|
174
|
+
end
|
175
|
+
|
176
|
+
command = ['ssh', config[:host] ]
|
177
|
+
command << '-l' << config[:user] if config.include? :user
|
178
|
+
command << '-p' << config[:port] if config.include? :port
|
179
|
+
command << '-i' << config[:cert] if config.include? :cert
|
180
|
+
|
181
|
+
return command
|
182
|
+
end
|
183
|
+
|
184
|
+
# default just to running login
|
185
|
+
'login'
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
Binary file
|
data/public/css/gear.png
ADDED
Binary file
|
@@ -0,0 +1,43 @@
|
|
1
|
+
input[type=button].abalone.control {
|
2
|
+
position: absolute;
|
3
|
+
border-radius: 3px;
|
4
|
+
background-color: #dedede;
|
5
|
+
border: 1px solid #ccc;
|
6
|
+
font-size: 0.75em;
|
7
|
+
margin: 0.25em;
|
8
|
+
}
|
9
|
+
input[type=button].abalone.control:hover {
|
10
|
+
background-color: #707070;
|
11
|
+
color: #fff;
|
12
|
+
}
|
13
|
+
input[type=button].abalone.control:hover:disabled,
|
14
|
+
input[type=button].abalone.control:disabled {
|
15
|
+
background-color: #e4e4e4;
|
16
|
+
color: #afafaf;
|
17
|
+
}
|
18
|
+
|
19
|
+
|
20
|
+
.abalone.control.location-ne,
|
21
|
+
.abalone.control.location-nw {
|
22
|
+
top: 0;
|
23
|
+
}
|
24
|
+
.abalone.control.location-se,
|
25
|
+
.abalone.control.location-sw {
|
26
|
+
bottom: 0;
|
27
|
+
}
|
28
|
+
.abalone.control.location-ne,
|
29
|
+
.abalone.control.location-se {
|
30
|
+
right: 0;
|
31
|
+
}
|
32
|
+
.abalone.control.location-nw,
|
33
|
+
.abalone.control.location-sw {
|
34
|
+
left: 0;
|
35
|
+
}
|
36
|
+
|
37
|
+
iframe.abalone {
|
38
|
+
width: 100% !important;
|
39
|
+
}
|
40
|
+
iframe.abalone.popup {
|
41
|
+
padding: 0;
|
42
|
+
margin: 0;
|
43
|
+
}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
html,
|
2
|
+
body {
|
3
|
+
height: 100%;
|
4
|
+
width: 100%;
|
5
|
+
margin: 0px;
|
6
|
+
background-color: #000;
|
7
|
+
}
|
8
|
+
#timer {
|
9
|
+
position: absolute;
|
10
|
+
z-index: 1000;
|
11
|
+
top: 15px;
|
12
|
+
right: 15px;
|
13
|
+
opacity: 0.25;
|
14
|
+
font-size: 3em;
|
15
|
+
font-weight: bolder;
|
16
|
+
color: red;
|
17
|
+
display: none;
|
18
|
+
}
|
19
|
+
#settings {
|
20
|
+
position: absolute;
|
21
|
+
z-index: 1000;
|
22
|
+
top: 3px;
|
23
|
+
right: 3px;
|
24
|
+
height: 16px;
|
25
|
+
width: 16px;
|
26
|
+
opacity: 0.15;
|
27
|
+
background-image: url(gear.png);
|
28
|
+
background-position: right top;
|
29
|
+
background-repeat: no-repeat;
|
30
|
+
}
|
31
|
+
#settings a,
|
32
|
+
#settings:hover ul,
|
33
|
+
#settings:hover ul li {
|
34
|
+
display: block;
|
35
|
+
padding: 0;
|
36
|
+
margin: 0;
|
37
|
+
}
|
38
|
+
#settings ul {
|
39
|
+
display: none;
|
40
|
+
}
|
41
|
+
#settings a {
|
42
|
+
text-decoration: none;
|
43
|
+
padding-right: 1em;
|
44
|
+
color: blue;
|
45
|
+
}
|
46
|
+
#settings:hover {
|
47
|
+
opacity: 1.0;
|
48
|
+
width: auto;
|
49
|
+
height: auto;
|
50
|
+
background-color: #b6a5a0;
|
51
|
+
background-image: none;
|
52
|
+
color: #fff;
|
53
|
+
font-weight: bold;
|
54
|
+
border: 1px solid #ccc;
|
55
|
+
}
|
56
|
+
#settings:hover ul {
|
57
|
+
list-style-type: none;
|
58
|
+
}
|
59
|
+
#settings:hover ul li:hover {
|
60
|
+
background-color: #ccc;
|
61
|
+
}
|
62
|
+
#overlay {
|
63
|
+
position: absolute;
|
64
|
+
z-index: 1000;
|
65
|
+
height: 100%;
|
66
|
+
width: 100%;
|
67
|
+
background-color: rgba(0,0,0,0.75);
|
68
|
+
}
|
69
|
+
#overlay input {
|
70
|
+
display: block;
|
71
|
+
margin: auto;
|
72
|
+
position: relative;
|
73
|
+
top: 50%;
|
74
|
+
transform: translateY(-50%);
|
75
|
+
}
|
76
|
+
#overlay #welcome {
|
77
|
+
color: white;
|
78
|
+
width: 65%;
|
79
|
+
margin: 4em auto 0;
|
80
|
+
padding: 0.5em 1em;
|
81
|
+
font-size: 2em;
|
82
|
+
border: 1px solid #ccc;
|
83
|
+
border-radius: 0.15em;
|
84
|
+
background-color: #1c0d00;
|
85
|
+
box-shadow: 3px 3px 5px 2px #333;
|
86
|
+
}
|
87
|
+
#overlay #welcome + input {
|
88
|
+
top: 35%;
|
89
|
+
}
|
90
|
+
#terminal {
|
91
|
+
display: block;
|
92
|
+
position: relative;
|
93
|
+
width: 100%;
|
94
|
+
height: 100%;
|
95
|
+
}
|
data/public/demo.html
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
<html
|
2
|
+
<head>
|
3
|
+
<title>Abalone Web Shell Demo</title>
|
4
|
+
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
|
5
|
+
<link rel="stylesheet" href="http://localhost:9000/css/launcher.css">
|
6
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/default.min.css">
|
7
|
+
|
8
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js"></script>
|
9
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
10
|
+
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
11
|
+
<script src="http://localhost:9000/js/launcher.js"></script>
|
12
|
+
<script>
|
13
|
+
hljs.initHighlightingOnLoad();
|
14
|
+
|
15
|
+
$(document).ready(function() {
|
16
|
+
|
17
|
+
$('pre.popup').AbaloneLauncher({
|
18
|
+
label: "Try out a popup!",
|
19
|
+
title: "Isn't this neat?",
|
20
|
+
server: "http://localhost:9000",
|
21
|
+
});
|
22
|
+
|
23
|
+
$('pre.inline').AbaloneLauncher({
|
24
|
+
label: "Try it out inline!",
|
25
|
+
target: "inline",
|
26
|
+
server: "http://localhost:9000",
|
27
|
+
});
|
28
|
+
|
29
|
+
$('pre.targeted').AbaloneLauncher({
|
30
|
+
label: "Try it out!",
|
31
|
+
target: "#abalone-shell",
|
32
|
+
location: "se",
|
33
|
+
server: "http://localhost:9000",
|
34
|
+
});
|
35
|
+
|
36
|
+
$('pre.tab').AbaloneLauncher({
|
37
|
+
label: "Try it in a new tab/window!",
|
38
|
+
target: "tab",
|
39
|
+
location: "se",
|
40
|
+
server: "http://localhost:9000",
|
41
|
+
});
|
42
|
+
|
43
|
+
$('a#launcher').AbaloneLauncher({
|
44
|
+
server: "http://localhost:9000",
|
45
|
+
});
|
46
|
+
|
47
|
+
});
|
48
|
+
</script>
|
49
|
+
|
50
|
+
<style>
|
51
|
+
#container {
|
52
|
+
display: flex;
|
53
|
+
justify-content: space-between;
|
54
|
+
border-top: 1px solid #ccc;
|
55
|
+
}
|
56
|
+
table {
|
57
|
+
width: 75%;
|
58
|
+
margin: 0 auto;
|
59
|
+
border-collapse: collapse;
|
60
|
+
}
|
61
|
+
th, td {
|
62
|
+
border: 1px solid black;
|
63
|
+
padding: 0 0.25em;
|
64
|
+
}
|
65
|
+
.column {
|
66
|
+
width: 50%;
|
67
|
+
margin: 0 1em;
|
68
|
+
}
|
69
|
+
.console {
|
70
|
+
border: 4px solid #a53737;
|
71
|
+
padding: 8px;
|
72
|
+
margin: 0;
|
73
|
+
}
|
74
|
+
#abalone-shell iframe {
|
75
|
+
height: 500px;
|
76
|
+
}
|
77
|
+
</style>
|
78
|
+
|
79
|
+
</head>
|
80
|
+
<body>
|
81
|
+
<h1>Abalone Web Shell launcher demo</h1>
|
82
|
+
<p>
|
83
|
+
This page demonstrates some of the ways you can use the Abalone launcher system.
|
84
|
+
The minimum external dependencies are jQuery and jQuery UI. See the source code
|
85
|
+
of this page and read the <code>README.me</code> for more configuration info.
|
86
|
+
</p>
|
87
|
+
|
88
|
+
<div id="container">
|
89
|
+
<div id="content" class="column">
|
90
|
+
<h3>Targeted Demo</h3>
|
91
|
+
<p>
|
92
|
+
If you pass a selector to a container element when you declare the launcher
|
93
|
+
then the terminal window will be injected into that container.
|
94
|
+
</p>
|
95
|
+
|
96
|
+
<pre class="targeted"><code class="language-javascript">$('pre.targeted').AbaloneLauncher({
|
97
|
+
label: "Try it out!",
|
98
|
+
target: "#abalone-shell",
|
99
|
+
location: "se",
|
100
|
+
server: "http://localhost:9000",
|
101
|
+
});</code></pre>
|
102
|
+
|
103
|
+
<h3>Inline Demo</h3>
|
104
|
+
<p>
|
105
|
+
Declare the target as <code>inline</code> and the terminal will be appended
|
106
|
+
to the container element that you attached the launcher to.
|
107
|
+
</p>
|
108
|
+
|
109
|
+
<pre class="inline"><code class="language-javascript">$('pre.inline').AbaloneLauncher({
|
110
|
+
label: "Try it out inline!",
|
111
|
+
target: "inline",
|
112
|
+
server: "http://localhost:9000",
|
113
|
+
});</code></pre>
|
114
|
+
|
115
|
+
<h3>Popup Demo</h3>
|
116
|
+
<p>
|
117
|
+
If you don't declare a target, or if you set it to <code>popup</code>, then you'll
|
118
|
+
get a popup dialog with a terminal in it.
|
119
|
+
</p>
|
120
|
+
|
121
|
+
<pre class="popup"><code class="language-javascript">$('pre.popup').AbaloneLauncher({
|
122
|
+
label: "Try out a popup!",
|
123
|
+
title: "Isn't this neat?",
|
124
|
+
server: "http://localhost:9000",
|
125
|
+
});</code></pre>
|
126
|
+
|
127
|
+
<h3>Tab/Window Demo</h3>
|
128
|
+
<p>
|
129
|
+
Declare the target as <code>tab</code> and the terminal will be opened in a new
|
130
|
+
tab or window, depending on browser preferences.
|
131
|
+
</p>
|
132
|
+
|
133
|
+
<pre class="tab"><code class="language-javascript">$('pre.tab').AbaloneLauncher({
|
134
|
+
label: "Try it in a new tab/window!",
|
135
|
+
target: "tab",
|
136
|
+
location: "se",
|
137
|
+
server: "http://localhost:9000",
|
138
|
+
});</code></pre>
|
139
|
+
|
140
|
+
<h3>Trigger via a link</h3>
|
141
|
+
<p>
|
142
|
+
You can attach the launcher to any element. If it's a <code>block</code> element,
|
143
|
+
like the prior examples, then the launcher button will be injected into it and if
|
144
|
+
it's an <code>inline</code> element, <a href="#" id="launcher">like this</a>
|
145
|
+
then it will trigger the terminal directly.
|
146
|
+
</p>
|
147
|
+
|
148
|
+
<pre><code class="language-javascript">$('a#launcher').AbaloneLauncher({
|
149
|
+
server: "http://localhost:9000",
|
150
|
+
});</code></pre>
|
151
|
+
|
152
|
+
</div>
|
153
|
+
|
154
|
+
<div id="workspace" class="column">
|
155
|
+
<h2>Workspace</h2>
|
156
|
+
<div class="console"><div id="abalone-shell"></div></div>
|
157
|
+
</div>
|
158
|
+
|
159
|
+
</div>
|
160
|
+
</body>
|
161
|
+
</html>
|
data/public/js/abalone.js
CHANGED
@@ -53,6 +53,16 @@ function connected() {
|
|
53
53
|
buf = '';
|
54
54
|
}
|
55
55
|
});
|
56
|
+
|
57
|
+
/* save our terminal state on the server periodically, just in case we restart a session */
|
58
|
+
// Note: Don't try to tie it to the setDECMode() method as that's called all the time.
|
59
|
+
setInterval(function() {
|
60
|
+
socket.send(JSON.stringify({
|
61
|
+
event: 'modes',
|
62
|
+
data: getModes()
|
63
|
+
}));
|
64
|
+
}, 5000);
|
65
|
+
|
56
66
|
}
|
57
67
|
|
58
68
|
function disconnected() {
|
@@ -71,6 +81,9 @@ function messageHandler(message) {
|
|
71
81
|
document.getElementById("timer").innerHTML = data;
|
72
82
|
break;
|
73
83
|
|
84
|
+
case 'modes':
|
85
|
+
setModes(data);
|
86
|
+
|
74
87
|
default:
|
75
88
|
if (!term) {
|
76
89
|
buf += data;
|
@@ -80,6 +93,22 @@ function messageHandler(message) {
|
|
80
93
|
}
|
81
94
|
}
|
82
95
|
|
96
|
+
function getModes() {
|
97
|
+
return {
|
98
|
+
"cursorBlink" : term.vt.terminal.options_.cursorBlink,
|
99
|
+
"cursorVisible" : term.vt.terminal.options_.cursorVisible,
|
100
|
+
"bracketedPaste" : term.vt.terminal.options_.bracketedPaste,
|
101
|
+
"applicationCursor" : term.vt.terminal.keyboard.applicationCursor
|
102
|
+
};
|
103
|
+
}
|
104
|
+
|
105
|
+
function setModes(modes) {
|
106
|
+
term.vt.terminal.options_.cursorBlink = modes["cursorBlink"];
|
107
|
+
term.vt.terminal.options_.cursorVisible = modes["cursorVisible"];
|
108
|
+
term.vt.terminal.options_.bracketedPaste = modes["bracketedPaste"];
|
109
|
+
term.vt.terminal.keyboard.applicationCursor = modes["applicationCursor"];
|
110
|
+
}
|
111
|
+
|
83
112
|
/* borrowed from https://github.com/krishnasrinivas/wetty */
|
84
113
|
function Abalone(argv) {
|
85
114
|
this.argv_ = argv;
|
@@ -0,0 +1,112 @@
|
|
1
|
+
(function ( $ ) {
|
2
|
+
/* global counter for constructing unique IDs */
|
3
|
+
abaloneInstanceCount = 0;
|
4
|
+
|
5
|
+
$.fn.AbaloneLauncher = function(options) {
|
6
|
+
var settings = $.extend({
|
7
|
+
location: 'ne',
|
8
|
+
label: "Launch",
|
9
|
+
title: "Abalone Web Shell",
|
10
|
+
target: "popup",
|
11
|
+
params: {},
|
12
|
+
server: null,
|
13
|
+
height: 480,
|
14
|
+
width: 640,
|
15
|
+
}, options );
|
16
|
+
|
17
|
+
return this.each(function() {
|
18
|
+
if (settings.server == null) {
|
19
|
+
console.log("[FATAL] Abalone: server is a required parameter.")
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
|
23
|
+
abaloneInstanceCount++;
|
24
|
+
var element = $(this);
|
25
|
+
var serverURL = settings.server + '?' + $.param(settings.params);
|
26
|
+
|
27
|
+
if (element.css("display") == "block") {
|
28
|
+
var launcher = $("<input>", {
|
29
|
+
"type": "button",
|
30
|
+
"class": "launcher",
|
31
|
+
"value": settings.label,
|
32
|
+
});
|
33
|
+
|
34
|
+
launcher.addClass("control");
|
35
|
+
|
36
|
+
if (-1 != $.inArray(settings.location, ["ne", "se", "sw", "nw"])) {
|
37
|
+
launcher.addClass("location-" + settings.location);
|
38
|
+
}
|
39
|
+
|
40
|
+
/* we need this for the absolutely posititioned button */
|
41
|
+
if (element.css("position") == "static" ) {
|
42
|
+
element.css("position", "relative");
|
43
|
+
}
|
44
|
+
|
45
|
+
element.prepend(launcher);
|
46
|
+
} else {
|
47
|
+
if (settings.target == "inline") {
|
48
|
+
console.log("[FATAL] Abalone: you cannot use the inline target without a container.")
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
var launcher = element;
|
52
|
+
}
|
53
|
+
launcher.addClass("abalone launcher instance-"+abaloneInstanceCount);
|
54
|
+
launcher.click(function(e) {
|
55
|
+
e.preventDefault();
|
56
|
+
var button = $(this);
|
57
|
+
switch(settings.target) {
|
58
|
+
case "popup":
|
59
|
+
var abalone = $("<iframe>", { "class": "abalone popup", "src": serverURL });
|
60
|
+
abalone.dialog({
|
61
|
+
height: settings.height,
|
62
|
+
width: settings.width,
|
63
|
+
title: settings.title,
|
64
|
+
close: function( event, ui ) {
|
65
|
+
button.prop('disabled', false);
|
66
|
+
abalone.remove();
|
67
|
+
}
|
68
|
+
});
|
69
|
+
button.prop('disabled', true);
|
70
|
+
break;
|
71
|
+
|
72
|
+
case "inline":
|
73
|
+
var abalone = $("<iframe>", { "class": "abalone inline", "src": serverURL });
|
74
|
+
element.append(abalone);
|
75
|
+
break;
|
76
|
+
|
77
|
+
case "tab":
|
78
|
+
window.open(serverURL, 'abaloneTerminal');
|
79
|
+
break
|
80
|
+
|
81
|
+
/* Assume that the user has passed in a string as a selector target */
|
82
|
+
default:
|
83
|
+
var abalone = $("<iframe>", { "class": "abalone targeted", "src": serverURL });
|
84
|
+
var target = $(settings.target)
|
85
|
+
target.append(abalone);
|
86
|
+
break;
|
87
|
+
}
|
88
|
+
|
89
|
+
/* swap out for the close button, unless we're using a popup/tab */
|
90
|
+
if (["popup", "tab"].indexOf(settings.target) == -1) {
|
91
|
+
var close = $("<input>", {
|
92
|
+
"type": "button",
|
93
|
+
"class": "abalone inline control exit location-" + settings.location,
|
94
|
+
"value": "Close",
|
95
|
+
});
|
96
|
+
close.on("click", function() {
|
97
|
+
$(this).remove();
|
98
|
+
abalone.remove();
|
99
|
+
button.show();
|
100
|
+
});
|
101
|
+
|
102
|
+
element.prepend(close);
|
103
|
+
button.hide();
|
104
|
+
}
|
105
|
+
|
106
|
+
});
|
107
|
+
|
108
|
+
return this;
|
109
|
+
});
|
110
|
+
};
|
111
|
+
|
112
|
+
}(jQuery));
|
data/views/index.erb
CHANGED
@@ -3,53 +3,31 @@
|
|
3
3
|
<head>
|
4
4
|
<meta charset="UTF-8">
|
5
5
|
<title>Abalone Web Shell</title>
|
6
|
+
<link rel="stylesheet" href="css/terminal.css">
|
6
7
|
<script src="js/hterm_all.js"></script>
|
7
8
|
<script src="js/abalone.js"></script>
|
9
|
+
|
10
|
+
<% if @autoconnect %>
|
8
11
|
<style>
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
width: 100%;
|
13
|
-
margin: 0px;
|
14
|
-
}
|
15
|
-
#timer {
|
16
|
-
position: absolute;
|
17
|
-
z-index: 1000;
|
18
|
-
top: 15px;
|
19
|
-
right: 15px;
|
20
|
-
opacity: 0.25;
|
21
|
-
font-size: 3em;
|
22
|
-
font-weight: bolder;
|
23
|
-
color: red;
|
24
|
-
display: none;
|
25
|
-
}
|
26
|
-
#overlay {
|
27
|
-
position: absolute;
|
28
|
-
z-index: 1000;
|
29
|
-
height: 100%;
|
30
|
-
width: 100%;
|
31
|
-
background-color: rgba(0,0,0,0.75);
|
32
|
-
<% if @autoconnect %>display: none;<% end %>
|
33
|
-
}
|
34
|
-
#overlay input {
|
35
|
-
display: block;
|
36
|
-
margin: auto;
|
37
|
-
position: relative;
|
38
|
-
top: 50%;
|
39
|
-
transform: translateY(-50%);
|
40
|
-
}
|
41
|
-
#terminal {
|
42
|
-
display: block;
|
43
|
-
position: relative;
|
44
|
-
width: 100%;
|
45
|
-
height: 100%;
|
46
|
-
}
|
12
|
+
#overlay {
|
13
|
+
display: none;
|
14
|
+
}
|
47
15
|
</style>
|
16
|
+
<% end %>
|
48
17
|
</head>
|
49
18
|
|
50
19
|
<body <% if @autoconnect %>onload="connect(<%= @requestUsername %>);"<% end %>>
|
51
20
|
<div id="timer"></div>
|
52
|
-
<div id="
|
21
|
+
<div id="settings">
|
22
|
+
<ul>
|
23
|
+
<li><a href="/api/reset">Reset Terminal</a></li>
|
24
|
+
<li><a href="https://github.com/binford2k/abalone" target="_new">Project Page</a></li>
|
25
|
+
</ul>
|
26
|
+
</div>
|
27
|
+
<div id="overlay">
|
28
|
+
<% if @welcome %><div id="welcome"><%= @welcome %></div><% end %>
|
29
|
+
<input type="button" onclick="javascript:connect(<%= @requestUsername %>);" value="Start Session" />
|
30
|
+
</div>
|
53
31
|
<div id="terminal"></div>
|
54
32
|
</body>
|
55
33
|
|
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.
|
4
|
+
version: 0.4.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: 2017-02-
|
11
|
+
date: 2017-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -52,12 +52,19 @@ files:
|
|
52
52
|
- README.md
|
53
53
|
- LICENSE
|
54
54
|
- bin/abalone
|
55
|
+
- lib/abalone/buffer.rb
|
56
|
+
- lib/abalone/terminal.rb
|
55
57
|
- lib/abalone/watchdog.rb
|
56
58
|
- lib/abalone.rb
|
57
59
|
- views/index.erb
|
58
|
-
- public/
|
60
|
+
- public/css/gear.full.png
|
61
|
+
- public/css/gear.png
|
62
|
+
- public/css/launcher.css
|
63
|
+
- public/css/terminal.css
|
64
|
+
- public/demo.html
|
59
65
|
- public/js/abalone.js
|
60
66
|
- public/js/hterm_all.js
|
67
|
+
- public/js/launcher.js
|
61
68
|
homepage: https://github.com/binford2k/abalone/
|
62
69
|
licenses: []
|
63
70
|
metadata: {}
|
data/public/index.html
DELETED
@@ -1,53 +0,0 @@
|
|
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
|
-
#timer {
|
16
|
-
position: absolute;
|
17
|
-
z-index: 1000;
|
18
|
-
top: 15px;
|
19
|
-
right: 15px;
|
20
|
-
opacity: 0.5;
|
21
|
-
display: none;
|
22
|
-
}
|
23
|
-
#overlay {
|
24
|
-
position: absolute;
|
25
|
-
z-index: 1000;
|
26
|
-
height: 100%;
|
27
|
-
width: 100%;
|
28
|
-
background-color: rgba(0,0,0,0.75);
|
29
|
-
display: none;
|
30
|
-
}
|
31
|
-
#overlay input {
|
32
|
-
display: block;
|
33
|
-
margin: auto;
|
34
|
-
position: relative;
|
35
|
-
top: 50%;
|
36
|
-
transform: translateY(-50%);
|
37
|
-
}
|
38
|
-
#terminal {
|
39
|
-
display: block;
|
40
|
-
position: relative;
|
41
|
-
width: 100%;
|
42
|
-
height: 100%;
|
43
|
-
}
|
44
|
-
</style>
|
45
|
-
</head>
|
46
|
-
|
47
|
-
<body onload="connect();">
|
48
|
-
<div id="timer"></div>
|
49
|
-
<div id="overlay"><input type="button" onclick="javascript:connect();" value="reconnect" /></div>
|
50
|
-
<div id="terminal"></div>
|
51
|
-
</body>
|
52
|
-
|
53
|
-
</html>
|