우주지상국 소프트웨어를 개발하면서 Telnet 으로 서브 장비를 제어할 일이 생겼습니다. Telnet 은 인증 절차를 제외하면 일반적인 TCP 서버 통신과 크게 다르지 않습니다. 그래서 Telnet 서버와 통신 하기 위해 Telnet 인증을 자동 처리하는 Handler 를 구현해 보았습니다. Telnet 서버에 접속하면 다음과 같은 소개 메시지와 “Username: “, “Passwrod: “ 메시지가 차례로 출력되며 사용자 인증을 요구하게 되는데요. 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 는 간단히 다음과 같이 동작합니다.
만약 Telnet 서버에 등록되지 않은 계정이거나, 패스워드가 일치하지 않는 경우 “Username: “ 또는 “Password: “ 문자열이 반복 수신되게 됩니다. 인증 실패 오류는 복구할 수 없음으로 사용자에게 인증 절차 실패를 알리고 연결을 끊도록 합니다.
@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 (msg.contains(channelSpec.getReqUserTag())) {
if (alreadyUserTried) {
processFail(ctx);
}
ctx.channel().writeAndFlush(channelSpec.getAccount().getUser() + channelSpec.getEndLine());
alreadyUserTried = true;
return;
}
// 수신 메시지가 패스워드 요청 메시지를 포함하면, 패스워드를 전송한다.
if (msg.contains(channelSpec.getReqPasswordTag())) {
if (alreadyPasswordTried) {
processFail(ctx);
}
ctx.channel().writeAndFlush(channelSpec.getAccount().getPassword() + channelSpec.getEndLine());
alreadyPasswordTried = true;
return;
}
// 수신 메시지가 입력 대기 메시지를 포함하면, Pipeline 에서 현재 핸들러를 삭제한다.
if (msg.contains(channelSpec.getStandByTag())) {
ctx.pipeline().remove(this.getClass());
}
}
private void processFail(ChannelHandlerContext ctx) {
ctx.fireUserEventTriggered(ErrorMessage.AUTHENTICATE_FAIL);
ctx.close();
}
}
TelnetAuthenticator Handler 를 포함하는 ChannelPipeline 구성은 다음과 같이 할 수 있습니다. 먼저 InboundHandler 들을 다음과 같이 등록합니다.
Outbound Handler 에는 간단히 StringEncoder 만을 추가합니다. 필요에 따라 다른 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, Port, 계정 정보, 구분자 등을 관리합니다.
@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;
}