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