Python - 使用SSH遠程登錄

一、SSH簡介

SSH(Secure Shell)屬于在傳輸層上運行的用戶層協議,相對于Telnet來說具有更高的安全性。SSH是專為遠程登錄會話和其他網絡服務提供安全性的協議。利用 SSH 協議可以有效防止遠程管理過程中的信息泄露問題。SSH最初是UNIX系統上的一個程序,后來又迅速擴展到其他操作平臺。SSH在正確使用時可彌補網絡中的漏洞。SSH客戶端適用于多種平臺。幾乎所有UNIX平臺—包括HP-UXLinuxAIXSolarisDigital UNIXIrix,以及其他平臺,都可運行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可以解決該問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。