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