一错蝴、SSH簡(jiǎn)介
SSH(Secure Shell)屬于在傳輸層上運(yùn)行的用戶層協(xié)議斗锭,相對(duì)于Telnet來(lái)說(shuō)具有更高的安全性草则。SSH是專為遠(yuǎn)程登錄會(huì)話和其他網(wǎng)絡(luò)服務(wù)提供安全性的協(xié)議屁桑。利用 SSH 協(xié)議可以有效防止遠(yuǎn)程管理過(guò)程中的信息泄露問(wèn)題庙洼。SSH最初是UNIX系統(tǒng)上的一個(gè)程序陨界,后來(lái)又迅速擴(kuò)展到其他操作平臺(tái)巡揍。SSH在正確使用時(shí)可彌補(bǔ)網(wǎng)絡(luò)中的漏洞。SSH客戶端適用于多種平臺(tái)菌瘪。幾乎所有UNIX平臺(tái)—包括HP-UX腮敌、Linux、AIX俏扩、Solaris糜工、Digital UNIX、Irix录淡,以及其他平臺(tái)啤斗,都可運(yùn)行SSH。
二赁咙、SSH遠(yuǎn)程連接
SSH遠(yuǎn)程連接有兩種方式钮莲,一種是通過(guò)用戶名和密碼直接登錄,另一種則是用過(guò)密鑰登錄彼水。
1崔拥、用戶名和密碼登錄
老王要在自己的主機(jī)登錄老張的電腦,他可以通過(guò)運(yùn)行以下代碼來(lái)實(shí)現(xiàn)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 跳過(guò)了遠(yuǎn)程連接中選擇‘是’的環(huán)節(jié),
ssh.connect('IP', 22, '用戶名', '密碼')
stdin, stdout, stderr = ssh.exec_command('df') print stdout.read()
?
????????在這里要用到paramiko模塊凤覆,這是一個(gè)第三方模塊链瓦,要自自己導(dǎo)入(要想使用paramiko模塊,還要先導(dǎo)入pycrypto模塊才能用)。
查看并啟動(dòng)ssh服務(wù)
service ssh status
添加用戶:useradd -d /home/zet zet
passwd zet
賦予ssh權(quán)限
vi /etc/ssh/sshd_config
添加
AllowUsers:zet
2慈俯、密鑰登錄
???????老王要在自己的主機(jī)登錄老張的電腦渤刃,老王用命令ssh.keygen -t rsa生成公鑰和私鑰,他將自己的公鑰發(fā)給老張贴膘,使用ssh-copy-id -i ~/ssh/id_rsa.pub laozhang@IP命令
然后運(yùn)行以下代碼來(lái)實(shí)現(xiàn)
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()
???????關(guān)于密鑰登錄卖子,每個(gè)人都有一個(gè)公鑰,一個(gè)私鑰刑峡,公鑰是給別人的洋闽,私鑰是自己留著,只有自己的私鑰能解開(kāi)自己公鑰加密的文件突梦。
???????老王有一個(gè)機(jī)密文件要發(fā)給老張诫舅,就要先下載老張的公鑰進(jìn)行加密,這樣老張就能用自己私鑰解開(kāi)這份機(jī)密文件宫患,獲得內(nèi)容刊懈。
???????如果老張要確認(rèn)是否是老王本人給他的機(jī)密文件,就去下載一個(gè)老王的公鑰娃闲,隨機(jī)寫(xiě)一些字符虚汛,用老王的公鑰加密,發(fā)給老王畜吊,老王解密之后發(fā)回給老張,如果老張收到的解密后的字母和自己發(fā)出去的一樣户矢,對(duì)方就是老王無(wú)疑了玲献。
三、使用SSH連接服務(wù)器
客戶端代碼:
#-*- 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()) #設(shè)置自動(dòng)添加和保存目標(biāo)ssh服務(wù)器的ssh密鑰
client.connect(ip, port, username=user, password=passwd) #連接
ssh_session = client.get_transport().open_session() #打開(kāi)會(huì)話
if ssh_session.active:
ssh_session.send(command) #發(fā)送command這個(gè)字符串梯浪,并不是執(zhí)行命令
print ssh_session.recv(1024) #返回命令執(zhí)行結(jié)果(1024個(gè)字符)
while True:
command = ssh_session.recv(1024) #從ssh服務(wù)器獲取命令
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)
服務(wù)端代碼:
#-*- 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設(shè)置為1捌年,表示將SO_REUSEADDR標(biāo)記為TRUE,操作系統(tǒng)會(huì)在服務(wù)器socket被關(guān)閉或服務(wù)器進(jìn)程終止后馬上釋放該服務(wù)器的端口挂洛,否則操作系統(tǒng)會(huì)保留幾分鐘該端口礼预。
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8001)) #綁定ip和端口
sock.listen(100) #最大連接數(shù)為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) #設(shè)置超時(shí)值為20
print '[+] Authenticated!'
print chan.recv(1024)
chan.send("Welcome to bh_ssh")
while True:
try:
command = raw_input("Enter command:").strip("\n") #strip移除字符串頭尾指定的字符(默認(rèn)為空格),這里是換行
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)
四、接下來(lái)是了解一下進(jìn)程的創(chuàng)建過(guò)程虏劲,用最原始的方式實(shí)現(xiàn)了一個(gè)ssh shell命令的執(zhí)行托酸。
#coding=utf8
'''
用python實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的shell,了解進(jìn)程創(chuàng)建
類unix 環(huán)境下 fork和exec 兩個(gè)系統(tǒng)調(diào)用完成進(jìn)程的創(chuàng)建
'''
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 # 返回值是一個(gè)16位的二進(jìn)制數(shù)字柒巫,高8位為退出狀態(tài)碼励堡,低8位為程序結(jié)束系統(tǒng)信號(hào)的編號(hào)
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)
一個(gè)SSH項(xiàng)目,需要在客戶端集成一個(gè)交互式ssh功能堡掏,大概就是客戶端跟服務(wù)器申請(qǐng)個(gè)可用的機(jī)器应结,服務(wù)端返回個(gè)ip,端口,密碼鹅龄, 然后客戶端就可以直接登錄到機(jī)器上操做了揩慕。該程序基于paramiko模塊。
???????經(jīng)查找扮休,從paramiko的源碼包demos目錄下迎卤,可以看到交互式shell的實(shí)現(xiàn),就是那個(gè)demo.py肛炮。但是用起來(lái)有些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) # 終端大小改變時(shí)碍扔,修改pty終端大小
stdin = os.fdopen(sys.stdin.fileno(), 'r', 0) # stdin buff置為空,否則粘貼多字節(jié)或者按方向鍵的時(shí)候顯示不正確
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信號(hào)將休眠的select系統(tǒng)調(diào)用喚醒引發(fā)的系統(tǒng)中斷秕重,忽略中斷重新調(diào)用解決不同。
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)
#關(guān)閉連接
channel.close()
ssh.close()
interactive.py代碼中主要修復(fù)了幾個(gè)問(wèn)題:
1、當(dāng)讀取鍵盤輸入時(shí)溶耘,方向鍵會(huì)有問(wèn)題二拐,因?yàn)榘匆淮畏较蜴I會(huì)產(chǎn)生3個(gè)字節(jié)數(shù)據(jù),我的理解是按鍵一次會(huì)被select捕捉一次標(biāo)準(zhǔn)輸入有變化凳兵,但是我每次只處理1個(gè)字節(jié)的數(shù)據(jù)百新,其他的數(shù)據(jù)會(huì)存放在輸入緩沖區(qū)中,等待下次按鍵的時(shí)候一起發(fā)過(guò)去。這就導(dǎo)致了本來(lái)3個(gè)字節(jié)才能完整定義一個(gè)方向鍵的行為逐样,但是我只發(fā)過(guò)去一個(gè)字節(jié)翁涤,所以終端并不知道我要干什么。所以沒(méi)有變化铅辞,當(dāng)下次觸發(fā)按鍵,才會(huì)把上一次的信息完整發(fā)過(guò)去萨醒,看起來(lái)就是按一下方向鍵有延遲斟珊。多字節(jié)的粘貼也是一個(gè)原理。解決辦法是將輸入緩沖區(qū)置為0富纸,這樣就沒(méi)有緩沖囤踩,有多少發(fā)過(guò)去多少,這樣就不會(huì)有那種顯示的延遲問(wèn)題了晓褪。
2高职、終端大小適應(yīng)。paramiko.channel會(huì)創(chuàng)建一個(gè)pty(偽終端)辞州,有個(gè)默認(rèn)的大姓俊(width=80, height=24),所以登錄過(guò)去會(huì)發(fā)現(xiàn)能顯示的區(qū)域很小,并且是固定的埃元。編輯vim的時(shí)候尤其痛苦涝涤。channel中有resize_pty方法,但是需要獲取到當(dāng)前終端的大小岛杀。經(jīng)查找阔拳,當(dāng)終端窗口發(fā)生變化時(shí),系統(tǒng)會(huì)給前臺(tái)進(jìn)程組發(fā)送SIGWINCH信號(hào)类嗤,也就是當(dāng)進(jìn)程收到該信號(hào)時(shí)糊肠,獲取一下當(dāng)前size,然后再同步到pty中遗锣,那pty中的進(jìn)程等于也感受到了窗口變化货裹,也會(huì)收到SIGWINCH信號(hào)。
3精偿、讀寫(xiě)‘慢’設(shè)備(包括pipe弧圆,終端設(shè)備,網(wǎng)絡(luò)連接等)笔咽。讀時(shí)搔预,數(shù)據(jù)不存在,需要等待叶组;寫(xiě)時(shí)拯田,緩沖區(qū)滿或其他原因,需要等待甩十。ssh通道屬于這一類的船庇。本來(lái)進(jìn)程因?yàn)榫W(wǎng)絡(luò)沒(méi)有通信,select調(diào)用為阻塞中的狀態(tài)枣氧,但是當(dāng)終端窗口大小變化溢十,接收到SIGWINCH信號(hào)被喚醒垮刹。此時(shí)select會(huì)出現(xiàn)異常达吞,觸發(fā)系統(tǒng)中斷(4, 'Interrupted system call'),但是這種情況只會(huì)出現(xiàn)一次荒典,當(dāng)重新調(diào)用select方法又會(huì)恢復(fù)正常酪劫。所以捕獲到select異常后重新進(jìn)行select可以解決該問(wèn)題。