1、模擬sshpython
一、subprocess模塊linux
subprocess模塊是python從2.4版本開始引入的模塊。主要用來取代 一些舊的模塊方法,如os.system、os.spawn*、os.popen*、commands.*等。subprocess模塊可用於產生進程,並鏈接到進程的輸入/輸出/錯誤輸出管道,並獲取進程的返回值。算法
import subprocess res = subprocess.Popen("dir", shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) # PIPE表示的是管道 # stdin,stdout,stderr:分別表示程序的標準輸入、標準輸出、標準錯誤。 print(res.stdout.read().decode("gbk")) # 結果的編碼是以當前所在的系統爲準的,若是是windows, # 那麼res.stdout.read()讀出的就是GBK編碼的,在接收端須要用GBK解碼
二、寫一個模擬ssh的簡單程序,即實現客戶端向服務端發送一條終端命令,服務端執行這條命令後將執行結果返回給客戶端,以下示例:shell
import socket import subprocess server = socket.socket() server.bind(('127.0.0.1',8008)) server.listen(5) conn,addr = server.accept() while 1: data = conn.recv(1024).decode('utf8') res = subprocess.Popen(data, shell = True, stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out_result = res.stdout.read() # 發生錯誤後結果 err_result = res.stderr.read() # 命令正確的結果 if out_result: conn.send(out_result) print('已發送給客戶端','字節數是',len(out_result)) elif err_result: conn.send(err_result) print('發生錯誤','字節數是',len(err_result))
import socket client = socket.socket() client.connect(('127.0.0.1',8008)) while 1: comm = input('請輸入命令>>>').encode('utf8') client.send(comm) data = client.recv(1024) print(data.decode('gbk'))
分析:上述示例中有一個問題,就是當服務端執行命令的返回結果超過1024字節時,因爲咱們recv(1024)中要求最多一次接收1024字節,就會形成一部分接收不到的狀況,那麼這部分是丟失了嗎?通過測試發現沒有丟失,會在下一次再recv時接收剩下的部分,因此客戶端能夠經過循環recv接收剩下的部分,那麼問題又來了,循環多少次呢?你可能會想:先把執行結果的長度(字節數)發送給客戶端,再發送執行結果數據,客戶端經過長度決定循環接收數據的次數,以下示例:數據庫
import socket import subprocess server = socket.socket() server.bind(('127.0.0.1',8008)) server.listen(5) conn,addr = server.accept() while 1: data = conn.recv(1024).decode('utf8') res = subprocess.Popen(data, shell = True, stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out_result = res.stdout.read() # 發生錯誤後結果,字節串 err_result = res.stderr.read() # 命令正確的結果,字節串 len_out = len(out_result) # 錯誤結果字節數 len_err = len(err_result) # 正確結果字節數 if out_result: conn.send(str(len_out).encode('utf8')) # 發送正確結果字節數 conn.send(out_result) print('已發送給客戶端','字節數是',len(out_result)) elif err_result: conn.send(str(len_err).encode('utf8')) # 發送錯誤結果字節數 conn.send(err_result) print('發生錯誤','字節數是',len(err_result))
import socket client = socket.socket() client.connect(('127.0.0.1',8008)) while 1: comm = input('請輸入命令>>>').encode('utf8') client.send(comm) data_len = int(client.recv(1024).decode('utf8')) # data_len = 1365 print(data_len) data_all = b'' while len(data_all) < data_len: data = client.recv(1024) data_all += data print(data_all.decode('gbk'))
分析:試運行上面示例,分析有沒有問題?windows
2、黏包緩存
一、黏包現象:同時執行多條命令以後,獲得的結果極可能只有一部分,在執行其餘命令的時候又接收到以前執行的另一部分結果,這種現象就是黏包。上述模擬ssh示例中最後一個服務器端代碼就可能會出現黏包現象,由於服務端代碼中連續寫了兩個send(一個send長度,一個send數據內容),send不會發生阻塞,因此形成兩個send會造成一個包發送過去,接收方沒法分辨哪些數據是哪一個send發送的,也就沒法獲取到長度和內容。那麼怎麼解決黏包現象?安全
二、黏包成因服務器
a、TCP協議中的數據傳遞網絡
tcp協議的拆包機制:當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發送出去。MTU(Maximum Transmission Unit)的意思是網絡上傳送的最大數據包。MTU的單位是字節。大部分網絡設備的MTU都是1500,若是本機的MTU比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度。
面向流的通訊特色和Nagle算法:TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端就難於分辨出來了,必須提供科學的拆包機制。即面向流的通訊是無消息保護邊界的。對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收時,會繼續上次繼續接收,一端老是在收到ack時纔會清楚緩衝區內容。數據是可靠的,可是會黏包。
基於tcp協議特色的黏包現象成因:
發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。
也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。
而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。
怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看來,根本不知道該文件的字節流從何處開始,在何處結束;
此外,發送方引發的黏包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了黏包數據。
b、udp不會發生黏包
UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。
對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。
不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y;x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠。
總結:黏包現象只發生在tcp協議中:
1) 從表面上看,黏包問題主要是由於發送方和接收方的緩存機制、tcp協議面向流通訊的特色;
2) 實際上,主要仍是因接收方不知道消息之間的界限,不知道一次性提取多少字節的數據形成的;
三、struct模塊
這個模塊能夠把要發送的數據長度轉換成固定長度的bytes(字節),以下示例:
import struct res1 = struct.pack("i",23767) print(res1) # b'\xd7\\\x00\x00' print(len(res1)) # 4 固定長度是4個字節 obj1 = struct.unpack("i",res1) print(obj1 , obj1[0]) # (23767,) 23767
四、黏包的解決方案
咱們能夠藉助struct模塊,這個模塊能夠把要發送的數據長度轉換成固定長度的字節。這樣客戶端每次接收消息以前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,那麼最終接受的數據只要達到這個值就中止,就能恰好很少很多的接收完整的數據了。
發送時 |
接收時 |
先發送struct轉換好的數據長度4字節 |
先接受4個字節使用struct轉換成數字來獲取要接收的數據長度 |
再發送數據 |
再按照長度接收數據 |
因此上述模擬ssh示例最終能夠改成以下代碼:
# 服務器端代碼
import socket import subprocess import struct server = socket.socket() server.bind(('127.0.0.1',8008)) server.listen(5) conn,addr = server.accept() while 1: data = conn.recv(1024).decode('utf8') res = subprocess.Popen(data, shell = True, stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out_result = res.stdout.read() err_result = res.stderr.read() if out_result: conn.send(struct.pack('i',len(out_result))) # 構建報頭 conn.send(out_result) # 發送報文 print('已發送給客戶端','字節數是',len(out_result)) elif err_result: conn.send(struct.pack('i',len(err_result))) # 構建報頭 conn.send(err_result) # 發送報文 print('發生錯誤','字節數是',len(err_result))
# 客戶端代碼
import socket import struct client = socket.socket() client.connect(('127.0.0.1',8008)) while 1: comm = input('請輸入命令>>>').encode('utf8') client.send(comm) data_len = struct.unpack('i',client.recv(4))[0] data_all = b'' while len(data_all) < data_len: data = client.recv(1024) data_all += data print(data_all.decode('gbk'))
3、hashlib模塊
一、算法介紹
python的hashlib提供了常見的摘要算法,如MD5,SHA1等等。
什麼是摘要算法呢?摘要算法又稱哈希算法、散列算法。它經過一個函數,把任意長度的數據轉換爲一個長度固定的數據串(一般用16進制的字符串表示)。
摘要算法就是經過只要函數f1()對任意長度的數據data計算出固定長度的摘要digest,目的是爲了發現原始數據是否被人篡改過。
摘要算法之因此能指出數據是否被篡改過,就是由於摘要函數是一個單向函數,計算(data)很容易,但經過digest反推data卻很是困難,並且,對原始數據作一個bit的修改,都會致使計算出的摘要徹底不一樣。
咱們以前學過一個簡單用MD5加密的示例,以下:
import hashlib md5 = hashlib.md5() md5.update(b'how to use md5 in python hashlib?') print(md5.hexdigest()) # d26a53750bc40b38b65a520292f69306
若是數據量很大,能夠分屢次調用update(),最後計算的結果是同樣的,以下示例:
import hashlib md5 = hashlib.md5() md5.update(b'how to use md5 in ') md5.update(b'python hashlib?') print(md5.hexdigest()) # d26a53750bc40b38b65a520292f69306
MD5是最多見的摘要算法,速度很快,生成結果是固定的128bit字節,一般用一個32位的16進制字符串表示。另外一種常見的只要算法是SHA1,調用SHA1和調用MD5徹底相似,以下示例:
import hashlib sha1 = hashlib.sha1() sha1.update(b'how to use sha1 in ') sha1.update(b'python hashlib?') print(sha1.hexdigest()) # 2c76b57293ce30acef38d98f6046927161b46a44
SHA1的結果是160 bit字節,一般用一個40位的16進制字符串表示。比SHA1更安全的算法是SHA256和SHA512,不過越安全的算法越慢,並且摘要長度更長。
二、算法應用
任何容許用戶登陸的網站都會存儲用戶登陸的用戶名和口令。如何存儲用戶名和口令呢?方法是存到數據庫表中:
name | password --------+---------- michael | 123456 bob | abc999 alice | alice2008
若是以明文保存用戶口令,若是數據庫泄露,全部用戶的口令就落入黑客的手裏。此外,網站運維人員是能夠訪問數據庫的,也就是能獲取到全部用戶的口令。正確的保存口令的方式是不存儲用戶的明文口令,而是存儲用戶口令的摘要,好比MD5:
username | password ---------+--------------------------------- michael | e10adc3949ba59abbe56e057f20f883e bob | 878ef96e86145580c38c87f0410ad153 alice | 99b1c2188db85afee403b1536010c2c9
考慮這麼個狀況,不少用戶喜歡用123456,888888,password這些簡單的口令,因而,黑客能夠事先計算出這些經常使用口令的MD5值,獲得一個反推表:
'e10adc3949ba59abbe56e057f20f883e': '123456' '21218cca77804d2ba1922c33e0151105': '888888' '5f4dcc3b5aa765d61d8327deb882cf99': 'password'
這樣,無需破解,只須要對比數據庫的MD5,黑客就得到了使用經常使用口令的用戶帳號。
對於用戶來說,固然不要使用過於簡單的口令。可是,咱們可否在程序設計上對簡單口令增強保護呢?
因爲經常使用口令的MD5值很容易被計算出來,因此,要確保存儲的用戶口令不是那些已經被計算出來的經常使用口令的MD5,這一方法經過對原始口令加一個複雜字符串來實現,俗稱「加鹽」:
hashlib.md5("salt".encode("utf-8"))
通過Salt處理的MD5口令,只要Salt不被黑客知道,即便用戶輸入簡單口令,也很難經過MD5反推明文口令。
可是若是有兩個用戶都使用了相同的簡單口令好比123456,在數據庫中,將存儲兩條相同的MD5值,這說明這兩個用戶的口令是同樣的。有沒有辦法讓使用相同口令的用戶存儲不一樣的MD5呢?
若是假定用戶沒法修改登陸名,就能夠經過把登陸名做爲Salt的一部分來計算MD5,從而實現相同口令的用戶也存儲不一樣的MD5。
摘要算法在不少地方都有普遍的應用。要注意摘要算法不是加密算法,不能用於加密(由於沒法經過摘要反推明文),只能用於防篡改,可是它的單向計算特性決定了能夠在不存儲明文口令的狀況下驗證用戶口令。
4、補充知識
data = conn.recv(1024) # 阻塞
監聽的套接字對象conn斷開鏈接:
針對windows:異常報錯。
針對linux:接收一個空的data。