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,142 @@
|
|
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 'optparse'
|
14
|
+
|
15
|
+
require_relative 'config'
|
16
|
+
|
17
|
+
# The WebSocket module
|
18
|
+
module WebSocket
|
19
|
+
# The ArgumentsParser class
|
20
|
+
class ArgumentsParser
|
21
|
+
Flags = %i[
|
22
|
+
banner port telnet_proxy_host telnet_proxy_port
|
23
|
+
ssl ssl_certificate ssl_private_key use_jdk_ssl_provider
|
24
|
+
inspect_ssl idle_reading idle_writing log_requests web_root
|
25
|
+
verbose help version
|
26
|
+
].freeze
|
27
|
+
attr_reader :parser, :options
|
28
|
+
|
29
|
+
def initialize(option_parser = OptionParser.new)
|
30
|
+
@parser = option_parser
|
31
|
+
@options = ::WebSocket.server_config.dup
|
32
|
+
Flags.each { |method_name| method(method_name).call }
|
33
|
+
end
|
34
|
+
|
35
|
+
def banner
|
36
|
+
@parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [port] [options]"
|
37
|
+
@parser.separator ''
|
38
|
+
@parser.separator 'Options:'
|
39
|
+
end
|
40
|
+
|
41
|
+
def port
|
42
|
+
@parser.on_head('-p', '--port=port', 'Listen on this port for incoming connections') do |v|
|
43
|
+
@options[:port] = v.to_i
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def telnet_proxy_host
|
48
|
+
@parser.on('--telnet-proxy-host=host', 'Remote telnet proxy host') do |v|
|
49
|
+
@options[:telnet_proxy_host] = v
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def telnet_proxy_port
|
54
|
+
@parser.on('--telnet-proxy-port=port', 'Remote telnet proxy port') do |v|
|
55
|
+
@options[:telnet_proxy_port] = v.to_i
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def ssl
|
60
|
+
@parser.on('-s', '--ssl', 'Use TLS/SSL') do |v|
|
61
|
+
@options[:ssl] = v
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def ssl_certificate
|
66
|
+
@parser.on('-c', '--ssl-certificate=file', 'Path to ssl certificate file') do |v|
|
67
|
+
@options[:ssl_certificate_file_path] = v
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def ssl_private_key
|
72
|
+
@parser.on('-k', '--ssl-private-key=file', 'Path to private key file') do |v|
|
73
|
+
@options[:ssl_private_key_file_path] = v
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def use_jdk_ssl_provider
|
78
|
+
@parser.on('--use-jdk-ssl-provider', 'Use the JDK SSL provider') do
|
79
|
+
@options[:use_jdk_ssl_provider] = true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def inspect_ssl
|
84
|
+
@parser.on('--inspect-ssl', 'Verbosely log SSL messages for encryption confirmation') do
|
85
|
+
@options[:inspect_ssl] = true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def idle_reading
|
90
|
+
@parser.on('--idle-reading=seconds', 'Amount of time channel can idle without incoming data') do |v|
|
91
|
+
@options[:idle_reading] = v.to_i
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def idle_writing
|
96
|
+
@parser.on('--idle-writing=seconds', 'Amount of time channel can idle without outgoing data') do |v|
|
97
|
+
@options[:idle_writing] = v.to_i
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def log_requests
|
102
|
+
@parser.on('-r', '--log-requests', 'Include individual request info in log output') do |v|
|
103
|
+
@options[:log_requests] = v
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def web_root
|
108
|
+
@parser.on('-w', '--web-root=path', 'Set the web root to a specific path') do |v|
|
109
|
+
@options[:web_root] = v
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def verbose
|
114
|
+
@parser.on_tail('-v', '--verbose', 'Increase verbosity') do
|
115
|
+
@options[:log_level] -= 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def help
|
120
|
+
@parser.on_tail('-?', '--help', 'Show this message') do
|
121
|
+
puts @parser
|
122
|
+
exit
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def version
|
127
|
+
@parser.on_tail('--version', 'Show version') do
|
128
|
+
puts "#{File.basename($PROGRAM_NAME)} version #{WebSocket.version}"
|
129
|
+
exit
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
# class ArgumentsParser
|
134
|
+
|
135
|
+
def parse_arguments(arguments_parser = WebSocket::ArgumentsParser.new)
|
136
|
+
arguments_parser.parser.parse!(ARGV)
|
137
|
+
arguments_parser.options
|
138
|
+
rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => e
|
139
|
+
abort e.message
|
140
|
+
end
|
141
|
+
end
|
142
|
+
# module WebSocket
|
@@ -0,0 +1,73 @@
|
|
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 'http_static_file_server_handler'
|
17
|
+
require_relative 'idle_handler'
|
18
|
+
require_relative 'message_handler'
|
19
|
+
|
20
|
+
# The WebSocket module
|
21
|
+
module WebSocket
|
22
|
+
java_import Java::io.netty.handler.codec.http.HttpObjectAggregator
|
23
|
+
java_import Java::io.netty.handler.codec.http.HttpServerCodec
|
24
|
+
java_import Java::io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler
|
25
|
+
java_import Java::io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler
|
26
|
+
java_import Java::io.netty.handler.stream.ChunkedWriteHandler
|
27
|
+
java_import Java::io.netty.handler.timeout.IdleStateHandler
|
28
|
+
|
29
|
+
# The ChannelInitializer class programmatically specifies the
|
30
|
+
# configuration for the channel pipeline that will handle requests of
|
31
|
+
# the server.
|
32
|
+
class ChannelInitializer < ::Server::ChannelInitializer
|
33
|
+
HttpObjectAggregatorBufferBytesSize = 65_536
|
34
|
+
|
35
|
+
def initialize(channel_group, options = {})
|
36
|
+
super(channel_group, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# rubocop: disable Metrics/AbcSize
|
40
|
+
# rubocop: disable Metrics/MethodLength
|
41
|
+
def initChannel(channel)
|
42
|
+
pipeline = channel.pipeline
|
43
|
+
pipeline.addLast(ssl_handler(channel)) if @options[:ssl]
|
44
|
+
pipeline.addLast(HttpServerCodec.new)
|
45
|
+
pipeline.addLast(HttpObjectAggregator.new(HttpObjectAggregatorBufferBytesSize))
|
46
|
+
pipeline.addLast(ChunkedWriteHandler.new)
|
47
|
+
pipeline.addLast(IdleStateHandler.new(@options[:idle_reading], @options[:idle_writing], 0))
|
48
|
+
pipeline.addLast(WebSocket::IdleHandler.new)
|
49
|
+
pipeline.addLast(WebSocketServerCompressionHandler.new)
|
50
|
+
pipeline.addLast(WebSocketServerProtocolHandler.new(@options[:web_socket_path], nil, true))
|
51
|
+
pipeline.addLast(SslCipherInspector.new) if !ssl_context.nil? && @options[:inspect_ssl]
|
52
|
+
pipeline.addLast(HttpStaticFileServerHandler.new(@options))
|
53
|
+
add_user_handlers(pipeline)
|
54
|
+
pipeline.addLast(default_handler)
|
55
|
+
end
|
56
|
+
# rubocop: enable Metrics/AbcSize
|
57
|
+
# rubocop: enable Metrics/MethodLength
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def add_user_handlers(pipeline)
|
62
|
+
@user_handlers.each do |handler|
|
63
|
+
case handler
|
64
|
+
when Class then pipeline.addLast(handler.new)
|
65
|
+
when Proc then pipeline.addLast(::WebSocket::MessageHandler.new(&handler))
|
66
|
+
else pipeline.addLast(handler)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
# class ServerInitializer
|
72
|
+
end
|
73
|
+
# module WebSocket
|
@@ -0,0 +1,59 @@
|
|
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 'logger'
|
14
|
+
|
15
|
+
# The WebSocket module
|
16
|
+
module WebSocket
|
17
|
+
# rubocop: disable Metrics/MethodLength
|
18
|
+
def server_config
|
19
|
+
@server_config ||= begin
|
20
|
+
server_submodule_dir_path = File.expand_path(__dir__)
|
21
|
+
lib_dir_path = File.expand_path(File.dirname(server_submodule_dir_path))
|
22
|
+
project_dir_path = File.expand_path(File.dirname(lib_dir_path))
|
23
|
+
{
|
24
|
+
log_level: Logger::INFO,
|
25
|
+
host: '0.0.0.0',
|
26
|
+
port: 4000,
|
27
|
+
ssl_port: 443,
|
28
|
+
telnet_proxy_host: nil,
|
29
|
+
telnet_proxy_port: 21,
|
30
|
+
ssl: false,
|
31
|
+
ssl_certificate_file_path: File.join(project_dir_path, 'fullchain.pem'),
|
32
|
+
# openssl pkcs8 -topk8 -nocrypt -in websocket.key -out websocket_pcks8
|
33
|
+
#
|
34
|
+
# The privkey.key file here should be generated using the following
|
35
|
+
# openssl command:
|
36
|
+
#
|
37
|
+
# openssl pkcs8 -topk8 -inform PEM -outform DER -in privkey.pem -nocrypt > privkey.key
|
38
|
+
#
|
39
|
+
ssl_private_key_file_path: File.join(project_dir_path, 'privkey.key'),
|
40
|
+
use_jdk_ssl_provider: false,
|
41
|
+
inspect_ssl: false,
|
42
|
+
idle_reading: 5 * 60, # seconds
|
43
|
+
idle_writing: 30, # seconds
|
44
|
+
index_page: 'index.html',
|
45
|
+
web_root: File.join(project_dir_path, 'web'),
|
46
|
+
web_socket_path: '/websocket',
|
47
|
+
ping_message: "ping\n",
|
48
|
+
http_date_format: '%a, %d %b %Y %H:%M:%S %Z', # EEE, dd MMM yyyy HH:mm:ss zzz
|
49
|
+
http_date_gmt_timezone: 'GMT',
|
50
|
+
http_cache_seconds: 60,
|
51
|
+
insecure_uri_pattern: /.*[<>&"].*/,
|
52
|
+
allowed_file_name: /[A-Za-z0-9][-_A-Za-z0-9\\.]*/,
|
53
|
+
log_requests: false
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
module_function :server_config
|
58
|
+
# rubocop: enable Metrics/MethodLength
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
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.util.CharsetUtil
|
19
|
+
Encoding = CharsetUtil::UTF_8
|
20
|
+
HtmlContentType = "text/html; charset=#{Encoding.name}".freeze
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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 FileServerChannelProgressiveFutureListener class implements
|
16
|
+
# handler methods invoked within the server pipeline during a file
|
17
|
+
# transfer over a channel.
|
18
|
+
class FileServerChannelProgressiveFutureListener
|
19
|
+
def operationProgressed(future, progress, total)
|
20
|
+
if total.positive?
|
21
|
+
log.info "#{future.channel} Transfer progress: #{progress} / #{total}"
|
22
|
+
else # total unknown
|
23
|
+
log.info "#{future.channel} Transfer progress: #{progress}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def operationComplete(future)
|
28
|
+
log.debug "#{future.channel} Transfer complete"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
# module WebSocket
|
@@ -0,0 +1,71 @@
|
|
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.ChannelFutureListener
|
19
|
+
java_import Java::io.netty.channel.ChannelHandler
|
20
|
+
java_import Java::io.netty.handler.codec.http.FullHttpRequest
|
21
|
+
java_import Java::io.netty.handler.codec.http.websocketx.WebSocketFrame
|
22
|
+
java_import Java::io.netty.handler.codec.http.websocketx.TextWebSocketFrame
|
23
|
+
|
24
|
+
# The FrameHandler class implements a handler for incoming
|
25
|
+
# WebSocket request messages. This handler invokes a #handle_message
|
26
|
+
# method which should be implemented by a user provided subclass. The
|
27
|
+
# MessageHandler class is an example of one such subclass.
|
28
|
+
class FrameHandler < SimpleChannelInboundHandler
|
29
|
+
include ChannelHandler
|
30
|
+
include ChannelFutureListener
|
31
|
+
UnsupportedFrameTypeErrorTemplate =
|
32
|
+
'%<handler>s encountered unsupported frame type: %<frame>s'.freeze
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
super(WebSocketFrame.java_class)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Please keep in mind that this method will be renamed to
|
39
|
+
# messageReceived(ChannelHandlerContext, I) in 5.0.
|
40
|
+
#
|
41
|
+
# java_signature 'protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception'
|
42
|
+
def channelRead0(ctx, frame)
|
43
|
+
messageReceived(ctx, frame)
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_message(ctx, msg)
|
47
|
+
# Send the uppercase string back.
|
48
|
+
ctx.channel.writeAndFlush(TextWebSocketFrame.new(msg.to_s.strip.upcase))
|
49
|
+
end
|
50
|
+
|
51
|
+
def unsupported_frame(frame, handler)
|
52
|
+
raise java.lang.UnsupportedOperationException, format(
|
53
|
+
UnsupportedFrameTypeErrorTemplate, handler: handler.class, frame: frame.class
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def messageReceived(ctx, frame)
|
58
|
+
# ping and pong frames already handled
|
59
|
+
case frame
|
60
|
+
when TextWebSocketFrame
|
61
|
+
unsupported_frame(frame, self) unless frame.respond_to?(:text)
|
62
|
+
handle_message(ctx, frame.text)
|
63
|
+
when FullHttpRequest
|
64
|
+
# Let another handler handle it.
|
65
|
+
else unsupported_frame(frame, self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
# class WebSocketFrameHandler
|
70
|
+
end
|
71
|
+
# module WebSocket
|
@@ -0,0 +1,70 @@
|
|
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 '../server/mime_types'
|
17
|
+
|
18
|
+
# The WebSocket module
|
19
|
+
module WebSocket
|
20
|
+
java_import Java::io.netty.handler.codec.http.HttpHeaderValues
|
21
|
+
|
22
|
+
# The HeaderHelpers module
|
23
|
+
module HeaderHelpers
|
24
|
+
def keep_alive_header(response)
|
25
|
+
response.headers().set(HttpHeaderNames::CONNECTION, HttpHeaderValues::KEEP_ALIVE)
|
26
|
+
end
|
27
|
+
|
28
|
+
def date_header(response, date)
|
29
|
+
response.headers().set(HttpHeaderNames::DATE, date)
|
30
|
+
end
|
31
|
+
|
32
|
+
def expires_header(response, expires)
|
33
|
+
response.headers().set(HttpHeaderNames::EXPIRES, expires)
|
34
|
+
end
|
35
|
+
|
36
|
+
def cache_control_header(response, cache_control)
|
37
|
+
response.headers().set(HttpHeaderNames::CACHE_CONTROL, cache_control)
|
38
|
+
end
|
39
|
+
|
40
|
+
def last_modified_header(response, last_modified)
|
41
|
+
response.headers().set(HttpHeaderNames::LAST_MODIFIED, last_modified)
|
42
|
+
end
|
43
|
+
|
44
|
+
PrivateMaxAgeTempalte = 'private, max-age=%<max_age>s'.freeze
|
45
|
+
|
46
|
+
# rubocop: disable Metrics/AbcSize
|
47
|
+
def date_and_cache_headers(response, path, timestamp = Time.now)
|
48
|
+
maximum_age = options[:http_cache_seconds]
|
49
|
+
date_format = options[:http_date_format]
|
50
|
+
date_header(response, timestamp.strftime(date_format))
|
51
|
+
expires_header(response, Time.at(timestamp.to_i + maximum_age).strftime(date_format))
|
52
|
+
cache_control_header(response, format(PrivateMaxAgeTempalte, max_age: maximum_age.to_s))
|
53
|
+
last_modified_header(response, File.mtime(path).strftime(date_format))
|
54
|
+
end
|
55
|
+
# rubocop: enable Metrics/AbcSize
|
56
|
+
|
57
|
+
def content_type_header(response, content_type)
|
58
|
+
response.headers().set(HttpHeaderNames::CONTENT_TYPE, content_type)
|
59
|
+
end
|
60
|
+
|
61
|
+
def guess_content_type(path, _charset = nil)
|
62
|
+
i = path.rindex(/\./)
|
63
|
+
return nil if i == -1
|
64
|
+
extension = path[(i + 1)..].downcase.to_sym
|
65
|
+
::Server::MimeTypes.fetch(extension, ::Server::MimeTypes[:txt])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# module HeaderHelpers
|
69
|
+
end
|
70
|
+
# module WebSocket
|
@@ -0,0 +1,50 @@
|
|
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 'http_static_file_server_handler_instance_methods'
|
17
|
+
require_relative 'header_helpers'
|
18
|
+
require_relative 'response_helpers'
|
19
|
+
require_relative 'validation_helpers'
|
20
|
+
|
21
|
+
# The WebSocket module
|
22
|
+
module WebSocket
|
23
|
+
java_import Java::io.netty.channel.SimpleChannelInboundHandler
|
24
|
+
java_import Java::io.netty.handler.codec.http.FullHttpRequest
|
25
|
+
|
26
|
+
# The HttpStaticFileServerHandler class supports classical
|
27
|
+
# file server semantics.
|
28
|
+
class HttpStaticFileServerHandler < SimpleChannelInboundHandler
|
29
|
+
include WebSocket::HttpStaticFileServerHandlerInstanceMethods
|
30
|
+
include WebSocket::HeaderHelpers
|
31
|
+
include WebSocket::ResponseHelpers
|
32
|
+
include WebSocket::ValidationHelpers
|
33
|
+
attr_reader :options
|
34
|
+
|
35
|
+
def initialize(options = nil)
|
36
|
+
super(FullHttpRequest.java_class)
|
37
|
+
@options = options
|
38
|
+
end
|
39
|
+
|
40
|
+
# Please keep in mind that this method will be renamed to
|
41
|
+
# messageReceived(ChannelHandlerContext, I) in 5.0.
|
42
|
+
#
|
43
|
+
# protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception
|
44
|
+
def channelRead0(ctx, message)
|
45
|
+
messageReceived(ctx, message)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# class HttpStaticFileServerHandler
|
49
|
+
end
|
50
|
+
# module WebSocket
|
@@ -0,0 +1,160 @@
|
|
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 'cgi'
|
14
|
+
require 'date'
|
15
|
+
|
16
|
+
require 'java'
|
17
|
+
require 'netty'
|
18
|
+
|
19
|
+
require_relative 'file_server_channel_progressive_future_listener'
|
20
|
+
|
21
|
+
# The WebSocket module
|
22
|
+
module WebSocket
|
23
|
+
java_import Java::io.netty.channel.ChannelFutureListener
|
24
|
+
java_import Java::io.netty.channel.DefaultFileRegion
|
25
|
+
java_import Java::io.netty.handler.codec.http.DefaultHttpResponse
|
26
|
+
java_import Java::io.netty.handler.codec.http.HttpChunkedInput
|
27
|
+
java_import Java::io.netty.handler.codec.http.HttpResponseStatus
|
28
|
+
java_import Java::io.netty.handler.codec.http.HttpHeaderNames
|
29
|
+
java_import Java::io.netty.handler.codec.http.HttpMethod
|
30
|
+
java_import Java::io.netty.handler.codec.http.HttpVersion
|
31
|
+
java_import Java::io.netty.handler.codec.http.HttpUtil
|
32
|
+
java_import Java::io.netty.handler.codec.http.LastHttpContent
|
33
|
+
java_import Java::io.netty.handler.ssl.SslHandler
|
34
|
+
java_import Java::io.netty.handler.stream.ChunkedFile
|
35
|
+
java_import java.io.RandomAccessFile
|
36
|
+
|
37
|
+
# The HttpStaticFileServerHandlerInstanceMethods module
|
38
|
+
module HttpStaticFileServerHandlerInstanceMethods
|
39
|
+
ForwardSlashBeforeEOLPattern = %r{/$}.freeze
|
40
|
+
URIForwardSlashTemplate = '%<uri>s/'.freeze
|
41
|
+
|
42
|
+
# rubocop: disable Metrics/AbcSize
|
43
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
44
|
+
# rubocop: disable Metrics/MethodLength
|
45
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
46
|
+
def messageReceived(ctx, request)
|
47
|
+
return if %r{^#{options[:web_socket_path]}$}.match?(request.uri)
|
48
|
+
|
49
|
+
unless request.decoderResult().isSuccess()
|
50
|
+
send_error(ctx, HttpResponseStatus::BAD_REQUEST)
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
unless request.method() == HttpMethod::GET
|
55
|
+
send_error(ctx, HttpResponseStatus::METHOD_NOT_ALLOWED)
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
uri = request.uri
|
60
|
+
path = sanitize_uri(uri)
|
61
|
+
if path.nil?
|
62
|
+
send_error(ctx, HttpResponseStatus::FORBIDDEN)
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
unless File.exist? path
|
67
|
+
send_error(ctx, HttpResponseStatus::NOT_FOUND)
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
if File.directory? path
|
72
|
+
if ForwardSlashBeforeEOLPattern.match?(uri)
|
73
|
+
send_listing(ctx, path)
|
74
|
+
else
|
75
|
+
send_redirect(ctx, format(URIForwardSlashTemplate, uri: uri))
|
76
|
+
end
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
unless File.exist? path
|
81
|
+
send_error(ctx, HttpResponseStatus::FORBIDDEN)
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
# Cache Validation
|
86
|
+
modified_since = request.headers().get(HttpHeaderNames::IF_MODIFIED_SINCE)
|
87
|
+
if !modified_since.nil? && !modified_since.empty?
|
88
|
+
file_last_modified = File.mtime(path).to_s
|
89
|
+
# Only compare up to the second because the format of the timestamp
|
90
|
+
# sent to the client does not include milliseconds
|
91
|
+
modified_since_seconds = DateTime.parse(modified_since).to_time.to_i
|
92
|
+
file_last_modified_seconds = DateTime.parse(file_last_modified).to_time.to_i
|
93
|
+
|
94
|
+
if modified_since_seconds == file_last_modified_seconds
|
95
|
+
send_not_modified(ctx, file_last_modified_seconds)
|
96
|
+
return
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
raf = nil
|
101
|
+
begin
|
102
|
+
raf = RandomAccessFile.new(path, 'r')
|
103
|
+
rescue StandardError => _e
|
104
|
+
send_error(ctx, HttpResponseStatus::NOT_FOUND)
|
105
|
+
return
|
106
|
+
end
|
107
|
+
file_length = raf.length
|
108
|
+
|
109
|
+
response = DefaultHttpResponse.new(HttpVersion::HTTP_1_1, HttpResponseStatus::OK)
|
110
|
+
HttpUtil.setContentLength(response, file_length)
|
111
|
+
content_type_header(response, guess_content_type(path))
|
112
|
+
date_and_cache_headers(response, path)
|
113
|
+
keep_alive_header(response) if HttpUtil.isKeepAlive(request)
|
114
|
+
|
115
|
+
# Write the initial line and the header.
|
116
|
+
ctx.write(response)
|
117
|
+
|
118
|
+
send_file_future = nil
|
119
|
+
last_content_future = nil
|
120
|
+
progressive_promise = ctx.newProgressivePromise()
|
121
|
+
|
122
|
+
# Write the content.
|
123
|
+
if ctx.pipeline().get(SslHandler.java_class)
|
124
|
+
# SSL enabled - cannot use zero-copy file transfer.
|
125
|
+
chunked_file = ChunkedFile.new(raf, 0, file_length, 8192)
|
126
|
+
chunked_input = HttpChunkedInput.new(chunked_file)
|
127
|
+
send_file_future = ctx.writeAndFlush(chunked_input, progressive_promise)
|
128
|
+
# HttpChunkedInput will write the end marker (LastHttpContent) for us.
|
129
|
+
last_content_future = send_file_future
|
130
|
+
else
|
131
|
+
# SSL not enabled - can use zero-copy file transfer.
|
132
|
+
file_region = DefaultFileRegion.new(raf.channel, 0, file_length)
|
133
|
+
send_file_future = ctx.write(file_region, progressive_promise)
|
134
|
+
# Write the end marker.
|
135
|
+
last_content_future = ctx.writeAndFlush(LastHttpContent::EMPTY_LAST_CONTENT)
|
136
|
+
end
|
137
|
+
|
138
|
+
send_file_future.addListener(FileServerChannelProgressiveFutureListener.new)
|
139
|
+
|
140
|
+
# Decide whether to close the connection or not.
|
141
|
+
return if HttpUtil.isKeepAlive(request)
|
142
|
+
|
143
|
+
# Close the connection when the whole content is written out.
|
144
|
+
last_content_future.addListener(ChannelFutureListener::CLOSE)
|
145
|
+
end
|
146
|
+
# rubocop: enable Metrics/AbcSize
|
147
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
148
|
+
# rubocop: enable Metrics/MethodLength
|
149
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
150
|
+
|
151
|
+
def exceptionCaught(ctx, cause)
|
152
|
+
log.info "##{__method__} wtf2"
|
153
|
+
cause.printStackTrace()
|
154
|
+
return unless ctx.channel().isActive()
|
155
|
+
send_error(ctx, HttpResponseStatus::INTERNAL_SERVER_ERROR)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
# module HttpStaticFileServerHandlerInstanceMethods
|
159
|
+
end
|
160
|
+
# module WebSocket
|