使用ssh2实现shell自动化测试,实际工作中遇到非常多。各种语言都有相应的库可以使用。比如,c/c++语言可以使用libssh2;python可以使用paramkio库等。但这些库网上的帮助,都不是很全,都比较浅显。shell自动化,最基本的三个需求,一个是适合多重类型的操作系统;二是要能够支持交互式shell,比如使用sudo执行时,需要输入密码;三是读数据时要非阻塞的。
paramkio好像不支持交互式(shell命令不需要再根据输出输入不同的参数,实际上,这种情况遇到非常多),而且paramkio在windows上也很不好用,要实现非阻塞,找了几天都没能找到好的办法。
libssh2表面上不支持交互式,官方的例子也没有针对交互式shell进行举例,给库的使用者带来很大的困扰,比较难入门。实际上libssh2库是支持交互式shell的,而且支持多种OS,实现起来也相当的简单,只是过程摸索过于痛苦。关于libssh2的编译,也比较简单,这里就不列举了。libssh2是c语言的,样例写起来很繁琐,因此我用c++进行封装,学习起来比较直观。
实际libssh2两个官方的样例scp.c和scp_echo.c已经提供了交互式shell的实现方式,只是scp_echo.c有关ssh2登陆认证方式,没有像scp.c使用了ssh2的键盘交互式方式,一般ssh2服务器默认提供这个认证方式。scp_echo.c不容易调试通过。
首先,先看一下实现Ssh2类的使用代码:
#include <iostream> #include "ssh2.h" int main(int argc, const char * argv[]) { using namespace std; using namespace fish; Ssh2 ssh("127.0.0.1"); ssh.Connect("test","xxxxxx"); Channel* channel = ssh.CreateChannel(); channel->Write("cd /;pwd"); cout<<channel->Read()<<endl; channel->Write("ssh 127.0.0.1"); cout<<channel->Read(":")<<endl; channel->Write("xxxxxx"); cout<<channel->Read()<<endl; channel->Write("pwd"); cout<<channel->Read()<<endl; delete channel; return 0; }
读写都是非阻塞的,这个实际使用非常方便。代码就不详细讲解了。
Ssh2类的实现代码如下:
class Ssh2 { public: Ssh2(const string &srvIp, int srvPort=22 ); ~Ssh2(void); bool Connect( const string &userName, const string &userPwd); bool Disconnect(void); Channel* CreateChannel(const string &ptyType = "vanilla"); public: static void S_KbdCallback(const char*, int, const char*, int, int, const LIBSSH2_USERAUTH_KBDINT_PROMPT*, LIBSSH2_USERAUTH_KBDINT_RESPONSE*, void **a ); static string s_password; private: string m_srvIp; int m_srvPort; string m_userName; string m_password; int m_sock; LIBSSH2_SESSION *m_session; };
由于ssh2服务器要通过键盘交互式是通过回调函数实现认证的,所以使用了两个静态成员,一个是成员函数,一个是成员变量(当然,这种实现方式比较丑,暂时没有找到好的方法)。这里代码,没有考虑到多线程安全,因为实际需求不是很严格。当然,要支持多线程下ssh2登陆认证,也很容易实现。
string Ssh2::s_password; void Ssh2::S_KbdCallback(const char *name, int name_len, const char *instruction, int instruction_len, int num_prompts, const LIBSSH2_USERAUTH_KBDINT_PROMPT *prompts, LIBSSH2_USERAUTH_KBDINT_RESPONSE *responses, void **abstract) { (void)name; (void)name_len; (void)instruction; (void)instruction_len; if (num_prompts == 1) { responses[0].text = strdup(s_password.c_str()); responses[0].length = (int)s_password.size(); } (void)prompts; (void)abstract; } Ssh2::Ssh2(const string &srvIp, int srvPort) :m_srvIp(srvIp),m_srvPort(srvPort) { m_sock = -1; m_session = NULL; libssh2_init(0); } Ssh2::~Ssh2(void) { Disconnect(); libssh2_exit(); } bool Ssh2::Connect(const string &userName, const string &userPwd) { m_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(22); sin.sin_addr.s_addr = inet_addr(m_srvIp.c_str()); if ( connect( m_sock, (sockaddr*)(&sin), sizeof(sockaddr_in) ) != 0 ) { Disconnect(); return false; } m_session = libssh2_session_init(); if ( libssh2_session_handshake(m_session, m_sock) ) { Disconnect(); return false; } int auth_pw = 0; string fingerprint = libssh2_hostkey_hash(m_session, LIBSSH2_HOSTKEY_HASH_SHA1); string userauthlist = libssh2_userauth_list( m_session, userName.c_str(), (int)userName.size() ); if ( strstr( userauthlist.c_str(), "password") != NULL ) { auth_pw |= 1; } if ( strstr( userauthlist.c_str(), "keyboard-interactive") != NULL ) { auth_pw |= 2; } if ( strstr(userauthlist.c_str(), "publickey") != NULL) { auth_pw |= 4; } if (auth_pw & 1) { /* We could authenticate via password */ if ( libssh2_userauth_password(m_session, userName.c_str(), userPwd.c_str() ) ) { Disconnect(); return false; } } else if (auth_pw & 2) { /* Or via keyboard-interactive */ s_password = userPwd; if (libssh2_userauth_keyboard_interactive(m_session, userName.c_str(), &S_KbdCallback) ) { Disconnect(); return false; } } else { Disconnect(); return false; } return true; } bool Ssh2::Disconnect(void) { if( m_session ) { libssh2_session_disconnect(m_session, "Bye bye, Thank you"); libssh2_session_free(m_session); m_session = NULL; } if( m_sock != -1 ) { #ifdef WIN32 closesocket(m_sock); #else close(m_sock); #endif m_sock = -1; } return true; } Channel* Ssh2::CreateChannel(const string &ptyType) { if( NULL == m_session ) { return NULL; } LIBSSH2_CHANNEL *channel = NULL; /* Request a shell */ if ( !(channel = libssh2_channel_open_session(m_session)) ) { return NULL; } /* Request a terminal with 'vanilla' terminal emulation * See /etc/termcap for more options */ if ( libssh2_channel_request_pty(channel, ptyType.c_str() ) ) { libssh2_channel_free(channel); return NULL; } /* Open a SHELL on that pty */ if ( libssh2_channel_shell(channel) ) { libssh2_channel_free(channel); return NULL; } Channel *ret = new Channel(channel); cout<<ret->Read()<<endl; return ret; }
这里要关注两个函数调用,一个是libssh2_channel_request_pty指定channel类型,如果使用xterm,就可以使用彩色显示。libssh2_channel_shell用来指定是交互式shell。
下面是Channel类的定义:
const int CHANNEL_READ_TIMEOUT = 3000; class Channel { public: Channel(LIBSSH2_CHANNEL *channel); ~Channel(void); string Read( const string &strend = "$", int timeout = CHANNEL_READ_TIMEOUT ); bool Write(const string &data); private: Channel(const Channel&); Channel operator=(const Channel&); private: LIBSSH2_CHANNEL *m_channel; };
Channel类的实现:
Channel::Channel( LIBSSH2_CHANNEL*channel) :m_channel(channel) { } Channel::~Channel(void) { if (m_channel) { libssh2_channel_free(m_channel); m_channel = NULL; } } string Channel::Read( const string &strend, int timeout ) { string data; if( NULL == m_channel ) { return data; } LIBSSH2_POLLFD *fds = new LIBSSH2_POLLFD; fds->type = LIBSSH2_POLLFD_CHANNEL; fds->fd.channel = m_channel; fds->events = LIBSSH2_POLLFD_POLLIN | LIBSSH2_POLLFD_POLLOUT; if( timeout % 50 ) { timeout += timeout %50; } while(timeout>0) { int rc = (libssh2_poll(fds, 1, 10)); if (rc < 1) { timeout -= 50; usleep(50*1000); continue; } if ( fds->revents & LIBSSH2_POLLFD_POLLIN ) { char buffer[64*1024] = ""; size_t n = libssh2_channel_read( m_channel, buffer, sizeof(buffer) ); if ( n == LIBSSH2_ERROR_EAGAIN ) { //fprintf(stderr, "will read again\n"); } else if (n <= 0) { return data; } else { data += string(buffer); if( "" == strend ) { return data; } size_t pos = data.rfind(strend); if( pos != string::npos && data.substr(pos, strend.size()) == strend ) { return data; } } } timeout -= 50; usleep(50*1000); } cout<<"read data timeout"<<endl; return data; } bool Channel::Write(const string &data) { if( NULL == m_channel ) { return false; } string send_data = data + "\n"; return libssh2_channel_write_ex( m_channel, 0, send_data.c_str(), send_data.size() ) == data.size(); //return true; }
这里主要关注libssh2_poll函数,主要是检测通道是否有数据可以读。read函数简单做了封装,增加期望结果,有超时。Ssh2是支持多个Channel读写的,可以同时打个多个Channel,分别执行不同的操作,对于ssh2有登陆数限制时,还是很相当有用的。目前Ssh2客户端基本可用了。大家不妨试试。有关libssh2库再简单的例子,估计网上还没有吧,当然,有关libssh2安装,网上是有一大把。
用到的头文件包含如下:
#ifdef WIN32 #include <windows.h> #include <winsock2.h> #else #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #endif #include <sys/types.h> #include <fcntl.h> #include <errno.h> #include <stdio.h> #include <ctype.h> #include <libssh2.h> #include <libssh2_sftp.h>
c++比较复杂,也想在python中使用libssh2的便利性,扩展python语言。实现起来,当然要复杂一些。但复杂度也很低,只需要使用boost::python类,扩展python就行了。windows和linux下,都不复杂。后续有时间再整理。