abalone 0.3.4 → 0.4.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 +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>
|