最近学习ssh协议,为了方便,自己先实现一套telnet服务,以便之后套用ssh(自己进了一个深坑)。

    客户端:

    先从telnet客户端做起。这里先给出RFC的中文文档链接:http://oss.org.cn/man/develop/rfc/RFC854.txt 。基本上把telnet介绍得差不多了。但关于NVT的介绍太少,也没有给出一个标准。就比如说键盘的方向键ASCII映射NVT不知道是什么。网上查了很久的资料都没有找到。最终通过自己抓包权威的telnet软件才知道方向键与其他一些按键的NVT ASCII值。现在找到了一些关于NVTASCII的说明文档:http://oss.org.cn/man/develop/rfc/RFC698.txt  就上键来说,getch()收到的值是十进制:224 72 ,转换为ASCII是十进制 27 91 65 (/033[A),下是27 91 66 (/033[B)。因为没找到其他的标准,我就只处理了方向以及退格键的一些必要NVTASCII。

    拿了两个权威的telnet服务器来测试,一个是我的centos服务器的telnet,一个是我的win服务器自带的的telnet。

    键盘的获取使用conio.h下的getch函数,使用这个函数要注意不要阻塞IO,最好是使用_kbhit函数来检测键盘是否有输入再getch,否则可能会出现getch阻塞时,另外线程的IO也会等待getch的结束而阻塞。另外需要注意Ctrl+C、Ctrl+Z之类会直接影响到程序执行过程的输入,需要额外处理。

    客户端连接centos服务器:wKioL1Uc4TXRPK19AAHdWHGCzQo427.jpg

    doc.h 头文件:

#ifndef DOC_H_INCLUDED
#define DOC_H_INCLUDED

#define BS      (char)8
#define LF      (char)10
#define CR      (char)13

#define ESC     (char)27

#define SPACE   (char)32

#define MFLAG   (char)91

#define MARK    (char)224

#define SE      (char)240
#define NOP     (char)241
#define DM      (char)242
#define BRK     (char)243
#define IP      (char)244
#define AO      (char)245
#define AYT     (char)246
#define EC      (char)247
#define EL      (char)248
#define GA      (char)249
#define SB      (char)250
#define WILL    (char)251
#define WONT    (char)252
#define DO      (char)253
#define DON'T   (char)254
#define IAC     (char)255


#endif // DOC_H_INCLUDED

telnet客户端代码:

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <conio.h>
#include <windows.h>
#include <winsock2.h>

#include "doc.h"

//简单的方向键与NVT映射
int mhash[1000];
void init()
{
    mhash[72]='A';
    mhash[80]='B';
    mhash[77]='C';
    mhash[75]='D';;
}

SOCKET sock;//唯一用来通信的SOCKET
HANDLE inputth,outputth;//两个线程句柄
UINT inputthread(LPVOID Param);//输入与发送线程
UINT outputthread(LPVOID Param);//输出与接收线程

int main(int num,char *arr[])//程序的调用参数,接受ip和port
{
    if(num<2)
    {
        puts("no address");
        return false;
    }
    init();
    WSADATA wsadata;
    if(WSAStartup(MAKEWORD(1,0),&wsadata))
    {
        return -1;
    }

    sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sock==INVALID_SOCKET)
    {
        return -1;
    }
    sockaddr_in addr;
    addr.sin_family=AF_INET;
    //绑定端口,默认值23端口
    if(num>2)
    {
        addr.sin_port=htons(atoi(arr[2]));
    }
    else
    {
        addr.sin_port=htons(23);
    }
    //解析域名IP
    hostent *hname=gethostbyname(arr[1]);
    if(!hname)
    {
        puts("can't find address");
        return -1;
    }//puts(inet_ntoa(addr.sin_addr));
    addr.sin_addr.S_un.S_addr=*(u_long *)hname->h_addr_list[0];

    //puts(hname->h_addr_list[0]);
    //puts(inet_ntoa(addr.sin_addr));

    if(SOCKET_ERROR==connect(sock,(sockaddr *)&addr,sizeof(addr)))
    {
        return -1;
    }
    //启动输入线程(先启动便于建立链接)
    inputth=CreateThread(
                 NULL,
                 0,
                 (LPTHREAD_START_ROUTINE)inputthread,
                 NULL,
                 0,
                 NULL
                 );
    //启动输出线程
    outputth=CreateThread(
                 NULL,
                 0,
                 (LPTHREAD_START_ROUTINE)outputthread,
                 NULL,
                 0,
                 NULL
                 );

    DWORD ins=0,ous=0;
    //循环检测telnet结束
    while(1)
    {
        GetExitCodeThread(inputth,&ins);
        GetExitCodeThread(outputth,&ous);
        if(ins!=STILL_ACTIVE||ous!=STILL_ACTIVE)
        {
            puts("\nconnect over");
            break;
        }
        Sleep(1);
    }
    WSACleanup();
    return 0;
}

UINT inputthread(LPVOID Param)
{
    Sleep(500);
    char inchar[10];
    int len;
    int issend;
    int cnt;
    bool ismark=false;
    while(1)
    {
        if(!_kbhit())//检测键盘输入,防止阻塞导致输入冲突
        {
            Sleep(10);
            continue;
        }
        inchar[0]=getch();
        len=1;
        //printf("(%x)",inchar);
        if(inchar[0]==MARK)//识别额外的NVT键盘
        {
            ismark=true;
            continue;
        }
        if(ismark)//处理NVT(方向键)
        {
            if(inchar[0]<0)
            {
                continue;
            }
            inchar[2]=mhash[inchar[0]];
            inchar[0]=ESC;
            inchar[1]=MFLAG;
            len=3;
            ismark=false;
        }
        //printf("%x\n",inchar);
        issend=SOCKET_ERROR;
        cnt=0;
        //发送操作信息
        while(issend==SOCKET_ERROR)
        {
            issend=send(sock,inchar,len,0);
            cnt++;
            if(cnt>100)
            {
                puts("network error please restart");
                break;
            }
            Sleep(100);
        }

    }

    return 0;
}
UINT outputthread(LPVOID Param)
{
    char *rdata=new char [2];
    int rlen;
    bool isiac=false;
    bool issel=false;
    char iac;
    char sel;
    char rebuf[3];
    while(rlen=recv(sock,rdata,1,0))
    {
        if(rlen<=0){continue;}

        //printf("%u\n",(rdata[0]|0xffffff00)^0xffffff00);

        if(rdata[0]==IAC)
        {
            isiac=true;
        }
        else if(isiac==true)
        {
            if(issel==false)
            {
                iac=rdata[0];
                issel=true;
            }
            else
            {
                //处理选项,该处拒绝任何选项
                sel=rdata[0];
                isiac=issel=false;
                rebuf[0]=IAC;
                rebuf[1]=WONT;
                rebuf[2]=sel;
                send(sock,rebuf,3,0);
            }
        }
        else
        {
            //会向数据
            //printf("(%x)",rdata[0]);
            putch(rdata[0]);
        }

    }
    return 0;
}

    


    服务器:

    telnet服务器不能像客户端那样,什么平台一都个模子。因为我是写了用来做ssh实验的,就做了一个win环境下的telnet服务器。

    首先是关于NVT ASCII的问题,这里我就照着之前做客户端的标准,反向映射到键盘。对与命令操作,通过程序识别处理后,再将语句交给操作系统处理,将返回的信息反馈给客户端。

    比如用户要查看服务器的ip地址,客户端输入“ipconfig /all”,服务器识别后将调用函数system("ipconfig /all")。但是一般system的返回数据是直接回显到控制台命令窗口,而我们需要获得这些数据并发送给客户端。如果是在linux系统下,可以使用PIPE管道来将数据流出来获取返回,但是windows要麻烦些(但是IDE是linux比不了的),我们可以将输出定向到文件的方式来获取system返回的数据,由于telnet服务器是允许多个客户端同时访问,所以用文件方式也相对能有效地区分不同链接的输出。

    服务器测试:

    wKiom1Uc5Mfxl9YwAAQVXMdD93M109.jpg

    (没有处理中文字符..)

    写服务器要自己处理很多情况,比如说我要删除一个字符。BS退格,但是不能删除,也没有相应的删除ASCII。这里可以这样处理:先向客户端发送退格,再发送空格(覆盖要删除的字符),再发送退格。这样就实现了删除一个位置的字符。对于上键,需要服务器找到一个上一行命令,把改行发送给客户端。

    客户端代码:

    

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<conio.h>
#include<windows.h>
#include<winsock2.h>
#include<unistd.h>

#include "doc.h"
#define min(x,y) (x<y?x:y)

int mhash[1000];
void init()
{
    mhash['A']=72;
    mhash['B']=80;
    mhash['C']=77;
    mhash['D']=75;
    system("md tmp");
    //重定向输出
    freopen("tmp\\log","w+",stdout);
}

SOCKET ssock;
HANDLE listenth;
UINT listenthread(LPVOID Param);
UINT dealthread(LPVOID Param);
UINT dealid;

int main()
{
    init();

    WSAData wsadata;
    if(WSAStartup(MAKEWORD(1,0),&wsadata))
    {
        return -1;
    }

    //创建监听线程
    listenth=CreateThread(
                    NULL,
                    0,
                    (LPTHREAD_START_ROUTINE)listenthread,
                    NULL,
                    0,
                    NULL
                    );
    DWORD des;
    //检测线程结束
    while(1)
    {
        GetExitCodeThread(listenth,&des);
        if(des!=STILL_ACTIVE)
        {
            puts("\nconnect over");
            break;
        }
        Sleep(1);
    }

    WSACleanup();
    return 0;
}

UINT listenthread(LPVOID Param)
{
    ssock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(ssock==INVALID_SOCKET)
    {
        return -1;
    }
    sockaddr_in addr;
    addr.sin_family=AF_INET;
    //监听23端口
    addr.sin_port=htons(23);
    addr.sin_addr.S_un.S_addr=INADDR_ANY;
    if(bind(ssock,(sockaddr*)&addr,sizeof(sockaddr))==SOCKET_ERROR)
    {
        printf("bind error\n");
        return -1;
    }
    dealid=0;//telnet处理编号
    if(SOCKET_ERROR==listen(ssock,10))
    {
        printf("listen error\n");
        return -1;
    }

    int newlen=sizeof(sockaddr);
    sockaddr_in newaddr;
    //接收并创建独立线程处理新连接
    while(ssock!=SOCKET_ERROR)
    {
        SOCKET newsock=accept(ssock,(sockaddr*)&newaddr,&newlen);
        CreateThread(
                    NULL,
                    0,
                    (LPTHREAD_START_ROUTINE)dealthread,
                    &newsock,
                    0,
                    NULL
                    );
    }
    return 0;
}

//发送系统返回值
void sysreply(SOCKET sock,char *dir)
{
    //读取返回文件
    FILE *f=fopen(dir+3,"r");
    if(f==NULL)
    {
        return ;
    }
    fseek(f,0,SEEK_END);
    int len=ftell(f);
    fseek(f,0,SEEK_SET);
    char *rdata=new char[len+1];
    fread(rdata,len,1,f);
    int slen=0;
    int cnt=0;
    //将返回文件的值全部发送
    while(slen<len)
    {
        slen+=send(sock,rdata+slen,min(512,len-slen),0);
        cnt++;
        if(cnt>len)
        {
            return;
        }
    }
}
UINT dealthread(LPVOID Param)
{
    SOCKET sock=*(SOCKET *)Param;
    send(sock,"welcome my telnet server\n",25,0);

    //将系统返回值全定向到对应文件输出
    char dir[10]=" > tmp\\";
    dir[7]=(dealid+'0');
    dealid++;
    dir[8]='\0';

    char data;
    int len;
    char sysdata[1024];
    int syslen=0;

    bool isesc=false;
    bool ismflag=false;

    while(sock!=SOCKET_ERROR)
    {
        len=recv(sock,&data,1,0);
        if(len<1)
        {
            continue;
        }
        if(data==BS)//处理退格(前移光标、空格、前移光标)
        {
            if(syslen>0)
            {
                syslen--;
            }
            char rdata[]={BS,SPACE,BS};
            send(sock,rdata,3,0);
        }
        else if(data==CR)//处理提交
        {
            send(sock,"\n",1,0);
            if(syslen==0)
            {
                continue;
            }
            sysdata[syslen]='\0';
            if(strcmp(sysdata,"exit")==0)//退出telnet
            {
                send(sock,"exit",4,0);
                break;
            }
            strcat(sysdata,dir);
            //puts(sysdata);
            syslen=0;
            if(system(sysdata)!=0)
            {
                send(sock,"error\n",6,0);
                continue;
            }
            sysreply(sock,dir);
        }
        else if(data==ESC)
        {
            isesc=true;
        }
        else if(isesc==true||data==MFLAG)
        {
            ismflag=true;
            isesc=false;
        }
        else if(ismflag==true)//处理额外的NVT值
        {
            ismflag=false;
            if(data<0||data>1000)
            {
                continue;
            }
            sysdata[0]=MARK;
            sysdata[1]=mhash[data];
            sysdata[2]='\0';
            strcat(sysdata,dir);

            syslen=0;
            sysreply(sock,dir);
        }
        else//新数据进栈
        {
            sysdata[syslen++]=data;
            send(sock,&data,1,0);
        }
        //printf("%c (%d) (%x)\n",data,data,data);
    }

    //删除临时文件
    strcpy(sysdata,"rm ");
    strcat(sysdata,dir+3);
    system(sysdata);

    return 0;
}


    一套马马虎虎的telnet就完成了。

    代码下载