Netty | Telnet 인증 자동화 구현

주싱·2022년 2월 23일
0

Network Programming

목록 보기
6/21

시작하며

우주지상국 소프트웨어를 개발하면서 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

> 

Handler

TelnetAuthenticator Handler 는 간단히 다음과 같이 동작합니다.

  1. 메시지에 “Username: “ 문자열이 포함되어 있으면, 사용자 이름을 전송합니다.
  2. 메시지에 “Password: “ 문자열이 포함되어 있으면, 패스워드를 전송합니다.
  3. 메시지에 “>” 입력 대기 문자열이 포함되어 있으면, 인증 Handler 를 Pipeline 에서 삭제합니다. 인증 이후에는 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();
    }
}

Initialize ChannelPipeline

TelnetAuthenticator Handler 를 포함하는 ChannelPipeline 구성은 다음과 같이 할 수 있습니다. 먼저 InboundHandler 들을 다음과 같이 등록합니다.

  1. 먼저 “Username: “, “Password: “, “>” 문자열을 구분자로 하는 DelimiterBasedFrameDecoder 를 생성하고 추가합니다. 구분자들이 모두 수신되어야 인증 절차를 인식할 수 있기 때문에 stripDelimiter 옵션은 false로 설정합니다.
  2. StringDecoder 를 추가합니다.
  3. 구현한 TelnetAuthenticator Handler 를 추가합니다.
  4. 기타 필요한 비지니스 로직을 추가해줍니다.

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

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;
}
profile
소프트웨어 엔지니어, 일상

0개의 댓글