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可以解決該問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370