websocket-server 1.0.1-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +185 -0
  4. data/Rakefile +56 -0
  5. data/exe/websocket +41 -0
  6. data/lib/log.rb +244 -0
  7. data/lib/server/mime_types.rb +38 -0
  8. data/lib/websocket/arguments_parser.rb +142 -0
  9. data/lib/websocket/channel_initializer.rb +73 -0
  10. data/lib/websocket/config.rb +59 -0
  11. data/lib/websocket/encoding.rb +21 -0
  12. data/lib/websocket/file_server_channel_progressive_future_listener.rb +32 -0
  13. data/lib/websocket/frame_handler.rb +71 -0
  14. data/lib/websocket/header_helpers.rb +70 -0
  15. data/lib/websocket/http_static_file_server_handler.rb +50 -0
  16. data/lib/websocket/http_static_file_server_handler_instance_methods.rb +160 -0
  17. data/lib/websocket/idle_handler.rb +41 -0
  18. data/lib/websocket/idle_state_user_event_handler.rb +47 -0
  19. data/lib/websocket/instance_methods.rb +127 -0
  20. data/lib/websocket/listenable.rb +41 -0
  21. data/lib/websocket/message_handler.rb +47 -0
  22. data/lib/websocket/response_helpers.rb +83 -0
  23. data/lib/websocket/server.rb +26 -0
  24. data/lib/websocket/shutdown_hook.rb +36 -0
  25. data/lib/websocket/ssl_cipher_inspector.rb +44 -0
  26. data/lib/websocket/ssl_context_initialization.rb +106 -0
  27. data/lib/websocket/telnet_proxy.rb +22 -0
  28. data/lib/websocket/validation_helpers.rb +51 -0
  29. data/lib/websocket/version.rb +16 -0
  30. data/lib/websocket-server.rb +13 -0
  31. data/lib/websocket_client.rb +478 -0
  32. data/lib/websocket_server.rb +50 -0
  33. data/web/client.html +43 -0
  34. data/web/css/client/console.css +167 -0
  35. data/web/css/client/parchment.css +112 -0
  36. data/web/favicon.ico +0 -0
  37. data/web/fonts/droidsansmono.v4.woff +0 -0
  38. data/web/js/client/ansispan.js +103 -0
  39. data/web/js/client/client.js +144 -0
  40. data/web/js/client/console.js +393 -0
  41. data/web/js/client/websocket.js +76 -0
  42. data/web/js/jquery.min.js +2 -0
  43. metadata +145 -0
@@ -0,0 +1,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