websocket-server 1.0.1-java

Sign up to get free protection for your applications and to get access to all the features.
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