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