UNIX/LINUX network programming 2
(간단한 telnet client 프로그램의 구현)

    전영준(한국항공대학교 컴퓨터공학과)
    Hitel ID : MonaC
    E-mail : monac@ee.korea.ac.kr

     

 

1. 텔넷 프로토콜

    텔넷은 인터넷 사의 다른 컴퓨터로 로그인을 할 수 있게 하는 프로토콜이다.
    텔넷에서 클라이언트는 입력 디바이스(키보드)와 출력 디바이스(모니터)를 가지며, 텔넷 서버 프로그램을 띄우고 있는 원격 호스트에 로그인 한다. 이때 원격 호스트의 입력과 출력은 클라이언트의 입력과 출력으로 대치된다. 텔넷 프로토콜의 기본 목표는 텔넷 데몬과 텔넷 클라이언트를 통한 원격 로그인이지만 많은 다른 용도로 사용될 수도 있다.

 

2. 텔넷 명령어

    텔넷 프로토콜은 서버와 클라이언트 서로를 제어하기 위하여 많은 명령어를 사용한다. 이러한 명령어는 IAC(interpret as command)문자 다음에 보내진다.
    IAC는 아스키 코드 255번이다.

     

    이름   ascii번호                     의미

    SE      240    End of subnegotiation parameters.
    NOP    241    No operation Data mark. Indicates the position of a Synch
                       event within the data stream.
    DM     242    This should always be accompanied by a TCP urgent
                       notification.
    BRK   243     Break. Indicates that the "break" or "attention" key was hit.
    IP      244     Suspend, interrupt or abort the process to which the NVT is
                       connected.
    AO     245    Abort output. Allows the current process to run to completion
                       but donot send its output to the user.
    AYT    246    Are you there. Send back to the NVT some visible evidence
                       that the AYT was received.
    EC     247     Erase character. The receiver should delete the last preceding
                       undeleted character from the data stream.
    EL     248     Erase line. Delete characters from the data stream back to but
                       not including the previous CRLF.
    GA    249      Go ahead. Used, under certain circumstances, to tell the
                       other end that it can transmit.
    SB    250      Subnegotiation of the indicated option follows.
    WILL  251     Indicates the desire to begin performing, or confirmation that
                       you are now performing, the indicated option.
    WONT 252    Indicates the refusal to perform, or continue performing, the
                       indicated option.
    DO    253     Indicates the request that the other party perform, or
                       confirmation that you are expecting the other party to perform,
                       the indicated option.    
    DONT 254    Indicates the demand that the other party stop performing, or
                       confirmation that you are no longer expecting the other party
                       to perform, the indicated option.
    IAC    255     Interpret as command.


    < 표1 telnet commands >

 

3. 텔넷 옵션

    또한 여러 가지 상태를 설정하기 위한 옵션이 있다. 이 옵션들은 언제든지 재설정될 수 있으며, 서버나 클라이언트 어느 쪽에서도 설정할 수 있다. 옵션의 정확한 스펙은 각각의 rpc를 참고 하여야 한다.

     

    ascii번호      이름                              RFC번호

    1               echo                               857
    3               suppress go ahead           858
    5               status                              859
    6               timing mark                      860
    24              terminal type                   1091
    31              window size                    1073
    32              terminal speed                1079
    33              remote flow control          1372
    34              linemode                        1184
    36              environment variables      1408


    < 표2 중요한 옵션들의 rfc번호>

    옵션은 클라이언트나 서버 어느 한쪽에서 요구를 하여, 다른 한 쪽에서 그것에 응답하는 형태로 설정된다. 옵션을 요구할 때는

     

    IAC, <요구의 형태>,<옵션>

     

    의 차례로 코드를 보내며, 응답 또한 같은 방식이다.
    요구의 형태는 다음의 4가지가 있다.

     

    기호       ascii번호     요구의 형태

    Will        251            옵션을 사용하겠다.
    DO        252            (상대방에게) 옵션을 사용하라.
    WONT    253            옵션을 사용하지 않겠다.
    DONT    254            (상대방에게) 옵션을 사용하지 마라

     

    WILL이나 WONT는 옵션을 요구하는 쪽이 그 옵션을 사용 또는 사용하지 않겠다는 뜻이며, DO나 DONT는 상대방측에 옵션을 사용 또는 사용하지 말라고 요구하는 것이다.

    한쪽에서 요구를 하면 상대방은 응답을 해야 한다. 요구와 응답의 조합은 다음의 경우만이 가능하다.

     

    요구          응답           응답의 의미

    WILL         DO             옵션 사용을 허락한다.
    WILL         DONT         옵션 사용을 허락하지 않는다.
    DO           WILL           옵션을 사용하겠다.
    DO           WONT         옵션을 사용할 수 없다.
    WONT       DONT         옵션을 사용하지 않겠다.
    DONT       WONT         옵션을 사용하지 마라.

     

    SB명령어는 하나 이상의 옵션을 필요로 할 때 사용된다. 터미널 타입이나, 터미널의 크기 등을 보내고 받는데 사용된다.

    텔넷 클라이언트로 텔넷의 옵션 협상 과정을 살펴보자.

     

    monac:~$telnet
    telnet> toggle options
    Will show option processing.
    telnet> open localhost
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    SENT DO SUPPRESS GO AHEAD
    SENT WILL TERMINAL TYPE
    SENT WILL NAWS
    SENT WILL TSPEED
    SENT WILL LFLOW
    SENT WILL LINEMODE
    SENT WILLOLD-ENVIRON
    SENT DO STATUS
    SENT WILL XDISPLOC
    RCVD DO TERMINAL TYPE
    RCVD DO TSPEED
    RCVD DO XDISPLOC
    RCVD DO NEW-ENVIRON
    SENT WONT NEW-ENVIRON
    RCVD WILL SUPPRESS GO AHEAD
    RCVD DO NAWS
    SENT IAC SB NAWS 0 80 (80) 0 25 (25)
    RCVD DO LFLOW
    RCVD DONT LINEMODE
    RCVD DONT OLD-ENVIRON
    RCVD WILL STATUS
    RCVD IAC SB TERMINAL-SPEED SEND
    SENT IAC SB TERMINAL-SPEED IS 9600,9600
    RCVD IAC SB X-DISPLAY-LOCATION SEND
    SENT IAC SB X-DISPLAY-LOCATION IS "monac:0.0"
    RCVD IAC SB TERMINAL-TYPE SEND
    SENT IAC SB TERMINAL-TYPE IS "XTERM"
    RCVD DO ECHO
    SENT WONT ECHO
    RCVD WILL ECHO
    SENT DO ECHO
    Debian GNU/Linux 1.3 monac.hackers.org
    Monac login: Connection closed by foreign host.


    지면 관계상 옵션에 대한 자세한 설명은 생략한다. 소스를 분석하면서 간단한 설명을 더
    첨가하도록 하겠다. 프로토콜에 대한 자세한 스펙은 RFC를 참고하기 바란다.

 

4. 텔넷 클라이언트

    프로그램의 소스는 프로토콜을 처리하는 protocal.c와 그 외의 모든 함수가 포함된 telnet
    로 나뉘어져 있다. 두 파일에 공통으로 필요한 함수는 telnet.h에서 선언되었다.
    아래의 설명은 프로그램 소스를 보면서 읽어보기 바란다.

    텔넷 클라이언트가 해야 할 일은 기본적으로 다음과 같다.

    -서버로의 stream socket 연결을 한다.
    -소켓과 사용자로부터 데이터를 읽고 쓴다.
    -읽은 데이터를 파싱하여 프로토콜을 처리한다.

소켓 연결 및 초기 설정

    서버와 소켓을 연결하는 과정은 다른 stream socket을 이용하는 프로그램과 별차이점이 없
    다. 우리는 명령행으로부터 주소와 포트를 입력받고, 그것으로 서버와 소켓 연결을 시도한다. 이러한 과정은 main()함수에서 처리된다.
    기술적인 설명은 지난 호의 강좌에서 자세하게 다루었으므로 여기서는 생략한다.

    소스의 main()에서 불리어지는 init_system()함수를 살펴보자.
    이 함수에서는 read_buf, write_buf 구조체를 초기화하고, SIGPIPE를 위한 시그널 핸들러를
    설치, 소켓을 nonblock 상태로 설정, 그리고 터미널을 세팅한다. Read_buf, write_buf구조체에 대해서는 뒤에서 설명하겠다
    우리는 프로세서가 블럭되는 것을 막기 위해 소켓을 nonblock 상태로 만든다.
    블록은 다음의 경우에 일어난다.

    -소켓에 read()를 호출했는데 읽을 데이터가 없을 때.
    -소켓에 write()를 호출했는데 당장에 쓸 수가 없을 때.
    -ioctl()이 당장에 실행될 수 없을 때.

    이 외에도 많은 경우가 있지만 이 프로그램에서는 위의 경우에 블록이 일어난다. 블록이 일어나면 프로세서는 블록이 해제될 때까지 멈추어 진다.
    텔넷에서는 여러 작업을 동시에 수행하므로 블럭이 일어나는 것을 막아야 한다.
    nonblock 모드에서는 위의 작업이 절대로 블럭되지 않는다는 것을 보장한다.
    만약 nonblock 상태로 설정했기 때문에 작업이 완성되지 않는다면 해당 시스템 호출은 에러를 리턴하고, errno 변수가 EWOULDBLOCK으로 설정될 것이다.
    nonblock의 설정은 다음과 같이 행해진다.

     

    void nonblock(int onoff)
    {
      if (ioctl(sock, FIONBIO, &onoff)<0)
      {
        perror("ioctl");
        sys_error();
      }
    }

     

    소켓의 연결이 끊겼을 때에는 read()가 0을 리턴하고 또한 write()가 SIGPIPE 시그널을 발생한다. SIGPIPE 시그널을 가로챔으로서 소켓 연결이 끊겼을 때를 알 수 있다.

     

    Signal(SIGPIPE,peer_died);

     

    SIGPIPE 시그널은 peer_died()함수에 연결되었다. Peer_died()함수는 상대방의 연결이 끊겼을 때의 각종 처리를 수행한다. 그러므로, 소켓의 연결이 끊어져서 write()가 SIGPIPE 시그널을 발생시키면 peer_died()함수가 실행된다.
    set_terminal()은 현재의 텔넷 상태에 따라 터미널을 세팅하는 함수이다.
    터미널의 상태는 텔넷 프로토콜로 조정이 된다. set_terminal()의 인수가 0일 때는 터미널을
    예전의 상태로 되돌려 놓는다.  
    이 다음으로 main() 함수는 init_telnet((port ==23));을 실행한다.
    init_telnet()함수는 텔넷 프로토콜을 파싱하기 위한 변수들을 초기화 하고, 텔넷 연결이 텔넷 데몬과의 연결일 땐 텔넷 연결의 초기 설정을 위한 프로토콜을 보낸다. 일반적으로 텔넷데몬의 포트는 23번이므로 여기서는 이 포트번호에 근거해서 텔넷 데몬과의 연결인지를 판단한다.

main_loop()

    main-loop() 함수는 텔넷 클라이언트의 일반적인 작업을 수행한다. 하나의 무한루프인 이 함수는 소켓과 데이터의 입출력, 그것의 프로토콜 처리 그리고 키보드로부터의 입력을 처리하는 과정을 반복해서 처리한다.
    이러한 작업은 select()시스템 호출을 이용하여 이루어진다.
    텔넷에서는 전송되는 데이터에 대해 프로토콜을 처리해야 하며, 효율적으로 데이터를 전송해야 하기 때문에 우리는 입력과 출력에 대해 버퍼를 설정하였다.
    버퍼는 write_buf와 read_buf가 있으며 이것의 형은 buffer구조체이다.

     

    Struct buffer
    {
      char buf;       /*데이터를 저장할 주소공간*/
      int size;         /*buf에 할당된 메모리의 크기*/
      int head, tail; /*buf에 저장된 데이터의 처음과 끝의 인덱스 */
      int count;      /*buf에 저장된 데이터의 byte수*/
    };

    struct buffer write_buf, read_buf;

     

    우리가 소켓에 쓸 데이터는 먼저 write_buf에 저장된다. 이 데이터는 main_loop()에서 select()시스템 호출로 소켓에 데이터를 쓸 수 있다는 것이 확인된 뒤에야 write_socket()함수를 통해서 실제로 전송이 된다. 소켓으로 읽은 데이터는 읽을 데이터가 있을 때에만 read_socket()을 통해서, 모두 read_buf에 저장된다. read_buf에 저장된 데이터는 프로토콜 처리를 위하여 process_protocol() 함수에서 읽혀진다. 표3은 이러한 데이터의 처리 과정을 도식적으로 그린 것이다.

     

    KEY_BOARD  --------> write_buf --------> Socket output
              Read_terminal()        write_socket()
    TERMINAL  <--------  read_buf  <-------- Socket input
              Process_protocol()       read_socket()

     

    <표3 데이터 처리과정>
    main_loop()는 이러한 과정을 반복적으로 처리한다.

버퍼 처리 함수

    텔넷 프로토콜을 처리하는 과정에서 우리는 서버쪽으로 프로토콜을 보낼 수도 있어야 하고, 소켓에서 읽혀진 데이터를 파싱하여 프로토콜 처리를 할 때에는 read_buf에서 하나씩 문자를 읽어서 필요한 프로토콜 부분을 추출해야 한다.
    이것을 위해서는 write_buf에 임으로 데이터를 쓸 수 있는 함수가 필요하고, read_buf에서 데이터를 임의로 읽어 들이는 함수가 필요하다.

    putc_socket()과 puts_socket()은 write_buf에 데이터를 집어넣는 함수이다.
    putc_socket()은 하나의 문자만을, puts_socket()은 하나의 문자열을 인수로 받는다. 이 함수들은 서버로 프로토콜을 전송하기 위해 쓰여질 것이다.
    putc_socket()이나 puts_socket()에 전달된 데이터는 write_buf에 저장될 것이고, 이것은 키보드로부터 읽은 데이터와 함께 소켓을 통해 서버에 전송된다.
    getc_socket()은 read_buf로부터 하나의 문자를 끄집어 낸다.
    Putc_terminal()은 하나의 문자를 터미널, 즉 화면에 출력하는 함수이다.

    이러한 편의함수들은 전부 프로토콜을 처리하는 과정에서 필요한 함수들이다.
    표3의 데이터 처리과정으로부터 본다면, 이 함수들은 전부 process_protocol()함수에서 필
    요로 한다.

텔넷 프로토콜 파싱

    process_protocol()은 read_buf에 저장된 데이터를 파싱하여 텔넷 프로토콜을 처리한다. 이
    함수는 main_loop()에서 read_buf에 데이터가 있을 때만 불려진다.
    텔넷 프로토콜을 위한 여러 #define 등이 arpa/telnet.h에 정의되어 있다.

     

    #define TELCMDS
    #define TELOPTS
    #include <arpa/telnet.h>

     

    여기서 TELCMDS, TELOPTS를 #include 앞에서 정의한 이유는 telnet.h에 있는 텔넷 명령어 테이블인 telcmds와, 텔넷 옵션 테이블인 telopts를 사용하기 위해서이다.
    Process_protocol()에서는 read_buf에서 프로토콜을 추출하고, 프로토콜이 아니면 화면에 출력하는 작업을 한다. 텔넷 명령어는 항상 IAC로 시작한다.
    getc_socket()은 read_buf에서 하나의 문자를 끄집어 낸다. 그 문자가 IAC이면 다음 문자들을 텔넷 명령어로 인식을 하고 파싱하게 된다. 아니라면 putc_terminal()을 이용하여 화면에 출력한다.
    우리가 구현한 텔넷 명령어는 DO , DON'T, WILL, WONT으로 시작하는 텔넷 옵션과, SB로 시작하는 텔넷 서브 옵션이다.

텔넷 옵션 처리

    process_option()은 IAC+[DO|DONT|WILL|WONT]으로 시작되는 옵션을 처리하는 함수이다. 옵션을 처리하는데 있어서 가장 어려운 점은 텔넷 옵션 협상 과정이 대칭적이란 점이다. 즉 똑 같은 옵션을 받을 수 있고, 보낼 수도 있어야 한다는 점이다. 이럴 때는 서버로부터 받은 옵션이 우리가 보낸 옵션의 응답인지 아니면 요구를 하는 명령인지를 구분하기가 어렵다. 이 처리를 하기 위해서 option[]배열을 만들었다.

    options[]의 각각의 필드는 해당 옵션에 대해 요구를 했는지의 여부를 저장한다.
    이 배열을 다루기 위해 편의함수 option_requeted()와 option_request()가 있다.
    option_request()는 option[]에서 해당 옵션 필드를 요구시에는 1 증가 시키고, 응답시에는 1 감소시킴으로서 현재의 요구 상태를 관리한다.

    option_requested()는 해당 옵션이 요구되었는지 여부를 판단한다. 이것이 '참'을 리턴하면 해당 옵션이 요구되었다는 것을 의미한다. 이 함수를 이용하여 우리가 옵션을 서버에 보낼 때에는 option_requestd()를 호출하여 옵션 요구 상태를 설정하고, 옵션을 서버로부터 받을 때에는 그것이 요구인지 응답인지의 여부를 option_requested()를 호출함으로써 판단한다.

    옵션 협상 과정을 통해 처리된 옵션이 저장되어야 할 경우에는 이 정보를 mode에 저장한다. ECHO의 여부(MODE_ECHO)나 바이너리 모드인지의 여부 (MODE_INBIN, MODE_OUTBIN) 등이 이러한 정보다. Mode를 다루는 함수는 mode_set() 이다. 가공 모드에 대한 정의는 arpa/telnet.h에 포함되어 있다. 우리가 다루는 모드는 모두 set_terminal()에서 처리된다.
    옵션이 ON 또는 OFF 되었을 경우 set_terminal()이 호출되어 해당 모드에 대한 실제적인 처리를 한다.

    텔넷 프로토콜의 정의에 따르면 텔넷 클라이언트는 모든 텔넷 옵션을 처리할 수 있어야 한다고 되어 있다. 우리가 지원하지 않는 옵션이라도 서버가 요구하면 응답할 수 있어야 한다. Option_ok()는 옵션이 현재 지원되는지 여부를 판단한다.
    어떤 옵션은 요구할 때의 의미와 요구 받았을 때의 의미가 다르다

    TELOPT_BINARY 옵션은 8비트 데이터의 처리 여부를 결정하는 옵션이다. 서버가 WILL 또는 WONT로 요구했을 경우에는 서버가 8 비트 데이터를 처리할지 여부를 말하는 것이므로, 우리는 MODE_INBIN 즉 8비트를 받아들이겠다는 모드를 설정해야 한다. TELOPT_BINARY가 DO 또는 DONT를 요구한다면 이는 클라이언트의 8비트 처리 여부이크로, 틀라이언트가 8비트를 출력하겠다는 MODE_OUTBIN을 설정해야 한다. TELOPT_ECHO는 터미널이 키보드로부터의 입력을 ECHO해야 하는지를 설정한다. 서버가 ECHO를 하겠다면 컬라이언트는 ECHO를 하지 말아야하고, 서버가 ECHO를 하지 않겠다면 클라이언트는 ECHO를 해야한다.

Network Virtual Terminal

    텔넷 프로토콜은 Network Virtual Terminal(NVT)를 포함한다. 클라이언트는 서버로부터 받은 NVT코드를 화면 처리를 위한 적절한 코드로 바꾸어야 하고, 사용자 키보드로부터 받은 입력을 NVT코드로 바꾸어야 한다. 이것은 CR/NL 맵팅, 탭의 처리 등을 포함한다. 우리의 프로그램은 이러한 처리를 set_terminal()에서 터미널 세팅을 통해서 행한다. 그러나 이것은 NVT를 완벽히 지원하지 않는다. 다만 간단히 하기 위한 해킹이다.

프로그램 종료 함수

    프로그램을 끝내는 함수로는 peer_died(), do_bye(), sys_error()가 있다.
    peer_error()는 서버가 연결을 끊었을 때 호출되고, sys_error()는 시스템 호출의 에러로 인하여 더 이상 작업을 할 수 없을 때 호출된다. Do_bye()는 사용자가 연결을 끊을 때 불리어진다. 이 종료 함수들이 하는 일은 모두 같다.

    -close()로 소켓을 닫는다.
    -터미널을 원래의 상태로 회복시킨다.
    -malloc된 메모리를 모두 해제한다.
    -적절한 메시지를 내보낸다.
    -exit()를 호출하여 프로그램을 끝낸다.

    Set_terminal()에서 터미널을 항상 raw 모드로 만든다(완전한 raw 모드는 아니다). 그래서 사용자의 키보드 인터럽트 등이 동작하지 않는다. 우리가 escape character를 지원하지 않기 때문에 현재 do_bye()를 통해 사용자가 프로그램을 강제 종료 시킬 수는 없다.

 

5. 간단한 텔넷 클라이언트의 소스

    텔넷의 옵션은 많고도 아주 복잡하다. 이러한 처리를 모두 지원하기 위해서는 프로그램이 상당히 복잡해진다. 여기서는 간단히 필요한 몇 개의 옵션만 지원하며, 알고리즘도 간단하게 하여 완전한 텔넷 스펙을 지원하지 않는다.

     

    /*
     * telnet.h
     *
     *함수 선언.
     *by 전영준
     */

    /* telnet.c 에서 정의된 함수*/
    void peer_died();
    void do_bye();
    void sys_error();

    void putc_terminal(int.c);
    int getc_socket(int*c);
    void putc_socket(int c);
    void puts_socket(char*);

    int set_terminal(int set);
    void nonblock(int onoff);

    void debug(const char*, ...);

    /* protocol.c 에서 정의된 함수*/
    void int_telnet(int istelnet);
    void process_protocol(void);

    /*the end of telnet*/

    /*
    *telnet.c
    *
    *by 전영준
    */

    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <stdarg.h>

    #include <sys/time.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netdb.h>
    #include <arpa/inet.h>

    #include <termios.h>
    #include <sys/ioctl.h>

    #include <arpa/telnet.h>

    #include "telnet.h"

    #define MAX_WRITE_BUF PIPE_BUF*2
    #define MAX_READ_BUF PIPE_BUF

    int do_debug = 0;

    char hostname[MAXHOSTNAMELEN]
    int port;
    int sock;

    struct buffer
    {
      char buf;      /*데이터를 저장할 주소공간*/
      int size;     /*buf에 할당된 메모리의 크기*/
      int head, tail; /*buf에 저장된 데이터의 처음과 끈의 인덱스*/
      int count;    /*buf에 저장된 데이터의 byte수*/
    };

    struct buffer write_buf, read_buf;

    void init_system(void);

    void main_loop();
    void read_socket();
    void read_terminal();
    void write_socket();

    void usage()
    {
      fprintf(stdeer,"usage; mtelnet[-g] [hostname [port]]\n");
      exit(1);
    }

    void main(int argc, char *argv[])
    {
      struct hostent *host;
      struct sockaddr_ serv_addr;
      int c;
      extern int optind;

      while((c = getopt(argc, argv, "g"))!=EOF)
      {
        switch(c )
        {
        case 'g';
          do_debug = 1;
          break;
        default;
          usage();
        }  
    }

            argc-=optind;
            argv+=optind;

    if(argc <=0||argc>2)
      usage();
    if(argc==2)
      port=atoi(argv[1]);
    else
      port=23;

    bzero((char*)&serv_addr,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_port =htons(port);

    if((serv_addr.sin_addr.s_addr=inet_addr(argv[0]))!=INADDR_NONE)
    {
      strcpy(hostname,arv[0]);
    }
    else
    {
      if((host=gethostbyname(argv[0]))==NULL)
      {
        herror("gethostbyname");
        exit(1);
      }
      serv_addr.sin_family=host->h_addrtype;
    bcopy(host->h_addr,(char*) &serv_addr.sin_addr,host->h_length);
    strcpy(hostname, host->h_name);
    }

    if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
    {
    perror("socket");
    exit(1);
    }
    printf("Connected to %s.\n", hostname);

    init_system(void)
    {
    write_buf.buf = (char*) malloc(MAX_WRITE_BUF);
    write_buf.size = MAX_WRITE_BUF;
    write_buf.head = write_buf.tail = 0;
    write_buf.count = 0;

    read_buf.buf = (char*) malloc(MAX_READ_BUF);
    read_buf.size = MAX_READ_BUF;
    read_buf.head = read_buf.tail = 0;
    read_buf.count = 0;

      signal(SIGPIPE, peer_died);
      nonblock(1);
      set_terminal(1);
    }

    void peer_died(void)
    {
      close(sock);
      set_terminal(0);

      free(write_buf.buf);
      free(read_buf.buf);

      fprintf(stderr,"Connection closed by foreign host.\n");
      exit(1);
    }

    void do_bye()
    {
      close(sock);
      set_terminal(0);

      free(write_buf.buf);
      free(read_buf.buf);

              printf("Connection closed.\n");
      exit(0);
    }

    void sys_error()
    {
      close(sock);
      set_terminal(0);

      free(write_buf.buf);
      free(read_buf.buf);

      exit(1);
    }

    void main_loop()
    {
      int maxfd;
      fd_set in_set, out_set, exc_set;
      int ret;

      for(;;)
      {
        FD_ZERO(&in_set);
        FD_ZERO(&out_set);
        FD_ZERO(&exc_set);
        FD_SET(sock, &in_set);
        If(write_buf.count>0)
          FD_SET(sock, &out_set);
        FD_SET(0, &in_set);
        maxfd = sock;
        /*
         *디스크립터에 데이터가 들어올때까지 계속 기다리게 하기 위하여
         *timeout을 NULL로 지정 하였다.
         */
         ret = select(maxfd + 1, &in_set, &out_set, &exc_set, NULL);
         if(ret<0)
         {
           perror("select");
           sys_error();
         }
         /*키보드로부터의 입력을 읽는다.*/
         if(FD_ISSET(0, &in_set))
         {
           FD_CLR(sock, &in_set);
           read_terminal();
         }
         /*소켓에 데이터를 쓴다.*/
         if(FD_ISEET(sock, &out_set))
         {
           F

    D_DLR(sock, &out_set);
           Write_socket();
         }
         /*소켓으로부터 데이터를 읽는다.*/
         if(FD_ISSET(sock, &in_set))
         {
            FD_CLR(sock, &in_set);
            read_socket();
         }  
         /*소켓에서부터 읽은 데이터가 있으면 그것을 처리한다.*/
         if(read_buf.count>0)
         {
           process_protocol();
         }
      
      }
    }
    /*소켓에서 데이터를 읽어서 read_bif에 저장한다. */
    void read_socket()
    {
      int n;

    if(read_buf.size == read_buf.tail)
      return;/*read_buf에 여유공간이 없다. */

    n=read(sock, read_buf.buf+read_buf.tail, read_buf.size_read_buf.tail);
      if(n< 0 && errno ==EWOULDBLOCK)
         n = 0;
      if(n<0)
      {
        perror("read");
        sys_error();
      }
      if (n ==0)
        peer_died();

      read_buf.count +=n;
      read_buf.tail +=n;
    }

    /*키보드로부터 입력을 받아서 write_buf에 저장한다.*/
    void read_terminal()
    {
      int n;

      if (write_buf.size ==write_buf.tail)
         return;

      n = read(0, write_buf.buf+write_buf.tail,
          write_buf.size-write_buf.tail);
      if(n<0 &&errno ==EWOULDBLOCK)
        n = 0;
      if(n<0)
      {
        perror("read");
        sys_error();
      }
      if(n ==0)
      {
        do_bye();
      }

      write_buf.count+= n;
      write_buf.tail+= n;

    }

    /*write_buf에 있는 데이터를 소켓에 쓴다.*/
    void write_socket()
    {
      int n;

      n = write(sock, write_buf.buf+write_buf.head, write_buf.count);
             if(n<0&&(errno==ENOBUFS || errno==EWOULDBLOCK))
        n = 0;
      if(n<0)
      {
        perror("write");
        sys_error();
      }

      write_buf.head+=n;
      write_buf.count-=n;

      if(write_buf.count==0)
        write_buf.hea = write_buf.tail = 0;

    }

    /*
     *putc_socket(int)
     *puts_socket(char*)
     *getc_socket(char*)
     *putc_terminal(int)
     *이 함수들은 process_protocol() 수행중 필요한 함수들이다.
     *
     *putc_socket()와 puts_socket()는 write_buf에 데이터를 쓰기위한 함수.
     *getc_socket()는 read_buf에서 데이터를 읽기 위한 함수.
     *putc_terminal()는 데이터를 화면에 찍기 위한 함수이다.
     */

    void putc_socket(int c)
    {
    if(write_buf.tail == write_buf.size)
      /*write_buf에 여유공간이 없다.*/
      write_socket();
    if(write_buf.tail ==write_buf.size)
    {
      fprintf(stderr,"write buffer full!\n");
      return;
    }
    write_buf.buf[write_buf.tail++] = c;
    write_buf.count++;
    }

    void puts_socket(char*s)
    {
    int len = strlen(s);
    if(write_buf.tail + len>write_buf.size)
      /*write_buf에 여유공간이 없다.*/
      write_socket();
    if(write_buf.tail + len > write_buf.size)
    {
      fprintf(stderr,"write buffer full!\n");
      return;
    }
    strcpy(write_buf.buf+write_buf.tail, s);
    write_buf.tail +=len;
    write_buf.count+=len;
    }

    int getc_socket(int*c)
    {
    if(read_buf.count==0)
      /*더 이상 읽을 것이 없다.*/
      return 0;

    *c = read_buf.buf[read_buf.head++];
    *c = *c & 0xff;

    read_buf.count-;

    if(read_buf.count ==0)
      read_buf.head = read_buf.tail =0;
    return read_buf.count+1;
    }

    void putc_terminal(int c)
    {
    int n;
    n = write(1, &c, 1);
    if(n<0)
    {
      perror("write");
      sys_error();
    }
    }

    /*소켓을 nonblock 상태로 만든다. */
    void nonblock(int onoff)
    {
    if(ioctl(sock, FIONBIO, &onoff)<0
    {
      perror("ioctl");
      sys_error();
    }

    }

    int set_terminal(int set)
    {
    static struct termios save_termios;
    struct termios buf;
    static int isset = 0;
    extern int mode;

    if(isset == 0 && set ==0)
      return 0;

    if(set ==0)
    {
      if(tcsetattr(0, TCSAFLUSH, &save_termios)<0)
        return -1;
      return 0;
    }

    if(isset == 0)
      if(tcgetattr(0, &save_termios)<0)
        return-1;

    if(tcgetattr(0, &buf)<0)
      return-1;

    buf.c_lflag &=~ISIG;
    buf.c_oflag |=ONLCR;
    buf.c_oflag |=OPOST;
    buf.c_iflag |=ICRNL;

      if(mode & MODE_ECHO)
    {
    buf.c_flag|=ECHO;
    buf.c_oflag|=ONLCR;

    buf.c_iflag|=ICRNL;
    buf.c_lflag|=ICANON;
    }
    else
    {
    buf.c_lflag & =~ECHO;
    /*buf.c_oflag&=~ONLCR;*/

    buf.c_lflag&=~ICANON;
    /*어떤 시스템에서는 NL(0x0A)을 보내야 한다.*/
    /*buf.c_iflag & =~ICRNL;*/
    buf.c_cc[VMIN] = 1;
    buf.c_cc[VTIME] = 0;
      }

    if(mode & MODE_FLOW)
    buf.c_iflag|=IXANY|IXOFF|IXON;
    else
    buf.c_iflag &=~(IXANY|IXOFF|IXON);

          if (mode & MODE_INBIN)
                    buf.c_iflag & =~ISTRIP;
          else
    buf.c_iflag |=ISTRIP;
          if(mode& MODE_OUTBIN)
    {
                    buf.c_cflag &=~(CSIZE|PARENB);
                    buf.c_cflag|=CS8;
                    /*buf.c_oflag &= ~OPOST;*/
    }
    else
    {
    buf.c_cflag &=~(CSIZE|PARENB);
    buf.c_cflag|=save_termios.c_cflag & (CSIZE|PARENNB);
    buf.c_oflag|=OPOST;
    }
    if(tcsetattr(0, TCSAFLUSH, &buf<0)
      return-1;
    isset = set;
    return 0;
    }

    void debug(const char *msg, ...)
    {
    va_list ap;

    if(!do_debug)
      return;

    va_start( ap,msg);
    vfprintf( stderr, msg, ap);
    va_end(ap);
    fprintf(stderr, "\n");

    }

    /*the end of telnet.c*/

    /*
     *protocol.c
     *
     *프로토콜 처리를 위한 루틴들
     *by 전영준
     */

    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/ioctl.h>

    #define TELCMDS
    #define TELOPTS
    #include <arpa/telnet.h>

    #include "telnet.h"

    char options[256];
    int mode;

    void process_option(int cmd, int opt);
    void send_option(in cmd, int opt, int);
    int option_ok(int opt);
    void clean_sb();
    void process_sb();
    void send_naws();
    void send_ttype();
    void mode_set(int m, int set);
    int option_requested(int opt);
    void option_request(int opt, int req);

    void init_telnet(int istelnet)
    {
    int i;

    for(i=0;i<256;I++)
    options[i] = 0;

    mode = 0;
    /*현재 디폴트로 설정되어 있는 모드*/
    mode_set(MODE_ECHO,1);

    set_terminal(1);
    /*텔넷접속일 경우에는 텔넷 초기 설정을 한다.*/
    if(istelnet)
    {
       send_option(DO, TELOPT_SGA, 1);
       send_option(WILL, TELOPT_TTYPE, 1);
       send_option(WILL, TELOPT_NAWS, 1);
       send_option(WILL, TELOPT_LFLOW, 1);
       send_option(WILL, TELOPT_LINEMODE, 1);
       send_option(DO, TELOPT_STATUS, 1);

       /*enter binarymode*/
       send_option(DO, TELOPT_BINARY, 1);
       send_option(WILL, TELOPT_BINARY, 1);
    }
    }

    /*read_buf에 저장되어 있는 데이터에서 프로토콜을 찾아서 처리한다.*/
    void process_protocol(void)
    {
    int c;

    while(getc_socket(&c))
    {
      if(c ==IAC)
      {
        if(!getc_socket(&c))
           return;
        switch(c)
        {
        case IAC:
          putc_terminal(c);
          break;
        case DON'T:
        case DO:
        case WONT:
        case WILL:
        {
            int opt;
            if(!getc_socket(&opt))
    return;
            process_option(c, opt);
            break;
    }
    case SB:
       if(!getc_socket(&c))
         return;
       if(c ==TELOPT_TTYPE)
       {
         if(!getc_socket(&c))
           return;
         clean_sb();
         if(c ==TELQUAL_SEND)
           send_ttype();
         break;
       }
       clean_sb();
       break;

    default:
       break;

    }
        }
        else
        {
    putc_terminal(c);
        }
    }
    }

    void clean_sb()
    {
    int c;
    for(;;)
    {
         if(!getc_socket(&c))
            return;
         if(c ==IAC)
         {
    if(!getc_socket(&c))
       return;
    if(c ==SE)
       return;
    }
       }
    }

    void process_option(int cmd, int opt)
    {
    debug("RCVD: IAC %s%s", TELCMD(cmd), TELOPT(opt));

    /* If this is an option we do not understand or have not implemented, refuse any 'DO'
    request. */
    if(!option_ok(opt))
    {
      if( cmd == DO)
         send_option(WONT, opt,0);
      if( cmd == WILL)
         send_option(DONT, opt,0);
    }
    else if(cmd == DO)
    {
      switch(opt)
      {
      case TELOPT_ECHO:
        /*never echo if once turned off*/
        mode_set(MODE_ECHO, 0);
        send_option(WONT, opt, 0);
        goto out_processing;
        return;
      case TELOPT_BINARY:
        mode_set(MODE_OUTBIN, 1);
        break;
      case TELOPT_LFLOW:
        mode_set(MODE_FLOW, 1);
        break;
      case TELOPT_NAWS:
        send_naws();
        break;
      }
      if(!option_requested(opt))

        send_option(WILL, opt, 0);
    }
    else if( cmd == DONT)
    {
      switch(opt)
      {
      case TELOPT_ECHO;
        mode_set(MODE_ECHO, 0);
        break;
      case TELOPT_BINARY:
        mode_set(MODE_OUTBIN, 0);
        break;
      case TELOPT_LFLOW:
        mode_set(MODE_FLOW, 0);
        break;

      }
      if(!option_requested(opt))
        send_option(WONT, opt, 0);
    }
    else if( cmd==WILL)
    {
      switch(opt)
      {
      case TELOPT_ECHO:
        mode_set(MODE_ECHO, 0);
        break;
      case TELOPT_BINARY:
        mode_set(MODE_INBIN, 1);
        break;
      case TELOPT_LFLOW:
        mode_set(MODE_FLOW, 1);
        break;

      }
      if(!option_requested(opt))
        send_option(DO, opt, 0);
    }
    else if(cmd == WONT)
    {
      switch(opt)
      {
      case TELOPT_ECHO:
        mode_set(MODE_ECHO, 1);
        break;
      case TELOPT_BINARY:
        mode_set(MODE_INBIN, 0);
        break;
      case TELOPT_LFLOW:
        mode_set(MODE_FLOW, 0);
        break;
      }
      if(!option_requested(opt))
        send_option(DON'T, opt, 0);
    }
    out_processing:
    set_terminal(1);
    }
    void send_option(int cmd, int opt, int request)
    {
    if(request && !option_ok(opt))
      return;

    option_request(opt, request);

    debug("SENT:IAC%s%s", TELCMD(cmd), TELOPT(opt));
    putc_socket(IAC);
    putc_socket(cmd);
    putc_socket(opt);
    }

    /*터미널 타입*/
    void send_ttype()
    {
    char s[50];
    strcpy(s, (getenv("TERM") ==NULL? "UNKNOWN" : getenv("TERM")));

    putc_socket(IAC);
    putc_socket(SB);
    putc_socket(TELOPT_TTYPE);
    putc_socket(TELQUAL_IS);
    putc_socket(s);
    putc_socket(IAC);
    putc_socket(SE);

    debug("SENT: IAC SB TELOPT_TTYPE IS\"%s\" IAC AE" ,s);

    }

    /*터미널 윈도우 크기*/
    void send_naws()
    {
    char s[50];
    struct winsize size;
    if(ioctl(0, TIOCGWINSZ, (char*)&size)<0)
    {
      perror("ioctl");
      sys_error();
    }
    s[0] = (size.ws_col>>8)&0xFF;
    s[1] = (size.ws_col&0xFF);
    s[2] = (size.ws_row>>8)&0xFF;
    s[3] = (size.ws_row&0xFF);
    s[4] = 0;
    putc_socket(IAC);
    putc_socket(SB);
    putc_socket(TELOPT_NAWS);
    putc_socket(s);
    putc_socket(IAC);
    putc_socket(SE);

    debug("SENT:IAC SB TELOPT_NAWS%d%d%d%dIAC SE", s[0],s[1],s[2],s[3]);
    }

    int option_ok(int opt)
    {
    if(opt == TELOPT_ECHO||opt==TELOPT_BINARY||opt==TELOPT_SGA||
      opt == TELOPT_LFLOW||opt==TELOPT_TTYPE||opt==TELOPT_NAWS)
      return 1;
    else
      return 0;
    }

    void mode_set(int m, int set)
    {
    if(set)
      mode |=m;
    else
      mode &=~m;
    }

    int option_requested(int opt)
    {
    return option[opt]>0;
    }

    void option_request(int opt, int req)
    {
    if(req>0)
      options[opt]++;
    else if(options[opt]>0)
      options[opt]-;

    }

    /*the end of protocol.c*/
    다음은 이 프로그램을 위한 Makefile이다.
    telnet.h, telnet.c, protocol.c를 각각 저장한 뒤 같은 디렉토리에 다음의 Makefile을 저장하여 make하면 된다.

    #Makefile for mtelnet
    CC     = cc
    DEBUG  =
    O_FLAGS=
    C_FLAGS=$(O_FLAGS)-Wall$(DEBUG)
    L_FLAGS=$(O_FLAGS)

    O_FILES = mtelnet
    $(TARGET):$(O_FILES)
             $(CC)$(L_FLAGS)-o$(TARGET)$(O_FILES)

    .c.o:
             $(CC)$-c$(C_FLAGS)$<

    clean:
             rm*.o$(TARGET)

     

7.마치면서

    이 프로그램은 실제적인 어플리케이션에서 쓰이기에는 너무나 버그가 많고, 프로토콜 지원이 빈약하다. 그러나 텔넷 클라이언트 프로그래밍을 이해하기에는 충분하리라 생각된다. 다음 편에서는 소켓을 서버 프로그램의 입장에서 다루어 본다.




▲ top

home으로...