websocket-server 1.0.1-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/Rakefile +56 -0
- data/exe/websocket +41 -0
- data/lib/log.rb +244 -0
- data/lib/server/mime_types.rb +38 -0
- data/lib/websocket/arguments_parser.rb +142 -0
- data/lib/websocket/channel_initializer.rb +73 -0
- data/lib/websocket/config.rb +59 -0
- data/lib/websocket/encoding.rb +21 -0
- data/lib/websocket/file_server_channel_progressive_future_listener.rb +32 -0
- data/lib/websocket/frame_handler.rb +71 -0
- data/lib/websocket/header_helpers.rb +70 -0
- data/lib/websocket/http_static_file_server_handler.rb +50 -0
- data/lib/websocket/http_static_file_server_handler_instance_methods.rb +160 -0
- data/lib/websocket/idle_handler.rb +41 -0
- data/lib/websocket/idle_state_user_event_handler.rb +47 -0
- data/lib/websocket/instance_methods.rb +127 -0
- data/lib/websocket/listenable.rb +41 -0
- data/lib/websocket/message_handler.rb +47 -0
- data/lib/websocket/response_helpers.rb +83 -0
- data/lib/websocket/server.rb +26 -0
- data/lib/websocket/shutdown_hook.rb +36 -0
- data/lib/websocket/ssl_cipher_inspector.rb +44 -0
- data/lib/websocket/ssl_context_initialization.rb +106 -0
- data/lib/websocket/telnet_proxy.rb +22 -0
- data/lib/websocket/validation_helpers.rb +51 -0
- data/lib/websocket/version.rb +16 -0
- data/lib/websocket-server.rb +13 -0
- data/lib/websocket_client.rb +478 -0
- data/lib/websocket_server.rb +50 -0
- data/web/client.html +43 -0
- data/web/css/client/console.css +167 -0
- data/web/css/client/parchment.css +112 -0
- data/web/favicon.ico +0 -0
- data/web/fonts/droidsansmono.v4.woff +0 -0
- data/web/js/client/ansispan.js +103 -0
- data/web/js/client/client.js +144 -0
- data/web/js/client/console.js +393 -0
- data/web/js/client/websocket.js +76 -0
- data/web/js/jquery.min.js +2 -0
- metadata +145 -0
@@ -0,0 +1,41 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'idle_state_user_event_handler'
|
17
|
+
|
18
|
+
# The WebSocket module
|
19
|
+
module WebSocket
|
20
|
+
java_import Java::io.netty.handler.codec.http.websocketx.TextWebSocketFrame
|
21
|
+
|
22
|
+
# The IdleHandler class handles idle channels detected by the
|
23
|
+
# server pipeline.
|
24
|
+
class IdleHandler < WebSocket::IdleStateUserEventHandler
|
25
|
+
def initialize
|
26
|
+
super()
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_idle_channel(ctx, _evt)
|
30
|
+
klass = self.class.name
|
31
|
+
log.debug "#{klass} < IdleStateUserEventHandler ##{__method__}"
|
32
|
+
message = TextWebSocketFrame.new("\nDisconnecting idle session\n")
|
33
|
+
# TODO: Test
|
34
|
+
ctx.writeAndFlush(message).sync
|
35
|
+
ctx.close
|
36
|
+
# ctx.channel.writeAndFlush(message).sync
|
37
|
+
# ctx.channel.disconnect().awaitUninterruptibly()
|
38
|
+
# ctx.channel.close().awaitUninterruptibly()
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
# The WebSocket module
|
17
|
+
module WebSocket
|
18
|
+
java_import Java::io.netty.channel.ChannelDuplexHandler
|
19
|
+
java_import Java::io.netty.handler.codec.http.websocketx.PingWebSocketFrame
|
20
|
+
java_import Java::io.netty.handler.timeout.IdleState
|
21
|
+
|
22
|
+
# The PingMessage class is just a convenient alias
|
23
|
+
# for the PingWebSocketFrame class.
|
24
|
+
PingMessage = Class.new(PingWebSocketFrame)
|
25
|
+
|
26
|
+
# The IdleStateUserEventHandler class specifies methods implementing
|
27
|
+
# what to do when the pipeline detects an idle channel.
|
28
|
+
class IdleStateUserEventHandler < ChannelDuplexHandler
|
29
|
+
def initialize
|
30
|
+
super()
|
31
|
+
end
|
32
|
+
|
33
|
+
# java_signature 'public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception'
|
34
|
+
def userEventTriggered(ctx, evt)
|
35
|
+
return unless evt.respond_to?(:state)
|
36
|
+
case evt.state
|
37
|
+
when IdleState::READER_IDLE
|
38
|
+
return handle_idle_channel(ctx, evt) if respond_to?(:handle_idle_channel)
|
39
|
+
message = TextWebSocketFrame.new("\nDisconnecting idle session\n")
|
40
|
+
ctx.writeAndFlush(message).sync
|
41
|
+
ctx.close
|
42
|
+
when IdleState::WRITER_IDLE
|
43
|
+
ctx.writeAndFlush(PingMessage.new)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,127 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'channel_initializer'
|
17
|
+
require_relative 'shutdown_hook'
|
18
|
+
|
19
|
+
# The WebSocket module
|
20
|
+
module WebSocket
|
21
|
+
java_import Java::io.netty.bootstrap.ServerBootstrap
|
22
|
+
java_import Java::io.netty.channel.group.DefaultChannelGroup
|
23
|
+
java_import Java::io.netty.channel.nio.NioEventLoopGroup
|
24
|
+
java_import Java::io.netty.handler.logging.LoggingHandler
|
25
|
+
java_import Java::io.netty.handler.logging.LogLevel
|
26
|
+
java_import Java::io.netty.util.concurrent.GlobalEventExecutor
|
27
|
+
|
28
|
+
# The InstanceMethods module
|
29
|
+
module InstanceMethods
|
30
|
+
def configure_handlers(&block)
|
31
|
+
add_listener(self)
|
32
|
+
channel_initializer << block if block_given?
|
33
|
+
end
|
34
|
+
|
35
|
+
def bootstrap
|
36
|
+
@bootstrap = ServerBootstrap.new
|
37
|
+
@bootstrap.group(boss_group, worker_group)
|
38
|
+
@bootstrap.channel(Server::CHANNEL_TYPE)
|
39
|
+
@bootstrap.handler(logging_handler) if @options[:log_requests]
|
40
|
+
@bootstrap.childHandler(channel_initializer)
|
41
|
+
end
|
42
|
+
|
43
|
+
def channel_initializer
|
44
|
+
@channel_initializer ||= ::WebSocket::ChannelInitializer.new(channel_group, @options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def boss_group
|
48
|
+
@boss_group ||= NioEventLoopGroup.new(2)
|
49
|
+
end
|
50
|
+
|
51
|
+
def worker_group
|
52
|
+
@worker_group ||= NioEventLoopGroup.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def channel_group
|
56
|
+
@channel_group ||= DefaultChannelGroup.new('server_channels', GlobalEventExecutor::INSTANCE)
|
57
|
+
end
|
58
|
+
|
59
|
+
def logging_handler
|
60
|
+
LoggingHandler.new(LogLevel::INFO)
|
61
|
+
end
|
62
|
+
|
63
|
+
# rubocop: disable Metrics/AbcSize
|
64
|
+
# rubocop: disable Metrics/MethodLength
|
65
|
+
def run(params = {})
|
66
|
+
options.merge!(params)
|
67
|
+
channel = bootstrap.bind(port).sync().channel()
|
68
|
+
channel_group.add(channel)
|
69
|
+
::WebSocket::ShutdownHook.new(self)
|
70
|
+
log.info "Listening on #{channel.local_address}"
|
71
|
+
channel.closeFuture().sync()
|
72
|
+
rescue java.net.BindException => e
|
73
|
+
raise "Bind error: #{e.message}: #{options[:host]}:#{port}"
|
74
|
+
rescue java.net.SocketException => e
|
75
|
+
raise "Socket error: #{e.message}: #{options[:host]}:#{port}"
|
76
|
+
ensure
|
77
|
+
stop
|
78
|
+
end
|
79
|
+
# rubocop: enable Metrics/AbcSize
|
80
|
+
# rubocop: enable Metrics/MethodLength
|
81
|
+
|
82
|
+
IntegerPattern = /^\d+$/.freeze
|
83
|
+
|
84
|
+
def valid_port?(val)
|
85
|
+
IntegerPattern.match?(val.to_s) && val.positive? && val < 65_536
|
86
|
+
end
|
87
|
+
|
88
|
+
def given_port
|
89
|
+
value = (options[:ssl] ? options[:ssl_port] : options[:port]).to_i
|
90
|
+
raise 'Given port parameter is invalid' unless valid_port?(value)
|
91
|
+
value
|
92
|
+
end
|
93
|
+
|
94
|
+
def port
|
95
|
+
@port ||= given_port
|
96
|
+
end
|
97
|
+
|
98
|
+
def shutdown
|
99
|
+
channel_group.disconnect().awaitUninterruptibly()
|
100
|
+
channel_group.close().awaitUninterruptibly()
|
101
|
+
end
|
102
|
+
|
103
|
+
def stop
|
104
|
+
boss_group&.shutdownGracefully()
|
105
|
+
worker_group&.shutdownGracefully()
|
106
|
+
end
|
107
|
+
|
108
|
+
def <<(handler)
|
109
|
+
channel_initializer.handlers << handler
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_listener(listener)
|
113
|
+
channel_initializer.add_listener(listener)
|
114
|
+
end
|
115
|
+
|
116
|
+
# def all_exist?(*files)
|
117
|
+
# files.all? { |f| File.exist?(f) }
|
118
|
+
# end
|
119
|
+
|
120
|
+
# def init_ssl_context(certificate, private_key)
|
121
|
+
# return certificate_key_pair(certificate, private_key) if all_exist?(certificate, private_key)
|
122
|
+
# options[:use_jdk_ssl_provider] ? jdk_ssl_provider : self_signed_certificate
|
123
|
+
# end
|
124
|
+
end
|
125
|
+
# module ServerInstanceMethods
|
126
|
+
end
|
127
|
+
# module WebSocket
|
@@ -0,0 +1,41 @@
|
|
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 'java'
|
14
|
+
|
15
|
+
# The WebSocket module
|
16
|
+
module WebSocket
|
17
|
+
# The Listenable module
|
18
|
+
module Listenable
|
19
|
+
def listeners
|
20
|
+
@listeners ||= java.util.concurrent.CopyOnWriteArrayList.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_listener(listener)
|
24
|
+
listeners << listener
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_listener(listener)
|
28
|
+
listeners.delete(listener)
|
29
|
+
end
|
30
|
+
|
31
|
+
def notify(message, *args)
|
32
|
+
return if listeners.empty?
|
33
|
+
log.trace "Notifying listeners (#{listeners}) of message: #{message}"
|
34
|
+
listeners.each do |listener|
|
35
|
+
listener.send(message.to_sym, *args) if listener.respond_to?(message.to_sym)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# module Listenable
|
40
|
+
end
|
41
|
+
# module WebSocket
|
@@ -0,0 +1,47 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'frame_handler'
|
17
|
+
|
18
|
+
# The WebSocket module
|
19
|
+
module WebSocket
|
20
|
+
java_import Java::io.netty.channel.ChannelHandler
|
21
|
+
java_import Java::io.netty.channel.ChannelFutureListener
|
22
|
+
java_import Java::io.netty.handler.codec.http.websocketx.TextWebSocketFrame
|
23
|
+
|
24
|
+
# The MessageHandler class provides a method for specifying code
|
25
|
+
# to handle incoming messages and respond with the results from
|
26
|
+
# the given code.
|
27
|
+
class MessageHandler < WebSocket::FrameHandler
|
28
|
+
include ChannelHandler
|
29
|
+
include ChannelFutureListener
|
30
|
+
|
31
|
+
def initialize(&handler)
|
32
|
+
super()
|
33
|
+
@handler = handler
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle_message(ctx, message)
|
37
|
+
request = message&.to_s&.strip
|
38
|
+
return if request.nil?
|
39
|
+
response = @handler.call(ctx, request)
|
40
|
+
return if response.nil?
|
41
|
+
log.debug "#{self.class}#handle_message response: #{response.chomp}"
|
42
|
+
ctx.channel.writeAndFlush(TextWebSocketFrame.new(response))
|
43
|
+
response
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# module WebSocket
|
@@ -0,0 +1,83 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'encoding'
|
17
|
+
|
18
|
+
# The WebSocket module
|
19
|
+
module WebSocket
|
20
|
+
java_import Java::io.netty.channel.ChannelFutureListener
|
21
|
+
java_import Java::io.netty.buffer.Unpooled
|
22
|
+
java_import Java::io.netty.handler.codec.http.DefaultFullHttpResponse
|
23
|
+
java_import Java::io.netty.handler.codec.http.HttpHeaderNames
|
24
|
+
java_import Java::io.netty.handler.codec.http.HttpVersion
|
25
|
+
java_import Java::io.netty.util.CharsetUtil
|
26
|
+
|
27
|
+
# The ResponseHelpers helpers
|
28
|
+
module ResponseHelpers
|
29
|
+
NonSpacesBeforeEOLPattern = %r{([^\s]+)$}.freeze
|
30
|
+
|
31
|
+
def index_listing(path)
|
32
|
+
results = `ls -la #{path}`.strip.split("\n")
|
33
|
+
results.shift
|
34
|
+
index = results.collect do |s|
|
35
|
+
s.gsub(NonSpacesBeforeEOLPattern) do |resource_name|
|
36
|
+
%(<a href="#{resource_name}">#{resource_name}</a>)
|
37
|
+
end
|
38
|
+
end.join '<br />'
|
39
|
+
"<html><pre>#{index}</pre></html>"
|
40
|
+
end
|
41
|
+
|
42
|
+
def send_listing(ctx, path)
|
43
|
+
response = DefaultFullHttpResponse.new(HttpVersion::HTTP_1_1, HttpResponseStatus::OK)
|
44
|
+
response.headers().set(HttpHeaderNames::CONTENT_TYPE, WebSocket::HtmlContentType)
|
45
|
+
|
46
|
+
html = index_listing(path)
|
47
|
+
|
48
|
+
buffer = Unpooled.copiedBuffer(html, WebSocket::Encoding)
|
49
|
+
response.content().writeBytes(buffer)
|
50
|
+
buffer.release()
|
51
|
+
|
52
|
+
# Close the connection as soon as the error message is sent.
|
53
|
+
ctx.writeAndFlush(response).addListener(ChannelFutureListener::CLOSE)
|
54
|
+
end
|
55
|
+
|
56
|
+
def send_redirect(ctx, redirect_to_uri)
|
57
|
+
response = DefaultFullHttpResponse.new(HttpVersion::HTTP_1_1, HttpResponseStatus::FOUND)
|
58
|
+
response.headers().set(HttpHeaderNames::LOCATION, redirect_to_uri)
|
59
|
+
|
60
|
+
# Close the connection as soon as the error message is sent.
|
61
|
+
ctx.writeAndFlush(response).addListener(ChannelFutureListener::CLOSE)
|
62
|
+
end
|
63
|
+
|
64
|
+
def send_error(ctx, status)
|
65
|
+
message = Unpooled.copiedBuffer("#{status}\r\n", CharsetUtil::UTF_8)
|
66
|
+
response = DefaultFullHttpResponse.new(HttpVersion::HTTP_1_1, status, message)
|
67
|
+
response.headers().set(HttpHeaderNames::CONTENT_TYPE, WebSocket::HtmlContentType)
|
68
|
+
|
69
|
+
# Close the connection as soon as the error message is sent.
|
70
|
+
ctx.writeAndFlush(response).addListener(ChannelFutureListener::CLOSE)
|
71
|
+
end
|
72
|
+
|
73
|
+
def send_not_modified(ctx, date)
|
74
|
+
response = DefaultFullHttpResponse.new(HttpVersion::HTTP_1_1, HttpResponseStatus::NOT_MODIFIED)
|
75
|
+
date_header(response, date)
|
76
|
+
|
77
|
+
# Close the connection as soon as the error message is sent.
|
78
|
+
ctx.writeAndFlush(response).addListener(ChannelFutureListener::CLOSE)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
# module ResponseHelpers
|
82
|
+
end
|
83
|
+
# module WebSocket
|
@@ -0,0 +1,26 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
require 'tcp-server'
|
16
|
+
|
17
|
+
require_relative 'instance_methods'
|
18
|
+
|
19
|
+
# The WebSocket module
|
20
|
+
module WebSocket
|
21
|
+
# The Server class sets up the netty server
|
22
|
+
class Server < ::Server::Server
|
23
|
+
include ::WebSocket::InstanceMethods
|
24
|
+
end
|
25
|
+
end
|
26
|
+
# module WebSocket
|
@@ -0,0 +1,36 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
# The WebSocket module
|
17
|
+
module WebSocket
|
18
|
+
# The ShutdownHook class specifies a routine to be invoked when the
|
19
|
+
# java runtime is shutdown.
|
20
|
+
class ShutdownHook < java.lang.Thread
|
21
|
+
attr_reader :server
|
22
|
+
|
23
|
+
def initialize(server)
|
24
|
+
super()
|
25
|
+
@server = server
|
26
|
+
java.lang.Runtime.runtime.add_shutdown_hook(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
$stdout.write "\r\e[0K"
|
31
|
+
$stdout.flush
|
32
|
+
::WebSocket::Server.log.info 'Shutting down'
|
33
|
+
server.shutdown if server.respond_to?(:shutdown)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
# The WebSocket module
|
17
|
+
module WebSocket
|
18
|
+
java_import Java::io.netty.channel.SimpleChannelInboundHandler
|
19
|
+
java_import Java::io.netty.handler.ssl.SslHandler
|
20
|
+
|
21
|
+
# The SslCipherInspector class enables debugging the details around
|
22
|
+
# cipher configuration and pipeline handling of SSL handshakes.
|
23
|
+
class SslCipherInspector < SimpleChannelInboundHandler
|
24
|
+
def initialize
|
25
|
+
super()
|
26
|
+
end
|
27
|
+
|
28
|
+
def channelRead(ctx, msg)
|
29
|
+
cipher_suite = cipher_suite ctx
|
30
|
+
log.info "Server communications are secured by #{cipher_suite}"
|
31
|
+
ctx.fireChannelRead(msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def cipher_suite(ctx)
|
37
|
+
# Confirm the SSL context has been loaded into the channel pipeline
|
38
|
+
ssl_handler = ctx.pipeline.get(SslHandler.java_class)
|
39
|
+
ssl_engine = ssl_handler.engine()
|
40
|
+
ssl_session = ssl_engine.getSession()
|
41
|
+
ssl_session.getCipherSuite()
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,106 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
# The WebSocket module
|
17
|
+
module WebSocket
|
18
|
+
# This module implements methods responsible for the
|
19
|
+
# initialization of an SSL context
|
20
|
+
module SslContextInitializationMethods
|
21
|
+
java_import Java::io.netty.handler.ssl.SslContextBuilder
|
22
|
+
java_import Java::io.netty.handler.ssl.SslProvider
|
23
|
+
java_import Java::io.netty.handler.ssl.util.SelfSignedCertificate
|
24
|
+
java_import java.io.FileInputStream
|
25
|
+
java_import java.io.BufferedInputStream
|
26
|
+
java_import java.io.ByteArrayOutputStream
|
27
|
+
java_import java.security.KeyFactory
|
28
|
+
java_import java.security.cert.CertificateFactory
|
29
|
+
java_import java.security.spec.PKCS8EncodedKeySpec
|
30
|
+
|
31
|
+
DefaultCertificateType = 'X.509'.freeze
|
32
|
+
|
33
|
+
def init_ssl(channel_initializer)
|
34
|
+
return unless options[:ssl] && channel_initializer.respond_to?(:ssl_context)
|
35
|
+
context = ssl_context
|
36
|
+
channel_initializer.ssl_context = context unless context.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def ssl_context
|
40
|
+
@ssl_context ||= init_ssl_context(options[:ssl_certificate_file_path], options[:ssl_private_key_file_path])
|
41
|
+
end
|
42
|
+
|
43
|
+
def certificate_key_pair(certificate, private_key)
|
44
|
+
log.info "Securing socket layer using #{certificate} and #{private_key}"
|
45
|
+
certificate = read_certificate(certificate)
|
46
|
+
private_key = read_private_key(private_key)
|
47
|
+
SslContextBuilder.forServer(private_key, certificate).build()
|
48
|
+
end
|
49
|
+
|
50
|
+
def jdk_ssl_provider
|
51
|
+
log.info 'Securing socket layer using JDK self-signed certificate'
|
52
|
+
ssc = SelfSignedCertificate.new
|
53
|
+
context_builder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
|
54
|
+
context_builder.sslProvider(SslProvider::JDK).build()
|
55
|
+
end
|
56
|
+
|
57
|
+
def self_signed_certificate
|
58
|
+
log.info 'Securing socket layer using self-signed certificate'
|
59
|
+
ssc = SelfSignedCertificate.new
|
60
|
+
SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build()
|
61
|
+
end
|
62
|
+
|
63
|
+
def read_certificate(file_path)
|
64
|
+
file_input_stream = FileInputStream.new(file_path)
|
65
|
+
buffered_input_stream = BufferedInputStream.new(file_input_stream)
|
66
|
+
CertificateFactory.getInstance(DefaultCertificateType).generateCertificate(buffered_input_stream)
|
67
|
+
end
|
68
|
+
|
69
|
+
def read_private_key(file_path)
|
70
|
+
file_input_stream = FileInputStream.new(file_path)
|
71
|
+
buffered_input_stream = BufferedInputStream.new(file_input_stream)
|
72
|
+
decode_private_key(stream_data(buffered_input_stream))
|
73
|
+
ensure
|
74
|
+
close_streams(buffered_input_stream, file_input_stream)
|
75
|
+
end
|
76
|
+
|
77
|
+
def stream_data(input_stream, output_stream = ByteArrayOutputStream.new)
|
78
|
+
buffer = Java::byte[1024 * 4].new
|
79
|
+
loop do
|
80
|
+
n = input_stream.read(buffer)
|
81
|
+
break if n == -1
|
82
|
+
output_stream.write(buffer, 0, n)
|
83
|
+
end
|
84
|
+
output_stream.toByteArray()
|
85
|
+
ensure
|
86
|
+
close_streams(output_stream)
|
87
|
+
end
|
88
|
+
|
89
|
+
def close_streams(*streams)
|
90
|
+
streams.each { |stream| safely_close_stream(stream) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def safely_close_stream(stream)
|
94
|
+
stream.close()
|
95
|
+
rescue StandardError => e
|
96
|
+
log.warn "Failed to close stream: #{e.message}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def decode_private_key(encoded_private_key)
|
100
|
+
private_key_spec = PKCS8EncodedKeySpec.new(encoded_private_key)
|
101
|
+
KeyFactory.getInstance('RSA').generatePrivate(private_key_spec)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
# module SslContextInitializationMethods
|
105
|
+
end
|
106
|
+
# module WebSocket
|
@@ -0,0 +1,22 @@
|
|
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
|
+
# The WebSocket module
|
14
|
+
module WebSocket
|
15
|
+
# The TelnetProxy class adds a handler to the server channel
|
16
|
+
# pipeline which will proxy incoming requests to a telnet server.
|
17
|
+
class TelnetProxy
|
18
|
+
def initialize(host, port)
|
19
|
+
pipeline << TelnetProxyFrontendHandler.new(host, port)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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 'java'
|
14
|
+
require 'netty'
|
15
|
+
|
16
|
+
require_relative 'encoding'
|
17
|
+
|
18
|
+
# The WebSocket module
|
19
|
+
module WebSocket
|
20
|
+
# The ValidationHelpers module
|
21
|
+
module ValidationHelpers
|
22
|
+
FileSeparatorDotPattern = %r{#{File::SEPARATOR}\.}.freeze
|
23
|
+
DotFileSeparatorPattern = %r{\.#{File::SEPARATOR}}.freeze
|
24
|
+
|
25
|
+
# Simplistic dumb security check.
|
26
|
+
# Something more serious is required in a production environment.
|
27
|
+
def insecure_uri?(uri)
|
28
|
+
FileSeparatorDotPattern.match?(uri) ||
|
29
|
+
DotFileSeparatorPattern.match?(uri) ||
|
30
|
+
uri.start_with?('.') ||
|
31
|
+
uri.end_with?('.') ||
|
32
|
+
options[:insecure_uri_pattern].match?(uri)
|
33
|
+
end
|
34
|
+
|
35
|
+
QueryStringPattern = %r{\?.*}.freeze
|
36
|
+
ForwardSlashPattern = %r{/}.freeze
|
37
|
+
|
38
|
+
def sanitize_uri(uri)
|
39
|
+
# Decode the path.
|
40
|
+
uri = CGI.unescape(uri.gsub(QueryStringPattern, ''), WebSocket::Encoding.name)
|
41
|
+
return nil if uri.empty? || !uri.start_with?('/')
|
42
|
+
# Convert file separators.
|
43
|
+
uri = uri.gsub(ForwardSlashPattern, File::SEPARATOR)
|
44
|
+
return nil if insecure_uri?(uri)
|
45
|
+
# Convert to absolute path.
|
46
|
+
File.join(options[:web_root], uri)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
# module ValidationHelpers
|
50
|
+
end
|
51
|
+
# module WebSocket
|
@@ -0,0 +1,16 @@
|
|
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
|
+
# The WebSocket module
|
14
|
+
module WebSocket
|
15
|
+
VERSION = '1.0.1'.freeze
|
16
|
+
end
|