最近学习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服务器:
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服务器是允许多个客户端同时访问,所以用文件方式也相对能有效地区分不同链接的输出。
服务器测试:
(没有处理中文字符..)
写服务器要自己处理很多情况,比如说我要删除一个字符。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就完成了。
代码下载