websocket-server 1.0.1-java

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +185 -0
  4. data/Rakefile +56 -0
  5. data/exe/websocket +41 -0
  6. data/lib/log.rb +244 -0
  7. data/lib/server/mime_types.rb +38 -0
  8. data/lib/websocket/arguments_parser.rb +142 -0
  9. data/lib/websocket/channel_initializer.rb +73 -0
  10. data/lib/websocket/config.rb +59 -0
  11. data/lib/websocket/encoding.rb +21 -0
  12. data/lib/websocket/file_server_channel_progressive_future_listener.rb +32 -0
  13. data/lib/websocket/frame_handler.rb +71 -0
  14. data/lib/websocket/header_helpers.rb +70 -0
  15. data/lib/websocket/http_static_file_server_handler.rb +50 -0
  16. data/lib/websocket/http_static_file_server_handler_instance_methods.rb +160 -0
  17. data/lib/websocket/idle_handler.rb +41 -0
  18. data/lib/websocket/idle_state_user_event_handler.rb +47 -0
  19. data/lib/websocket/instance_methods.rb +127 -0
  20. data/lib/websocket/listenable.rb +41 -0
  21. data/lib/websocket/message_handler.rb +47 -0
  22. data/lib/websocket/response_helpers.rb +83 -0
  23. data/lib/websocket/server.rb +26 -0
  24. data/lib/websocket/shutdown_hook.rb +36 -0
  25. data/lib/websocket/ssl_cipher_inspector.rb +44 -0
  26. data/lib/websocket/ssl_context_initialization.rb +106 -0
  27. data/lib/websocket/telnet_proxy.rb +22 -0
  28. data/lib/websocket/validation_helpers.rb +51 -0
  29. data/lib/websocket/version.rb +16 -0
  30. data/lib/websocket-server.rb +13 -0
  31. data/lib/websocket_client.rb +478 -0
  32. data/lib/websocket_server.rb +50 -0
  33. data/web/client.html +43 -0
  34. data/web/css/client/console.css +167 -0
  35. data/web/css/client/parchment.css +112 -0
  36. data/web/favicon.ico +0 -0
  37. data/web/fonts/droidsansmono.v4.woff +0 -0
  38. data/web/js/client/ansispan.js +103 -0
  39. data/web/js/client/client.js +144 -0
  40. data/web/js/client/console.js +393 -0
  41. data/web/js/client/websocket.js +76 -0
  42. data/web/js/jquery.min.js +2 -0
  43. metadata +145 -0
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # =begin
8
+ #
9
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
10
+ #
11
+ # =end
12
+
13
+ require_relative 'websocket_server'
@@ -0,0 +1,478 @@
1
+ #! /usr/bin/env jruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # =begin
10
+ #
11
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
12
+ #
13
+ # =end
14
+
15
+ require 'optparse'
16
+
17
+ require 'java'
18
+ require 'netty'
19
+
20
+ require 'log'
21
+
22
+ # Simple websocket client ported from the example from Netty-io.
23
+
24
+ # The WebSocket module
25
+ module WebSocket
26
+ # rubocop: disable Metrics/AbcSize
27
+ def client_config
28
+ @client_config ||= {
29
+ log_level: Logger::INFO,
30
+ uri: java.net.URI.new('ws://127.0.0.1:4000/websocket'),
31
+ # Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
32
+ # If you change it to V00, ping is not supported and remember to change
33
+ # HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
34
+ websocket_version: Java::io.netty.handler.codec.http.websocketx.WebSocketVersion::V13,
35
+ subprotocol: nil,
36
+ allow_extensions: true,
37
+ default_headers: Java::io.netty.handler.codec.http.DefaultHttpHeaders.new,
38
+ prompt: '>'
39
+ }
40
+ end
41
+ module_function :client_config
42
+ # rubocop: enable Metrics/AbcSize
43
+ end
44
+ # module WebSocket
45
+
46
+ # The WebSocket module
47
+ module WebSocket
48
+ SUPPORTED_SCHEMES = %w[ws wss].freeze
49
+ CHANNEL_TYPE = Java::io.netty.channel.socket.nio.NioSocketChannel.java_class
50
+ PING_MSG_CONTENT = [8, 1, 8, 1].to_java(:byte)
51
+ UNEXPECTED_FULL_RESPONSE_ERROR =
52
+ 'Unexpected FullHttpResponse (getStatus=%<status>s, content=%<content>s)'.freeze
53
+ end
54
+ # module WebSocket
55
+
56
+ # The WebSocket module
57
+ module WebSocket
58
+ java_import Java::io.netty.bootstrap.Bootstrap
59
+ java_import Java::io.netty.channel.nio.NioEventLoopGroup
60
+ java_import Java::io.netty.handler.ssl.SslContext
61
+ java_import Java::io.netty.handler.ssl.SslContextBuilder
62
+ java_import Java::io.netty.handler.ssl.util.InsecureTrustManagerFactory
63
+
64
+ # The ClientInitializationMethods module
65
+ module ClientInitializationMethods
66
+ def init(options)
67
+ @options = options
68
+ configure_from_uri
69
+ @queue = java.util.concurrent.LinkedBlockingQueue.new
70
+ end
71
+
72
+ def bootstrap
73
+ @bootstrap = Bootstrap.new
74
+ @bootstrap.group(client_group)
75
+ @bootstrap.channel(::WebSocket::CHANNEL_TYPE)
76
+ @bootstrap.handler(logging_handler) if @options[:log_requests]
77
+ @bootstrap.handler(channel_initializer)
78
+ end
79
+
80
+ def client_group
81
+ @client_group ||= NioEventLoopGroup.new
82
+ end
83
+
84
+ def logging_handler
85
+ @logging_handler ||= LoggingHandler.new(LogLevel::INFO)
86
+ end
87
+
88
+ def channel_initializer
89
+ @channel_initializer ||= ::WebSocket::ClientChannelInitializer.new(@host, @port, @scheme, @options)
90
+ end
91
+
92
+ def validate_scheme(scheme)
93
+ raise 'Only WS(S) is supported' unless ::WebSocket::SUPPORTED_SCHEMES.include?(scheme)
94
+ end
95
+
96
+ def configure_from_uri
97
+ @uri = @options[:uri]
98
+ @scheme = validate_scheme(@uri.getScheme()&.downcase || 'ws')
99
+ @host = @uri.getHost() || '127.0.0.1'
100
+ @port = @uri.getPort() || case @scheme
101
+ when /ws/i then 80
102
+ when /wss/i then 443
103
+ else -1
104
+ end
105
+ end
106
+
107
+ def configure_handlers(*handlers, &block)
108
+ channel_initializer.default_handler.add_listener(self)
109
+ channel_initializer.default_handler.listeners.addAll(handlers)
110
+ @user_app = block
111
+ @application_handler = lambda do |ctx, msg|
112
+ if @user_app.nil? || @user_app.arity == 1
113
+ @queue.add(msg.chomp)
114
+ else
115
+ @user_app.call(ctx, msg)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ # module ClientInitializationMethods
121
+ end
122
+ # module WebSocket
123
+
124
+ # The WebSocket module
125
+ module WebSocket
126
+ java_import Java::io.netty.buffer.Unpooled
127
+ java_import Java::io.netty.channel.AbstractChannel
128
+ java_import Java::io.netty.handler.codec.http.websocketx.CloseWebSocketFrame
129
+ java_import Java::io.netty.handler.codec.http.websocketx.PingWebSocketFrame
130
+ java_import Java::io.netty.handler.codec.http.websocketx.PongWebSocketFrame
131
+ java_import Java::io.netty.handler.codec.http.websocketx.TextWebSocketFrame
132
+
133
+ # The ClientInstanceMethods module
134
+ module ClientInstanceMethods
135
+ def puts(msg)
136
+ sleep 0.1 until @channel.isActive()
137
+ msg.chomp!
138
+ log.trace "#puts msg: #{msg.inspect}"
139
+ raise 'Message is empty!' if msg.nil? || msg.empty?
140
+ @last_write_future = @channel.writeAndFlush(TextWebSocketFrame.new(msg))
141
+ end
142
+
143
+ def gets
144
+ log.debug 'Waiting for response from server'
145
+ @queue.take
146
+ rescue StandardError => e
147
+ warn "Unexpected error waiting for message: #{e.message}"
148
+ nil
149
+ end
150
+
151
+ def connect
152
+ @channel = bootstrap.connect(@host, @port).sync().channel()
153
+ channel_initializer.default_handler.handshake_future.sync()
154
+ rescue AbstractChannel::AnnotatedConnectException => e
155
+ raise e.message
156
+ rescue StandardError => e
157
+ raise "Connection failure: #{e.message}"
158
+ end
159
+
160
+ def close(channel = @channel)
161
+ log.debug 'Closing primary channel'
162
+ channel.writeAndFlush(CloseWebSocketFrame.new)
163
+ channel.closeFuture().sync()
164
+ ensure
165
+ shutdown
166
+ end
167
+
168
+ def shutdown
169
+ log.debug 'Shutting down gracefully'
170
+ @client_group&.shutdownGracefully()
171
+ ensure
172
+ client_has_shut_down
173
+ end
174
+
175
+ def ping
176
+ @channel.writeAndFlush(PingWebSocketFrame.new(Unpooled.wrappedBuffer(PING_MSG_CONTENT)))
177
+ end
178
+
179
+ def session
180
+ when_client_has_shut_down(@client_group) do |group|
181
+ log.debug "Channel group has shut down: #{group.inspect}"
182
+ end
183
+ @user_app.nil? ? read_user_commands : invoke_user_app
184
+ end
185
+
186
+ def invoke_user_app
187
+ @user_app&.call(self)
188
+ ensure
189
+ close
190
+ end
191
+
192
+ def shut_down_callbacks
193
+ @shut_down_callbacks ||= []
194
+ end
195
+
196
+ def when_client_has_shut_down(*args, &block)
197
+ shut_down_callbacks << {
198
+ block: block,
199
+ args: args
200
+ }
201
+ self
202
+ end
203
+
204
+ def client_has_shut_down
205
+ shut_down_callbacks.take_while do |callback|
206
+ callback[:block]&.call(*callback.fetch(:args, []))
207
+ end
208
+ rescue StandardError => e
209
+ log.error e.message
210
+ end
211
+
212
+ def channelActive(ctx)
213
+ log.info "Connected to #{ctx.channel().remoteAddress0()}"
214
+ end
215
+
216
+ def channelInactive(ctx)
217
+ log.info "Disconnected from #{ctx.channel().remoteAddress0()}"
218
+ end
219
+
220
+ def messageReceived(ctx, msg)
221
+ log.trace "Received message: #{msg}"
222
+ case msg
223
+ when TextWebSocketFrame then @application_handler.call(ctx, msg.text())
224
+ when PongWebSocketFrame then puts 'pong!'
225
+ when CloseWebSocketFrame then ctx.channel().close()
226
+ end
227
+ end
228
+
229
+ def execute_command(str, websocket = self)
230
+ return if str.empty?
231
+ case str
232
+ when /bye/i, /quit/i
233
+ websocket.puts "#{str}\r\n"
234
+ close
235
+ when /ping/i then ping
236
+ else websocket.puts "#{str}\r\n"
237
+ end
238
+ end
239
+
240
+ def read_user_commands(websocket = self)
241
+ loop do
242
+ print @options[:prompt]
243
+ input = $stdin.gets.chomp
244
+ raise 'Poll failure from stdin' if input.nil?
245
+ break unless @channel.active?
246
+ break if execute_command(input).is_a?(AbstractChannel::CloseFuture)
247
+ $stdout.puts websocket.gets
248
+ end
249
+ end
250
+ end
251
+ # module ClientInstanceMethods
252
+ end
253
+ # module WebSocket
254
+
255
+ # The WebSocket module
256
+ module WebSocket
257
+ # The Listenable module
258
+ module Listenable
259
+ def listeners
260
+ @listeners ||= java.util.concurrent.CopyOnWriteArrayList.new
261
+ end
262
+
263
+ def add_listener(listener)
264
+ listeners << listener
265
+ end
266
+
267
+ def remove_listener(listener)
268
+ listeners.delete(listener)
269
+ end
270
+
271
+ def notify(message, *args)
272
+ return if listeners.empty?
273
+ log.trace "Notifying listeners (#{listeners}) of message: #{message}"
274
+ listeners.each do |listener|
275
+ listener.send(message.to_sym, *args) if listener.respond_to?(message.to_sym)
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ # The WebSocket module
282
+ module WebSocket
283
+ java_import Java::io.netty.channel.ChannelHandlerContext
284
+ java_import Java::io.netty.channel.SimpleChannelInboundHandler
285
+ java_import Java::io.netty.handler.codec.http.FullHttpResponse
286
+ java_import Java::io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker
287
+ java_import Java::io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory
288
+ java_import Java::io.netty.handler.codec.http.websocketx.WebSocketHandshakeException
289
+ java_import Java::io.netty.util.CharsetUtil
290
+
291
+ # The ClientChannelHandler class
292
+ class ClientChannelHandler < SimpleChannelInboundHandler
293
+ include Listenable
294
+ attr_reader :handshake_future
295
+
296
+ def initialize(handshaker)
297
+ super()
298
+ @handshaker = handshaker
299
+ end
300
+
301
+ def handlerAdded(ctx)
302
+ @handshake_future = ctx.newPromise()
303
+ end
304
+
305
+ def channelActive(ctx)
306
+ @handshaker.handshake(ctx.channel())
307
+ notify(:channelActive, ctx)
308
+ end
309
+
310
+ def channelInactive(ctx)
311
+ notify(:channelInactive, ctx)
312
+ end
313
+
314
+ # Please keep in mind that this method will be renamed to
315
+ # messageReceived(ChannelHandlerContext, I) in 5.0.
316
+ #
317
+ # java_signature 'protected void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception'
318
+ def channelRead0(ctx, msg)
319
+ messageReceived(ctx, msg)
320
+ end
321
+
322
+ def messageReceived(ctx, msg)
323
+ return finish_handshake(ctx, msg) unless @handshaker.isHandshakeComplete()
324
+ validate_message(msg)
325
+ notify(:messageReceived, ctx, msg)
326
+ end
327
+
328
+ def exceptionCaught(ctx, cause)
329
+ log.info "##{__method__} wtf1"
330
+ cause.printStackTrace()
331
+ @handshake_future.setFailure(cause) unless @handshake_future.isDone()
332
+ ctx.close()
333
+ end
334
+
335
+ protected
336
+
337
+ def finish_handshake(ctx, msg)
338
+ @handshaker.finishHandshake(ctx.channel(), msg)
339
+ @handshake_future.setSuccess()
340
+ rescue WebSocketHandshakeException => e
341
+ log.warn "Connection failure: #{e.message}"
342
+ @handshake_future.setFailure(e)
343
+ end
344
+
345
+ def validate_message(msg)
346
+ case msg
347
+ when FullHttpResponse
348
+ raise IllegalStateException, format(
349
+ UNEXPECTED_FULL_RESPONSE_ERROR,
350
+ status: msg.status(),
351
+ content: msg.content().toString(CharsetUtil::UTF_8)
352
+ )
353
+ end
354
+ end
355
+ end
356
+ # class ClientChannelHandler
357
+ end
358
+ # module WebSocket
359
+
360
+ # The WebSocket module
361
+ module WebSocket
362
+ java_import Java::io.netty.handler.codec.http.HttpClientCodec
363
+ java_import Java::io.netty.handler.codec.http.HttpObjectAggregator
364
+ java_import Java::io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler
365
+
366
+ # The ClientChannelInitializer class
367
+ class ClientChannelInitializer < Java::io.netty.channel.ChannelInitializer
368
+ FrameDecoderBufferSize = 8192 # bytes
369
+ def initialize(host, port, scheme, options = {})
370
+ super()
371
+ @host = host
372
+ @port = port
373
+ @options = options
374
+ @ssl_ctx = ssl_context if /wss/i.match?(scheme)
375
+ end
376
+
377
+ def handshaker
378
+ @handshaker ||= WebSocketClientHandshakerFactory.newHandshaker(
379
+ @options[:uri],
380
+ @options[:websocket_version],
381
+ @options[:subprotocol],
382
+ @options[:allow_extensions],
383
+ @options[:default_headers]
384
+ )
385
+ end
386
+
387
+ def default_handler
388
+ @default_handler ||= ::WebSocket::ClientChannelHandler.new(handshaker)
389
+ end
390
+
391
+ def ssl_context
392
+ context_builder = SslContextBuilder.forClient()
393
+ trust_manager = context_builder.trustManager(InsecureTrustManagerFactory::INSTANCE)
394
+ trust_manager.build()
395
+ end
396
+
397
+ def initChannel(channel)
398
+ pipeline = channel.pipeline()
399
+ pipeline.addLast(@ssl_ctx.newHandler(channel.alloc(), @host, @port)) unless @ssl_ctx.nil?
400
+ pipeline.addLast(
401
+ HttpClientCodec.new,
402
+ HttpObjectAggregator.new(FrameDecoderBufferSize),
403
+ WebSocketClientCompressionHandler::INSTANCE
404
+ )
405
+ pipeline.addLast(default_handler)
406
+ end
407
+ end
408
+ # class ClientChannelInitializer
409
+ end
410
+ # module WebSocket
411
+
412
+ # The WebSocket module
413
+ module WebSocket
414
+ # The WebSocket::Client class
415
+ class Client
416
+ include ::WebSocket::ClientInitializationMethods
417
+ include ::WebSocket::ClientInstanceMethods
418
+ include ::WebSocket::Listenable
419
+
420
+ def initialize(options = {}, &block)
421
+ init(::WebSocket.client_config.merge(options))
422
+ configure_handlers(&block)
423
+ connect
424
+ session
425
+ end
426
+
427
+ IdentiferTemplate = '#<%<class>s:0x%<id>s>'.freeze
428
+
429
+ def to_s
430
+ format(IdentiferTemplate, class: self.class.name, id: object_id.to_s(16))
431
+ end
432
+ alias inspect to_s
433
+ end
434
+ end
435
+ # module WebSocket
436
+
437
+ # The WebSocket module
438
+ module WebSocket
439
+ # rubocop: disable Metrics/AbcSize
440
+ # rubocop: disable Metrics/MethodLength
441
+ def parse_arguments(options = ::WebSocket.client_config.dup)
442
+ parser = OptionParser.new
443
+ parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options]"
444
+ parser.separator ''
445
+ parser.separator 'Options:'
446
+ parser.on_head('-u', '--uri=<uri>', 'Fully qualified connection string') do |v|
447
+ options[:uri] = java.net.URI.new(v)
448
+ end
449
+ parser.on_tail('-v', '--verbose', 'Increase verbosity') { options[:log_level] -= 1 }
450
+ parser.on_tail('-?', '--help', 'Show this message') do
451
+ puts parser
452
+ exit
453
+ end
454
+ parser.on_tail('--version', 'Show version') do
455
+ puts "#{$PROGRAM_NAME} version #{::WebSocket.version}"
456
+ exit
457
+ end
458
+ parser.parse!
459
+ options
460
+ end
461
+ # rubocop: enable Metrics/AbcSize
462
+ # rubocop: enable Metrics/MethodLength
463
+
464
+ def main(args = parse_arguments)
465
+ Logging.log_level = args[:log_level]
466
+ ::WebSocket::Client.new(args)
467
+ rescue Interrupt => e
468
+ warn "\n#{e.class}"
469
+ exit
470
+ rescue StandardError => e
471
+ ::WebSocket::Client.log.fatal "Unexpected error: #{e.class}: #{e.message}"
472
+ e.backtrace.each { |t| log.debug t } if Logging.log_level == Logger::DEBUG
473
+ exit 1
474
+ end
475
+ end
476
+ # module WebSocket
477
+
478
+ Object.new.extend(::WebSocket).main if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,50 @@
1
+ #! /usr/bin/env jruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # =begin
10
+ #
11
+ # Copyright Nels Nelson 2016-2022 but freely usable (see license)
12
+ #
13
+ # =end
14
+
15
+ require_relative 'log'
16
+ require_relative 'websocket/arguments_parser'
17
+ require_relative 'websocket/server'
18
+ require_relative 'websocket/telnet_proxy'
19
+
20
+ # The WebSocket module
21
+ module WebSocket
22
+ EchoServerMessageTemplate = "%<message>s\n".freeze
23
+ InterruptTemplate = "\r%<class>s".freeze
24
+
25
+ def echo_server(options)
26
+ WebSocket::Server.new(options: options) do |_ctx, msg|
27
+ format(EchoServerMessageTemplate, message: msg.upcase)
28
+ end.run
29
+ end
30
+
31
+ def telnet_proxy(options)
32
+ handler = WebSocket::TelnetProxy.new(options[:telnet_proxy_host], options[:telnet_proxy_port])
33
+ WebSocket::Server.new(handler: handler, options: options).run
34
+ end
35
+
36
+ def main(args = parse_arguments)
37
+ Logging.log_level = args[:log_level]
38
+ return telnet_proxy(args) unless args[:telnet_proxy_host].nil?
39
+ echo_server(args)
40
+ rescue Interrupt => e
41
+ warn format(InterruptTemplate, class: e.class)
42
+ exit
43
+ rescue StandardError => e
44
+ WebSocket::Server.log.fatal(e.message)
45
+ abort
46
+ end
47
+ end
48
+ # module WebSocket
49
+
50
+ Object.new.extend(WebSocket).main if $PROGRAM_NAME == __FILE__
data/web/client.html ADDED
@@ -0,0 +1,43 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
5
+ <head>
6
+ <meta charset="utf-8"></meta>
7
+ <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no"></meta>
8
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8"></meta>
9
+ <link id='theme' rel='stylesheet' media="screen, print" href='css/client/console.css' type='text/css'></link>
10
+ <title>WebSocket Client</title>
11
+ </head>
12
+ <body class="preload">
13
+ <div class="body">
14
+
15
+ <div class="nav">
16
+ <ul class="horizontal">
17
+ <li class="link">
18
+ <a id="nav-home" href="/" target="_blank">Home</a></li>
19
+ <li class="control">
20
+ <a id="nav-menu">Menu &#9660;</a></li>
21
+ </ul>
22
+ <div class="menu">
23
+ <ul class="vertical">
24
+ <li class="menuitem">
25
+ <a id="nav-help">Help</a></li>
26
+ <li class="menuitem">
27
+ <a id="nav-style">&#9680</a></li>
28
+ </ul>
29
+ </div>
30
+ </div> <!-- nav -->
31
+
32
+ <div class="space"></div>
33
+ <div class="display"><div class="input"><form class="input"><table width="100%"><tr><td><label for="input" id="input" class="input"></label><label for="password" id="password" class="password"></label></td><td class="input" width="100%"><input id="input" class="input" autocapitalize="off" autocorrect="off" x-webkit-speech="x-webkit-speech" data-lpignore="true"></input><input type="password" class="password" id="password" data-lpignore="true"></input></td></tr></table></form></div></div>
34
+
35
+
36
+ </div> <!-- body -->
37
+ </body>
38
+ <script type='text/javascript' src='/js/jquery.min.js'></script>
39
+ <script type='text/javascript' src='/js/client/ansispan.js'></script>
40
+ <script type='text/javascript' src='/js/client/console.js'></script>
41
+ <script type='text/javascript' src='/js/client/websocket.js'></script>
42
+ <script type='text/javascript' src='/js/client/client.js'></script>
43
+ </html>