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