telnet-server 1.0.2-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 +189 -0
- data/Rakefile +56 -0
- data/exe/telnet +17 -0
- data/lib/log.rb +241 -0
- data/lib/telnet/argument_parser.rb +97 -0
- data/lib/telnet/config.rb +37 -0
- data/lib/telnet/handler.rb +99 -0
- data/lib/telnet/server.rb +34 -0
- data/lib/telnet/version.rb +16 -0
- data/lib/telnet-server.rb +13 -0
- data/lib/telnet_client.rb +518 -0
- data/lib/telnet_server.rb +37 -0
- metadata +116 -0
@@ -0,0 +1,99 @@
|
|
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 Telnet module
|
17
|
+
module Telnet
|
18
|
+
java_import Java::io.netty.channel.ChannelFutureListener
|
19
|
+
java_import Java::io.netty.channel.ChannelOption
|
20
|
+
java_import Java::io.netty.channel.SimpleChannelInboundHandler
|
21
|
+
java_import java.net.InetAddress
|
22
|
+
|
23
|
+
# The Handler class implements the barest minimum
|
24
|
+
# handler methods to support a telnet server.
|
25
|
+
class Handler < SimpleChannelInboundHandler
|
26
|
+
def initialize(options = {}, &handler)
|
27
|
+
super()
|
28
|
+
@options = options
|
29
|
+
@handler = handler
|
30
|
+
end
|
31
|
+
|
32
|
+
def isSharable
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def channelActive(ctx)
|
37
|
+
host_name = InetAddress.local_host.host_name
|
38
|
+
# Send greeting for a new connection.
|
39
|
+
ctx.write "Welcome to #{host_name}!\r\n"
|
40
|
+
ctx.write "The time is #{Time.now}.\r\n"
|
41
|
+
ctx.write @options[:prompt]
|
42
|
+
ctx.flush
|
43
|
+
end
|
44
|
+
|
45
|
+
# Please keep in mind that this method will be renamed to
|
46
|
+
# messageReceived(ChannelHandlerContext, I) in 5.0.
|
47
|
+
#
|
48
|
+
# java_signature 'protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception'
|
49
|
+
def channelRead0(ctx, msg)
|
50
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
|
51
|
+
messageReceived(ctx, msg)
|
52
|
+
end
|
53
|
+
|
54
|
+
def messageReceived(ctx, msg)
|
55
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
|
56
|
+
msg&.chomp!
|
57
|
+
log.info "Received message: #{msg}"
|
58
|
+
return super(ctx, msg) unless respond_to?(:handle_message)
|
59
|
+
handle_message(ctx, msg)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Generate and write a response.
|
63
|
+
# Writing to a ChannelBuffer is not needed here.
|
64
|
+
# The encoder will do the conversion.
|
65
|
+
def handle_message(ctx, message)
|
66
|
+
request = (message&.to_s || '').strip
|
67
|
+
return handle_empty_request(ctx) if request.empty?
|
68
|
+
return close(ctx) if quit_command?(request)
|
69
|
+
ctx.write "Server echo: #{request}\r\n"
|
70
|
+
ctx.write @options[:prompt]
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle_empty_request(ctx)
|
74
|
+
ctx.write "Please type something.\r\n"
|
75
|
+
ctx.write @options[:prompt]
|
76
|
+
end
|
77
|
+
|
78
|
+
def quit_command?(request)
|
79
|
+
@options[:quit_commands].include?(request.to_s.downcase.to_sym)
|
80
|
+
end
|
81
|
+
|
82
|
+
def close(ctx)
|
83
|
+
future = ctx.write "Bye!\r\n"
|
84
|
+
# Close the connection
|
85
|
+
future.addListener(ChannelFutureListener::CLOSE)
|
86
|
+
end
|
87
|
+
|
88
|
+
def channelReadComplete(ctx)
|
89
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
90
|
+
ctx.flush
|
91
|
+
end
|
92
|
+
|
93
|
+
def exceptionCaught(ctx, cause)
|
94
|
+
cause.printStackTrace()
|
95
|
+
ctx.close
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
# module Telnet
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
require 'java'
|
14
|
+
require 'netty'
|
15
|
+
require 'tcp-server'
|
16
|
+
|
17
|
+
require_relative 'handler'
|
18
|
+
|
19
|
+
# The Telnet module
|
20
|
+
module Telnet
|
21
|
+
# The Server class sets up the netty server
|
22
|
+
class Server < Server::Server
|
23
|
+
def initialize(params = {}, &block)
|
24
|
+
super
|
25
|
+
channel_initializer << ::Telnet::Handler.new(@options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def bootstrap
|
29
|
+
super
|
30
|
+
@bootstrap.childOption(Java::io.netty.channel.ChannelOption::SO_KEEPALIVE, true)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# module Telnet
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: false
|
3
|
+
|
4
|
+
# -*- mode: ruby -*-
|
5
|
+
# vi: set ft=ruby :
|
6
|
+
|
7
|
+
# =begin
|
8
|
+
#
|
9
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
10
|
+
#
|
11
|
+
# =end
|
12
|
+
|
13
|
+
# The Telnet module
|
14
|
+
module Telnet
|
15
|
+
VERSION = '1.0.2'.freeze
|
16
|
+
end
|
@@ -0,0 +1,518 @@
|
|
1
|
+
#! /usr/bin/env jruby
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
# frozen_string_literal: false
|
5
|
+
|
6
|
+
# -*- mode: ruby -*-
|
7
|
+
# vi: set ft=ruby :
|
8
|
+
|
9
|
+
# =begin
|
10
|
+
#
|
11
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
12
|
+
#
|
13
|
+
# =end
|
14
|
+
|
15
|
+
require 'logger'
|
16
|
+
require 'optparse'
|
17
|
+
|
18
|
+
require 'java'
|
19
|
+
require 'netty'
|
20
|
+
|
21
|
+
require_relative 'log'
|
22
|
+
require_relative 'telnet/version'
|
23
|
+
|
24
|
+
# Simple telnet client
|
25
|
+
|
26
|
+
# The Telnet module
|
27
|
+
module Telnet
|
28
|
+
def client_config
|
29
|
+
@client_config ||= {
|
30
|
+
host: '0.0.0.0',
|
31
|
+
port: 21,
|
32
|
+
ssl: false,
|
33
|
+
log_level: Logger::INFO,
|
34
|
+
quit_commands: %i[bye cease desist exit leave quit stop terminate],
|
35
|
+
max_frame_length: 8192,
|
36
|
+
delimiter: Java::io.netty.handler.codec.Delimiters.nulDelimiter
|
37
|
+
}
|
38
|
+
end
|
39
|
+
module_function :client_config
|
40
|
+
end
|
41
|
+
# module Telnet
|
42
|
+
|
43
|
+
# The Telnet module
|
44
|
+
module Telnet
|
45
|
+
CHANNEL_TYPE = Java::io.netty.channel.socket.nio.NioSocketChannel.java_class
|
46
|
+
java_import Java::io.netty.bootstrap.Bootstrap
|
47
|
+
java_import Java::io.netty.channel.nio.NioEventLoopGroup
|
48
|
+
java_import Java::io.netty.handler.ssl.SslContext
|
49
|
+
java_import Java::io.netty.handler.ssl.SslContextBuilder
|
50
|
+
java_import Java::io.netty.handler.ssl.util.InsecureTrustManagerFactory
|
51
|
+
|
52
|
+
# The ClientInitializationMethods module
|
53
|
+
module ClientInitializationMethods
|
54
|
+
def init(options)
|
55
|
+
@options = options
|
56
|
+
@host = options[:host]
|
57
|
+
@port = options[:port]
|
58
|
+
@queue = java.util.concurrent.LinkedBlockingQueue.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def bootstrap
|
62
|
+
@bootstrap = Bootstrap.new
|
63
|
+
@bootstrap.group(client_group)
|
64
|
+
@bootstrap.channel(::Telnet::CHANNEL_TYPE)
|
65
|
+
@bootstrap.handler(logging_handler) if @options[:log_requests]
|
66
|
+
@bootstrap.handler(channel_initializer)
|
67
|
+
end
|
68
|
+
|
69
|
+
def client_group
|
70
|
+
@client_group ||= NioEventLoopGroup.new
|
71
|
+
end
|
72
|
+
|
73
|
+
def logging_handler
|
74
|
+
@logging_handler ||= LoggingHandler.new(LogLevel::INFO)
|
75
|
+
end
|
76
|
+
|
77
|
+
def channel_initializer
|
78
|
+
@channel_initializer ||= ::Telnet::ClientChannelInitializer.new(@host, @port, @options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def configure_handlers(*handlers, &block)
|
82
|
+
channel_initializer.default_handler.add_listener(self)
|
83
|
+
channel_initializer.default_handler.listeners.addAll(handlers)
|
84
|
+
@user_app = block
|
85
|
+
@application_handler = lambda do |ctx, msg|
|
86
|
+
if @user_app.nil? || @user_app.arity == 1
|
87
|
+
@queue.add(msg)
|
88
|
+
else
|
89
|
+
@user_app.call(ctx, msg)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
# module ClientInitializationMethods
|
95
|
+
end
|
96
|
+
# module Telnet
|
97
|
+
|
98
|
+
# The Telnet module
|
99
|
+
module Telnet
|
100
|
+
java_import Java::io.netty.buffer.Unpooled
|
101
|
+
java_import Java::io.netty.channel.AbstractChannel
|
102
|
+
|
103
|
+
# The ClientInstanceMethods module
|
104
|
+
module ClientInstanceMethods
|
105
|
+
def puts(msg)
|
106
|
+
wait_until_channel_is_active
|
107
|
+
msg.chomp!
|
108
|
+
log.trace "#puts msg: #{msg.inspect}"
|
109
|
+
return if msg.nil? || msg.empty?
|
110
|
+
@last_write_future = @channel.writeAndFlush("#{msg}\r\n")
|
111
|
+
end
|
112
|
+
|
113
|
+
def gets(timeout = nil)
|
114
|
+
log.debug 'Waiting for response from server'
|
115
|
+
timeout.nil? ? @queue.take : @queue.take(timeout)
|
116
|
+
rescue StandardError => e
|
117
|
+
warn "Unexpected error waiting for message: #{e.message}"
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def wait_until_channel_is_active(timeout = 5, give_up = Time.now + timeout)
|
122
|
+
sleep 0.1 until @channel.active? || Time.now > give_up
|
123
|
+
end
|
124
|
+
|
125
|
+
def connect
|
126
|
+
@channel = bootstrap.connect(@host, @port).sync().channel()
|
127
|
+
@last_write_future&.sync()
|
128
|
+
rescue AbstractChannel::AnnotatedConnectException => e
|
129
|
+
raise e.message
|
130
|
+
rescue StandardError => e
|
131
|
+
raise "Connection failure: #{e.message}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def close(channel = @channel)
|
135
|
+
log.debug 'Closing primary channel'
|
136
|
+
channel.closeFuture().sync()
|
137
|
+
ensure
|
138
|
+
shutdown
|
139
|
+
end
|
140
|
+
|
141
|
+
def shutdown
|
142
|
+
log.debug 'Shutting down gracefully'
|
143
|
+
@client_group&.shutdownGracefully()
|
144
|
+
ensure
|
145
|
+
client_has_shut_down
|
146
|
+
end
|
147
|
+
|
148
|
+
def session
|
149
|
+
when_client_has_shut_down(@client_group) do |group|
|
150
|
+
log.debug "Channel group has shut down: #{group.inspect}"
|
151
|
+
end
|
152
|
+
@user_app.nil? ? read_user_commands : invoke_user_app
|
153
|
+
end
|
154
|
+
|
155
|
+
def invoke_user_app
|
156
|
+
@user_app&.call(self)
|
157
|
+
ensure
|
158
|
+
close
|
159
|
+
end
|
160
|
+
|
161
|
+
def shut_down_callbacks
|
162
|
+
@shut_down_callbacks ||= []
|
163
|
+
end
|
164
|
+
|
165
|
+
def when_client_has_shut_down(*args, &block)
|
166
|
+
shut_down_callbacks << {
|
167
|
+
block: block,
|
168
|
+
args: args
|
169
|
+
}
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
def client_has_shut_down
|
174
|
+
shut_down_callbacks.take_while do |callback|
|
175
|
+
callback[:block]&.call(*callback.fetch(:args, []))
|
176
|
+
end
|
177
|
+
rescue StandardError => e
|
178
|
+
log.error e.message
|
179
|
+
end
|
180
|
+
|
181
|
+
def channel_unregistered(ctx)
|
182
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
183
|
+
shutdown
|
184
|
+
end
|
185
|
+
|
186
|
+
def channel_active(ctx)
|
187
|
+
log.info "Connected to #{ctx.channel().remoteAddress0()}"
|
188
|
+
end
|
189
|
+
|
190
|
+
def channel_inactive(ctx)
|
191
|
+
log.info "Disconnected from #{ctx.channel().remoteAddress0()}"
|
192
|
+
end
|
193
|
+
|
194
|
+
def message_received(ctx, message)
|
195
|
+
log.debug "##{__method__} message: #{message}"
|
196
|
+
notify :message_received, ctx, message
|
197
|
+
if @application_handler.nil?
|
198
|
+
$stdout.print message.chomp unless message.nil?
|
199
|
+
else
|
200
|
+
@application_handler.call(ctx, message)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def execute_command(str, client = self)
|
205
|
+
return if str.empty?
|
206
|
+
client.puts "#{str}\r\n"
|
207
|
+
close if @options[:quit_commands].include?(str.downcase.to_sym)
|
208
|
+
end
|
209
|
+
|
210
|
+
def read_user_commands
|
211
|
+
log.trace 'Reading user commands'
|
212
|
+
loop do
|
213
|
+
log.debug 'Waiting for user input...'
|
214
|
+
input = $stdin.gets&.chomp
|
215
|
+
raise 'Poll failure from stdin' if input.nil?
|
216
|
+
break unless @channel.active?
|
217
|
+
break if execute_command(input).is_a?(AbstractChannel::CloseFuture)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
# module ClientInstanceMethods
|
222
|
+
end
|
223
|
+
# module Telnet
|
224
|
+
|
225
|
+
# The Telnet module
|
226
|
+
module Telnet
|
227
|
+
# The Listenable module
|
228
|
+
module Listenable
|
229
|
+
def listeners
|
230
|
+
@listeners ||= java.util.concurrent.CopyOnWriteArrayList.new
|
231
|
+
end
|
232
|
+
|
233
|
+
def add_listener(listener)
|
234
|
+
listeners << listener
|
235
|
+
end
|
236
|
+
|
237
|
+
def remove_listener(listener)
|
238
|
+
listeners.delete(listener)
|
239
|
+
end
|
240
|
+
|
241
|
+
def notify(message, *args)
|
242
|
+
return if listeners.empty?
|
243
|
+
log.trace "Notifying listeners (#{listeners}) of message: #{message}"
|
244
|
+
listeners.each do |listener|
|
245
|
+
listener.send(message.to_sym, *args) if listener.respond_to?(message.to_sym)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# The Telnet module
|
252
|
+
module Telnet
|
253
|
+
java_import Java::io.netty.channel.SimpleChannelInboundHandler
|
254
|
+
|
255
|
+
# The ModularHandler class
|
256
|
+
class ModularHandler < SimpleChannelInboundHandler
|
257
|
+
include ::Telnet::Listenable
|
258
|
+
|
259
|
+
def isSharable
|
260
|
+
true
|
261
|
+
end
|
262
|
+
|
263
|
+
def channelRegistered(ctx)
|
264
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
265
|
+
notify :channel_registered, ctx
|
266
|
+
super(ctx)
|
267
|
+
end
|
268
|
+
|
269
|
+
def channelUnregistered(ctx)
|
270
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
271
|
+
notify :channel_unregistered, ctx
|
272
|
+
super(ctx)
|
273
|
+
end
|
274
|
+
|
275
|
+
def channelActive(ctx)
|
276
|
+
::Telnet.log.info "Channel active #{ctx.channel}"
|
277
|
+
notify :channel_active, ctx
|
278
|
+
super(ctx)
|
279
|
+
end
|
280
|
+
|
281
|
+
def channelInactive(ctx)
|
282
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
283
|
+
notify :channel_inactive, ctx
|
284
|
+
super(ctx)
|
285
|
+
end
|
286
|
+
|
287
|
+
def messageReceived(ctx, msg)
|
288
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
|
289
|
+
notify :message_received, ctx, msg
|
290
|
+
end
|
291
|
+
|
292
|
+
def channelRead(ctx, msg)
|
293
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
|
294
|
+
notify :channel_read, ctx, msg
|
295
|
+
super(ctx, msg)
|
296
|
+
end
|
297
|
+
|
298
|
+
# Please keep in mind that this method will be renamed to
|
299
|
+
# messageReceived(ChannelHandlerContext, I) in 5.0.
|
300
|
+
#
|
301
|
+
# java_signature 'protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception'
|
302
|
+
def channelRead0(ctx, msg)
|
303
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{msg.inspect}"
|
304
|
+
messageReceived(ctx, msg)
|
305
|
+
end
|
306
|
+
|
307
|
+
def channelReadComplete(ctx)
|
308
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
309
|
+
notify :channel_read_complete, ctx
|
310
|
+
super(ctx)
|
311
|
+
end
|
312
|
+
|
313
|
+
def channelWritabilityChanged(ctx)
|
314
|
+
log.trace "##{__method__} channel: #{ctx.channel}"
|
315
|
+
notify :channel_writability_changed, ctx
|
316
|
+
super(ctx)
|
317
|
+
end
|
318
|
+
|
319
|
+
def userEventTriggered(ctx, evt)
|
320
|
+
log.trace "##{__method__} channel: #{ctx.channel}, event: #{evt}"
|
321
|
+
notify :user_event_triggered, ctx, evt
|
322
|
+
super(ctx, evt)
|
323
|
+
end
|
324
|
+
|
325
|
+
def exceptionCaught(ctx, cause)
|
326
|
+
::Telnet.log.warn "##{__method__} channel: #{ctx.channel}, cause: #{cause.message}"
|
327
|
+
listeners = notify :exception_caught, ctx, cause
|
328
|
+
super(ctx, cause) if listeners.empty?
|
329
|
+
end
|
330
|
+
|
331
|
+
IdentiferTemplate = '#<%<class>s:0x%<id>s>'.freeze
|
332
|
+
|
333
|
+
def to_s
|
334
|
+
format(IdentiferTemplate, class: self.class.name, id: object_id.to_s(16))
|
335
|
+
end
|
336
|
+
alias inspect to_s
|
337
|
+
end
|
338
|
+
# class ModularHandler
|
339
|
+
end
|
340
|
+
# module Telnet
|
341
|
+
|
342
|
+
# The Telnet module
|
343
|
+
module Telnet
|
344
|
+
java_import Java::io.netty.handler.codec.DelimiterBasedFrameDecoder
|
345
|
+
java_import Java::io.netty.handler.codec.string.StringDecoder
|
346
|
+
java_import Java::io.netty.handler.codec.string.StringEncoder
|
347
|
+
|
348
|
+
# The ClientChannelInitializer class
|
349
|
+
class ClientChannelInitializer < Java::io.netty.channel.ChannelInitializer
|
350
|
+
attr_accessor :decoder, :encoder
|
351
|
+
|
352
|
+
def initialize(host, port, options = {})
|
353
|
+
super()
|
354
|
+
@host = host
|
355
|
+
@port = port
|
356
|
+
@options = options
|
357
|
+
@decoder = StringDecoder.new
|
358
|
+
@encoder = StringEncoder.new
|
359
|
+
end
|
360
|
+
|
361
|
+
def default_handler
|
362
|
+
@default_handler ||= ::Telnet::ModularHandler.new
|
363
|
+
end
|
364
|
+
|
365
|
+
def initChannel(channel)
|
366
|
+
pipeline = channel.pipeline()
|
367
|
+
pipeline.addLast(ssl_handler(channel)) if @options[:ssl]
|
368
|
+
# pipeline.addLast(frame_decoder)
|
369
|
+
pipeline.addLast(decoder)
|
370
|
+
pipeline.addLast(encoder)
|
371
|
+
pipeline.addLast(default_handler)
|
372
|
+
end
|
373
|
+
|
374
|
+
def frame_decoder
|
375
|
+
DelimiterBasedFrameDecoder.new(@options[:max_frame_length], @options[:delimiter])
|
376
|
+
end
|
377
|
+
|
378
|
+
def ssl_handler(channel)
|
379
|
+
ssl_context&.newHandler(channel.alloc(), @host, @port)
|
380
|
+
end
|
381
|
+
|
382
|
+
def ssl_context
|
383
|
+
nil # TODO: Implement
|
384
|
+
end
|
385
|
+
end
|
386
|
+
# class ClientChannelInitializer
|
387
|
+
end
|
388
|
+
# module Telnet
|
389
|
+
|
390
|
+
# The Telnet module
|
391
|
+
module Telnet
|
392
|
+
# The Telnet::Client class
|
393
|
+
class Client
|
394
|
+
include ::Telnet::ClientInitializationMethods
|
395
|
+
include ::Telnet::ClientInstanceMethods
|
396
|
+
include ::Telnet::Listenable
|
397
|
+
|
398
|
+
def initialize(params = {}, &block)
|
399
|
+
init(::Telnet.client_config.merge(params.fetch(:options, {})))
|
400
|
+
configure_handlers(params.fetch(:handlers, []), &block)
|
401
|
+
connect
|
402
|
+
session
|
403
|
+
end
|
404
|
+
|
405
|
+
IdentiferTemplate = '#<%<class>s:0x%<id>s>'.freeze
|
406
|
+
|
407
|
+
def to_s
|
408
|
+
format(IdentiferTemplate, class: self.class.name, id: object_id.to_s(16))
|
409
|
+
end
|
410
|
+
alias inspect to_s
|
411
|
+
end
|
412
|
+
end
|
413
|
+
# module Telnet
|
414
|
+
|
415
|
+
# The Telnet module
|
416
|
+
module Telnet
|
417
|
+
# The ArgumentsParser class
|
418
|
+
class ArgumentsParser
|
419
|
+
IntegerPattern = /^\d+$/.freeze
|
420
|
+
Flags = %i[banner ssl log_level help version].freeze
|
421
|
+
attr_reader :parser, :options
|
422
|
+
|
423
|
+
def initialize(parser = OptionParser.new, options = ::Telnet.client_config.dup)
|
424
|
+
@parser = parser
|
425
|
+
@options = options
|
426
|
+
Flags.each { |method_name| method(method_name)&.call if respond_to?(method_name) }
|
427
|
+
end
|
428
|
+
|
429
|
+
def banner
|
430
|
+
@parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] <hostname> [port]"
|
431
|
+
@parser.separator ''
|
432
|
+
@parser.separator 'Options:'
|
433
|
+
end
|
434
|
+
|
435
|
+
def log_level
|
436
|
+
@parser.on_tail('-v', '--verbose', 'Increase verbosity') do
|
437
|
+
@options[:log_level] ||= 0
|
438
|
+
@options[:log_level] -= 1
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def help
|
443
|
+
@parser.on_tail('-?', '--help', 'Show this message') do
|
444
|
+
puts @parser
|
445
|
+
exit
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def version
|
450
|
+
@parser.on_tail('--version', 'Show version') do
|
451
|
+
puts "#{$PROGRAM_NAME} version #{Version}"
|
452
|
+
exit
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def validated_port(val)
|
457
|
+
raise OptionParser::InvalidArgument, "Invalid port: #{val}" unless \
|
458
|
+
IntegerPattern.match?(val.to_s) && val.positive? && val < 65_536
|
459
|
+
|
460
|
+
val
|
461
|
+
end
|
462
|
+
|
463
|
+
def parse_positional_arguments!
|
464
|
+
@options[:host] = ARGV.shift or raise OptionParser::MissingArgument, 'hostname'
|
465
|
+
return if (given_port = ARGV.shift&.to_i).nil?
|
466
|
+
|
467
|
+
@options[:port] = @parser.validated_port(given_port).to_i
|
468
|
+
end
|
469
|
+
end
|
470
|
+
# class ArgumentsParser
|
471
|
+
|
472
|
+
def parse_arguments(arguments_parser = ArgumentsParser.new)
|
473
|
+
arguments_parser.parser.parse!(ARGV)
|
474
|
+
arguments_parser.parse_positional_arguments!
|
475
|
+
arguments_parser.options
|
476
|
+
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption,
|
477
|
+
OptionParser::MissingArgument, OptionParser::NeedlessArgument => e
|
478
|
+
puts e.message
|
479
|
+
puts parser
|
480
|
+
exit
|
481
|
+
rescue OptionParser::AmbiguousOption => e
|
482
|
+
abort e.message
|
483
|
+
end
|
484
|
+
end
|
485
|
+
# module Telnet
|
486
|
+
|
487
|
+
# The Telnet module
|
488
|
+
module Telnet
|
489
|
+
# The ConsoleHandler class
|
490
|
+
class ConsoleHandler
|
491
|
+
def message_received(ctx, message)
|
492
|
+
log.trace "##{__method__} channel: #{ctx.channel}, message: #{message}"
|
493
|
+
log.debug "Received message: #{message}"
|
494
|
+
$stdout.print message unless message.nil?
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
# module Client
|
499
|
+
|
500
|
+
# The Telnet module
|
501
|
+
module Telnet
|
502
|
+
# rubocop: disable Metrics/AbcSize
|
503
|
+
def main(args = parse_arguments)
|
504
|
+
Logging.log_level = args[:log_level]
|
505
|
+
::Telnet::Client.new(options: args, handlers: ::Telnet::ConsoleHandler.new)
|
506
|
+
rescue Interrupt => e
|
507
|
+
warn "\n#{e.class}"
|
508
|
+
exit
|
509
|
+
rescue StandardError => e
|
510
|
+
::Telnet::Client.log.fatal "Unexpected error: #{e.class}: #{e.message}"
|
511
|
+
e.backtrace.each { |t| log.debug t } if Logging.log_level == Logger::DEBUG
|
512
|
+
exit 1
|
513
|
+
end
|
514
|
+
# rubocop: enable Metrics/AbcSize
|
515
|
+
end
|
516
|
+
# module Telnet
|
517
|
+
|
518
|
+
Object.new.extend(::Telnet).main if $PROGRAM_NAME == __FILE__
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#! /usr/bin/env jruby
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
# frozen_string_literal: false
|
5
|
+
|
6
|
+
# -*- mode: ruby -*-
|
7
|
+
# vi: set ft=ruby :
|
8
|
+
|
9
|
+
# =begin
|
10
|
+
#
|
11
|
+
# Copyright Nels Nelson 2016-2022 but freely usable (see license)
|
12
|
+
#
|
13
|
+
# =end
|
14
|
+
|
15
|
+
require 'java'
|
16
|
+
require 'tcp-server'
|
17
|
+
|
18
|
+
require_relative 'log'
|
19
|
+
require_relative 'telnet/argument_parser'
|
20
|
+
require_relative 'telnet/server'
|
21
|
+
|
22
|
+
# The Telnet module
|
23
|
+
module Telnet
|
24
|
+
def main(args = parse_arguments)
|
25
|
+
Logging.log_level = args[:log_level]
|
26
|
+
::Telnet::Server.new(options: args).run
|
27
|
+
rescue Interrupt => e
|
28
|
+
warn format("\r%<class>s", class: e.class)
|
29
|
+
exit
|
30
|
+
rescue StandardError => e
|
31
|
+
::Telnet::Server.log.fatal(e.message)
|
32
|
+
abort
|
33
|
+
end
|
34
|
+
end
|
35
|
+
# module Telnet
|
36
|
+
|
37
|
+
Object.new.extend(Telnet).main if $PROGRAM_NAME == __FILE__
|