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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa5b5ee45c18c4e1c7e2db9945967b297d0820da
4
- data.tar.gz: 8baf74e20587c757447985d794195a21d879f965
3
+ metadata.gz: fc6db47f9f103f87a1e38c945833d736b4171008
4
+ data.tar.gz: 4fb8dce6cf6e70ba7e7ec011f7c250d8f540771f
5
5
  SHA512:
6
- metadata.gz: a7c9247504cc5c6840796e6287b70436d727793a93c0f66f5c9fb340d356810952cfee98cf32c3c1114d2c14413a2a6a70464969688899c8f564a41038ca382b
7
- data.tar.gz: 3909b638e6695e1633e55919798cca9fce3aec5c3db0ee383f667e78c00e9a572547b1ff7004b70e417860f16a753b5a46b3c6e39165ed41c12c9752995f2f9f
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.
@@ -2,8 +2,9 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'optparse'
5
- require 'abalone'
6
5
  require 'yaml'
6
+ require 'abalone'
7
+ require 'abalone/terminal'
7
8
 
8
9
  defaults = {
9
10
  :port => 9000,
@@ -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
- ENV['TERM'] ||= 'xterm' # make sure we've got a somewhat sane environment
45
-
46
- if settings.respond_to?(:bannerfile)
47
- ws.send({'data' => File.read(settings.bannerfile).encode(crlf_newline: true)}.to_json)
48
- ws.send({'data' => "\r\n\r\n"}.to_json)
49
- end
50
-
51
- reader, @writer, @pid = PTY.spawn(*shell_command)
52
- @writer.winsize = [24,80]
53
-
54
- # reader.sync = true
55
- # EM.add_periodic_timer(0.05) do
56
- # begin
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
- stop_term()
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
- @writer.write message['data']
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
- @writer.winsize = [row, col]
92
+ @terminal.resize(row, col)
138
93
 
139
94
  when 'logout', 'disconnect'
140
95
  warn("Client exited.")
141
- stop_term()
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
- puts "Remote terminal closed."
148
- puts e.message
149
- stop_term()
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
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
+ }
@@ -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>
@@ -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));
@@ -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
- 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.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="overlay"><input type="button" onclick="javascript:connect(<%= @requestUsername %>);" value="Start Session" /></div>
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.3.4
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-07 00:00:00.000000000 Z
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/index.html
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: {}
@@ -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>