希望我的这篇文章对某人有所帮助。
Netty | Implement Telnet Automated Authentication
在开发空间地面站软件时,我不得不使用 Telnet 来控制子设备。除了身份验证之外,Telnet 与常规 TCP 服务器通信非常相似。因此,我实现了一个自动处理 Telnet 身份验证以与 Telnet 服务器通信的 Handler。连接 Telnet 服务器时,依次显示以下介绍性消息“用户名:”、“密码:”消息,并请求用户验证。 Handler 自动处理身份验证过程,就像人类输入帐户信息一样。下面是实现的简要说明。
c:\> telnet 192.168.0.1 12345
Power On Self Test (POST) Passed.
Integrated Control Unit (ICU) Build xxx (Build:xxxxxx) - Feb 7 2022, 17:57:16 (Network/TCP)
Date and Time: 2022-02-16 20:01:19 (GMT)
MAC Address : [00:xx:xx:xx:C6:8F]
Username: User
Password: 1234
>
处理程序
TelnetAuthenticator Handler 的工作原理如下。
- 如果消息包含字符串“用户名:”,则发送用户名。
- 如果消息中包含字符串“Password:”,则发送密码。
- 如果消息包含等待输入的字符串“>”,则从管道中删除身份验证处理程序。认证后不需要 TelnetAuthenticator Handler。
如果账号未在Telnet服务器上注册或密码不匹配,则重复接收字符串“用户名:”或“密码:”。身份验证失败错误不可恢复,通知用户身份验证过程失败并强制他们断开连接。
@Slf4j
@RequiredArgsConstructor
public class TelnetAuthenticator extends SimpleChannelInboundHandler<String> {
private final ChannelSpec channelSpec;
private boolean alreadyUserTried = false;
private boolean alreadyPasswordTried = false;
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
// If the message contains the string “Username: “, send the username.
if (msg.contains(channelSpec.getReqUserTag())) {
if (alreadyUserTried) {
processFail(ctx);
}
ctx.channel().writeAndFlush(channelSpec.getAccount().getUser() + channelSpec.getEndLine());
alreadyUserTried = true;
return;
}
// If the message contains the string “Password: “, the password is sent.
if (msg.contains(channelSpec.getReqPasswordTag())) {
if (alreadyPasswordTried) {
processFail(ctx);
}
ctx.channel().writeAndFlush(channelSpec.getAccount().getPassword() + channelSpec.getEndLine());
alreadyPasswordTried = true;
return;
}
// If the incoming message contains an input waiting message, the Pipeline deletes the current handler.
if (msg.contains(channelSpec.getStandByTag())) {
ctx.pipeline().remove(this.getClass());
}
}
private void processFail(ChannelHandlerContext ctx) {
ctx.fireUserEventTriggered(ErrorMessage.AUTHENTICATE_FAIL);
ctx.close();
}
}
初始化 ChannelPipeline
带有 TelnetAuthenticator Handler 的 ChannelPipeline 配置可以是:首先,如下注册 InboundHandlers。
- 首先,添加带有“用户名:”、“密码:”、“>”字符串的 DelimiterBasedFrameDecoder 作为分隔符。 stripDelimiter 选项设置为 false,因为必须接收所有分隔符才能识别身份验证过程。
- 添加字符串解码器。
- 添加实现的 TelnetAuthenticator Handler。
- 添加其他必要的业务逻辑。
只需将 StringEncoder 添加到 Outbound。您可以根据需要添加其他 Handler。
public class PipelineInitializer extends ChannelInitializer<SocketChannel> {
private ChannelSpec channelSpec;
public void init(ChannelSpec channelSpec) {
this.channelSpec = channelSpec;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
// Inbound
.addLast(new DelimiterBasedFrameDecoder(1024, false,
channelSpec.getDelimiter().reqUserTag(),
channelSpec.getDelimiter().reqPasswordTag(),
channelSpec.getDelimiter().standByTag()))
.addLast(new StringDecoder())
.addLast(new TelnetAuthenticator(channelSpec))
.addLast(new BusinessLogic())
// Outbound
.addLast(new StringEncoder());
}
}
频道规格
ChannelSpec 定义了与 Telnet 服务器通信所需的规范。管理服务器IP、端口、账号信息、分隔符等。
@Getter
public class ChannelSpec {
private final String serverIp = "192.168.0.1";
private final int serverPort = 12345;
private final String endLine = "\r\n";
private final String standByTag = ">";
private final String reqUserTag = "Username: ";
private final String reqPasswordTag = "Password: ";
private final Account account = new Account("User", "1234");
private final Delimiter delimiter = new Delimiter();
public class Delimiter {
public ByteBuf standByTag() {
return toByteBuf(standByTag);
}
public ByteBuf reqUserTag() {
return toByteBuf(reqUserTag);
}
public ByteBuf reqPasswordTag() {
return toByteBuf(reqPasswordTag);
}
private ByteBuf toByteBuf(String input) {
ByteBuf delimiterBuf = Unpooled.buffer();
delimiterBuf.writeCharSequence(input, StandardCharsets.UTF_8);
return delimiterBuf;
}
}
}
@RequiredArgsConstructor
@Getter
public class Account {
private final String user;
private final String password;
}