Python - 使用SSH远程登录

一、SSH简介

SSH(Secure Shell)属于在传输层上运行的用户层协议,相对于Telnet来说具有更高的安全性。SSH是专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH最初是UNIX系统上的一个程序,后来又迅速扩展到其他操作平台。SSH在正确使用时可弥补网络中的漏洞。SSH客户端适用于多种平台。几乎所有UNIX平台—包括HP-UX、Linux、AIX、Solaris、Digital UNIX、Irix,以及其他平台,都可运行SSH。

二、SSH远程连接

SSH远程连接有两种方式,一种是通过用户名和密码直接登录,另一种则是用过密钥登录。

1、用户名和密码登录

老王要在自己的主机登录老张的电脑,他可以通过运行以下代码来实现

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 跳过了远程连接中选择‘是’的环节,
ssh.connect('IP', 22, '用户名', '密码')
stdin, stdout, stderr = ssh.exec_command('df') print stdout.read()

 
        在这里要用到paramiko模块,这是一个第三方模块,要自自己导入(要想使用paramiko模块,还要先导入pycrypto模块才能用)。

查看并启动ssh服务
service ssh status

添加用户:useradd -d /home/zet zet
passwd zet
赋予ssh权限
vi /etc/ssh/sshd_config
添加
AllowUsers:zet

2、密钥登录

       老王要在自己的主机登录老张的电脑,老王用命令ssh.keygen -t rsa生成公钥和私钥,他将自己的公钥发给老张,使用ssh-copy-id -i ~/ssh/id_rsa.pub laozhang@IP命令

然后运行以下代码来实现

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 
ssh.connect('IP', 22, '用户名', key)
stdin, stdout, stderr = ssh.exec_command('df') print stdout.read()

       关于密钥登录,每个人都有一个公钥,一个私钥,公钥是给别人的,私钥是自己留着,只有自己的私钥能解开自己公钥加密的文件。

       老王有一个机密文件要发给老张,就要先下载老张的公钥进行加密,这样老张就能用自己私钥解开这份机密文件,获得内容。

       如果老张要确认是否是老王本人给他的机密文件,就去下载一个老王的公钥,随机写一些字符,用老王的公钥加密,发给老王,老王解密之后发回给老张,如果老张收到的解密后的字母和自己发出去的一样,对方就是老王无疑了。

三、使用SSH连接服务器

客户端代码:

#-*- coding:utf8 -*-
 
import threading
import paramiko
import subprocess
 
def ssh_command(ip, user, passwd, command, port = 22):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())    #设置自动添加和保存目标ssh服务器的ssh密钥
    client.connect(ip, port, username=user, password=passwd)  #连接
    ssh_session = client.get_transport().open_session() #打开会话
    if ssh_session.active:
        ssh_session.send(command)   #发送command这个字符串,并不是执行命令
        print ssh_session.recv(1024)    #返回命令执行结果(1024个字符)
        while True:
            command = ssh_session.recv(1024)    #从ssh服务器获取命令
            try:
                cmd_output = subprocess.check_output(command, shell=True)
                ssh_session.send(cmd_output)
            except Exception, e:
                ssh_session.send(str(e))
        client.close()
    return
 
ssh_command('127.0.0.1', 'zet', 'zet', 'clientconnected',8001)

服务端代码:

#-*- coding:utf8 -*-
 
import socket
import paramiko
import threading
import sys
 
# 使用 Paramiko示例文件的密钥
#host_key = paramiko.RSAKey(filename='test_rsa.key')
host_key = paramiko.RSAKey(filename='/root/.ssh/id_rsa')
 
class Server(paramiko.ServerInterface):
    def __init__(self):
        self.event = threading.Event()
    def check_channel_request(self, kind, chanid):
        if kind == 'session':
             return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
    def check_auth_password(self, username, password):
        if (username == 'qing') and (password == 'qing'):
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED
 
server = sys.argv[1]
ssh_port = int(sys.argv[2])
try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    #TCP socket
    #这里value设置为1,表示将SO_REUSEADDR标记为TRUE,操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', 8001))   #绑定ip和端口
    sock.listen(100)    #最大连接数为100
    print '[+] Listening for connection ...'
    client, addr = sock.accept()
except Exception, e:
    print '[-] Listen failed: ' + str(e)
    sys.exit(1)
print '[+] Got a connection!'
 
try:
    bhSession = paramiko.Transport(client)
    bhSession.add_server_key(host_key)
    server = Server()
    try:
        bhSession.start_server(server=server)
    except paramiko.SSHException, x:
        print '[-] SSH negotiation failed'
    chan = bhSession.accept(20) #设置超时值为20
    print '[+] Authenticated!'
    print chan.recv(1024)
    chan.send("Welcome to bh_ssh")
    while True:
        try:
            command = raw_input("Enter command:").strip("\n")   #strip移除字符串头尾指定的字符(默认为空格),这里是换行
            if command != 'exit':
                chan.send(command)
                print chan.recv(1024) + '\n'
            else:
                chan.send('exit')
                print 'exiting'
                bhSession.close()
                raise Exception('exit')
        except KeyboardInterrupt:
            bhSession.close()
except Exception, e:
    print '[-] Caught exception: ' + str(e)
    try:
        bhSession.close()
    except:
        pass
    sys.exit(1)

四、接下来是了解一下进程的创建过程,用最原始的方式实现了一个ssh shell命令的执行。

#coding=utf8
 
'''
用python实现了一个简单的shell,了解进程创建
类unix 环境下 fork和exec 两个系统调用完成进程的创建
'''
 
import sys, os
 
 
def myspawn(cmdline):
    argv = cmdline.split()
    if len(argv) == 0:
        return 
    program_file = argv[0]
    pid = os.fork()
    if pid < 0:
        sys.stderr.write("fork error")
    elif pid == 0:
        # child
        os.execvp(program_file, argv)
        sys.stderr.write("cannot exec: "+ cmdline)
        sys.exit(127)
    # parent
    pid, status = os.waitpid(pid, 0)
    ret = status >> 8  # 返回值是一个16位的二进制数字,高8位为退出状态码,低8位为程序结束系统信号的编号
    signal_num = status & 0x0F
    sys.stdout.write("ret: %s, signal: %s\n" % (ret, signal_num))
    return ret
 
 
def ssh(host, user, port=22, password=None):
    if password:
        sys.stdout.write("password is: '%s' , plz paste it into ssh\n" % (password))
    cmdline = "ssh %s@%s -p %s " % (user, host, port)
    ret = myspawn(cmdline)
 
 
if __name__ == "__main__":
    host = ''
    user = ''
    password = ''
    ssh(host, user, password=password)


一个SSH项目,需要在客户端集成一个交互式ssh功能,大概就是客户端跟服务器申请个可用的机器,服务端返回个ip,端口,密码, 然后客户端就可以直接登录到机器上操做了。该程序基于paramiko模块。

       经查找,从paramiko的源码包demos目录下,可以看到交互式shell的实现,就是那个demo.py。但是用起来有些bug,于是我给修改了一下interactive.py(我把windows的代码删掉了,剩下的只能在linux下用)。代码如下:

#coding=utf-8
import socket
import sys
import os
import termios
import tty
import fcntl
import signal
import struct
import select
 
now_channel = None
 
def interactive_shell(chan):
    posix_shell(chan)
 
 
def ioctl_GWINSZ(fd):
    try:
        cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,'aaaa'))
    except:
        return
    return cr
 
 
def getTerminalSize():
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
    return int(cr[1]), int(cr[0])
    
 
def resize_pty(signum=0, frame=0):
    width, height = getTerminalSize()
    if now_channel is not None:
        now_channel.resize_pty(width=width, height=height)
 
 
 
def posix_shell(chan):
    global now_channel
    now_channel = chan
    resize_pty()
    signal.signal(signal.SIGWINCH, resize_pty) # 终端大小改变时,修改pty终端大小
    stdin = os.fdopen(sys.stdin.fileno(), 'r', 0) # stdin buff置为空,否则粘贴多字节或者按方向键的时候显示不正确
    fd = stdin.fileno()
    oldtty = termios.tcgetattr(fd)
    newtty = termios.tcgetattr(fd)
    newtty[3] = newtty[3] | termios.ICANON
    try:
        termios.tcsetattr(fd, termios.TCSANOW, newtty)
        tty.setraw(fd)
        tty.setcbreak(fd)
        chan.settimeout(0.0)
        while True:
            try:
                r, w, e = select.select([chan, stdin], [], [])
            except:
                # 解决SIGWINCH信号将休眠的select系统调用唤醒引发的系统中断,忽略中断重新调用解决。
                continue
            if chan in r:
                try:
                    x = chan.recv(1024)
                    if len(x) == 0:
                        print 'rn*** EOFrn',
                        break
                    sys.stdout.write(x)
                    sys.stdout.flush()
                except socket.timeout:
                    pass
            if stdin in r:
                x = stdin.read(1)
                if len(x) == 0:
                    break
                chan.send(x)
    finally:
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)

使用示例:

#coding=utf8
import paramiko
import interactive
 
 
#记录日志
paramiko.util.log_to_file('/tmp/aaa')
#建立ssh连接
ssh=paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('192.168.1.11',port=22,username='hahaha',password='********',compress=True)
 
#建立交互式shell连接
channel=ssh.invoke_shell()
#建立交互式管道
interactive.interactive_shell(channel)
#关闭连接
channel.close()
ssh.close()
interactive.py代码中主要修复了几个问题:

1、当读取键盘输入时,方向键会有问题,因为按一次方向键会产生3个字节数据,我的理解是按键一次会被select捕捉一次标准输入有变化,但是我每次只处理1个字节的数据,其他的数据会存放在输入缓冲区中,等待下次按键的时候一起发过去。这就导致了本来3个字节才能完整定义一个方向键的行为,但是我只发过去一个字节,所以终端并不知道我要干什么。所以没有变化,当下次触发按键,才会把上一次的信息完整发过去,看起来就是按一下方向键有延迟。多字节的粘贴也是一个原理。解决办法是将输入缓冲区置为0,这样就没有缓冲,有多少发过去多少,这样就不会有那种显示的延迟问题了。

2、终端大小适应。paramiko.channel会创建一个pty(伪终端),有个默认的大小(width=80, height=24),所以登录过去会发现能显示的区域很小,并且是固定的。编辑vim的时候尤其痛苦。channel中有resize_pty方法,但是需要获取到当前终端的大小。经查找,当终端窗口发生变化时,系统会给前台进程组发送SIGWINCH信号,也就是当进程收到该信号时,获取一下当前size,然后再同步到pty中,那pty中的进程等于也感受到了窗口变化,也会收到SIGWINCH信号。

3、读写‘慢’设备(包括pipe,终端设备,网络连接等)。读时,数据不存在,需要等待;写时,缓冲区满或其他原因,需要等待。ssh通道属于这一类的。本来进程因为网络没有通信,select调用为阻塞中的状态,但是当终端窗口大小变化,接收到SIGWINCH信号被唤醒。此时select会出现异常,触发系统中断(4, 'Interrupted system call'),但是这种情况只会出现一次,当重新调用select方法又会恢复正常。所以捕获到select异常后重新进行select可以解决该问题。

你可能感兴趣的:(Python - 使用SSH远程登录)