當前位置:
首頁 > 知識 > 不完整的Http讀取和Python中的Requests庫

不完整的Http讀取和Python中的Requests庫

不完整的Http讀取和Python中的Requests庫

Python部落(python.freelycode.com)組織翻譯,禁止轉載,歡迎轉發。

requests庫可以說是Python中使用最廣泛的HTTP庫了。然而,我相信大多數用戶並不知道的是,requests當前穩定版本接受長度小於Content-Length頭所給出的長度的響應。如果你自己不仔細檢查的話,你可能都沒注意就使用了損壞的數據。我親身經歷了這一點,同時這也是我為什麼寫這篇文章的理由。讓我們看看為什麼當前requests版本沒有做這個檢查(這是一個特點,不是bug),和如何在你的腳本中進行手動檢查。

什麼是Content-Length頭?

複習一下,在HTTP協議中,Content-Length頭說明了請求或響應體的長度。它以8位位元組給出,其中1個8位位元組是8位。為了簡單起見,通篇文章我將使用術語位元組而不是8位位元組。通常,Content-Length頭用於通知接收方當前請求(或響應)何時完成。沒有它的話,你不知道你是否接收到了所有的數據或者你不知道是否有更多的數據需要讀取。當然,伺服器可以在每個請求或響應結束後斷開連接(HTTP1.0就是這樣的),但是到了HTTP1.1,除非另有聲明所有的連接都被視為持續性的。這顯著地加快了通信速度,因為你無需為每個請求單獨打開一個連接。

在閱讀完上述段落之後,下面的問題可能會出現在你的腦海中:

如果我收到Content-Length的值比收到的位元組數少會發生什麼?

在某些情況下(網路或伺服器端錯誤),伺服器可能會在發送完整消息之前突然斷開連接。HTTP1.1 RFC指出:

當允許消息體的消息中給出Content-Length時,其欄位值必須與消息體中的位元組數完全匹配。當接收並檢測到無效長度時,HTTP1.1用戶代理必須通知用戶。

因此,一旦接收到比Content-Length頭部中規定的更少的位元組,人們希望能收到通知。為了檢查這一點,我啟動一個簡單的HTTP伺服器,它總是返回下面的響應,然後斷開連接:

不完整的Http讀取和Python中的Requests庫

然後,我編寫了一個Python腳本,它向伺服器發送GET請求,檢查它是否成功,並列印接收到的數據:

不完整的Http讀取和Python中的Requests庫

當你運行它時,它成功了,而且不會引發異常:

不完整的Http讀取和Python中的Requests庫

那麼,這難道就是所有客戶端的行為方式嗎?為了確定,我使用curl進行嘗試:

不完整的Http讀取和Python中的Requests庫

為了找到答案,我使用了reqwest,它是Rust的HTTP庫。我的測試客戶端的完整實現可以在這裡找到。當我運行它時,它也提示我這種差異:

不完整的Http讀取和Python中的Requests庫

requests在這裡發生一些可疑的事情...

為什麼requests庫不警告我?

當你搜索requests庫時,您會發現大量令人驚訝的issues。

基本上,沒有將這種檢查納入requests庫的原因有三:

1.首先,我認為requests在技術上不是user-agent,而是庫。這使我們擺脫了user-agent行為的一些限制(事實上,我們在庫的其他地方採取了這種自由,就像我們對重定向的行為一樣)。

那麼,如果它不是user-agent,為什麼默認情況下會發送以下User-Agent頭?

不完整的Http讀取和Python中的Requests庫

2.其次,如果我們拋出異常,我們不可改變地破壞我們讀取的數據。它變得不可能訪問。這意味著,用戶如果想要在不知情的情況下,儘可能多地讀取和保存數據,將變得困難。

這很好理解。但是,這應該是默認行為嗎?我認為這應該作為一個可選項,requests會默認提醒你,但你應該可以禁止這個警告並使用你能夠讀取的數據。

3.最後,即使我們確實需要這個功能,我們也需要在urllib3中實現它。Content-Length是指消息體傳輸的位元組數,而不是解碼的長度,所以如果我們得到一個gzip(或DEFLATEd)響應,我們需要知道在解碼之前有多少位元組。這通常不是我們在requests庫級別獲得的信息。所以如果你仍然對這種行為感興趣,我建議你在shazow / urllib3上打開一個issues。

urllib3是requests底層的http庫。最初的發帖人在這裡提交了一個issue。雖然有意願提交PR,但是後來被關閉了。幸運的是,一年半後,提交了這樣一個PR並且被接受了。

在閱讀了上面的第三點之後,您可能會開始高興。不好的一面是,即使urllib3 PR在2016年8月29日合併,當前穩定版本的requests(撰寫時為2.18.4,即2018-04-22)仍然使用舊版本的urllib3,它不提供這個功能。好的一面是,requests庫已經合併了新版本的urllib3,只不過是合併到了requests:proposed/3.0.0分支上。

那麼,我能做些什麼來檢測腳本中的不完整讀取?

requests 3.x

如果你看到這篇文章時,已經發布了requests 3.x,只需使用requests 3.x。它應該提供enforce_content_length參數,其默認值應該為True。也就是說,如果requests庫收到一個不完整的內容,它應該引發一個異常:

不完整的Http讀取和Python中的Requests庫

requests 2.x

如果你使用的是requests2.x,你必須自己檢查。你可以使用下面的一段代碼:

不完整的Http讀取和Python中的Requests庫

檢查工作如下。首先,我們確保響應具有Content-Length頭。如果沒有,檢查是毫無意義的。然後,我們得到實際讀取的位元組數,並將其與預期值進行比較。如果我們讀取了更少的位元組,我們會發出錯誤信號。當然,不要拋出異常,你可以做任何你想做的事情(重試,列印錯誤信息並退出,向朋友抱怨等)。

需要驗證的話,你可以運行content-length.py HTTP伺服器並通過client-with-check.py發送請求。伺服器的寫入方式使其返回的位元組數少於響應的Content-Length頭中所述的位元組數。

被壓縮的響應會怎麼樣?

響應可以被壓縮。例如,伺服器可能會返回Content-Encoding頭設置為gzip的響應。這意味著響應體通過Lempel-Ziv編碼(LZ77)進行壓縮。當requests庫收到這樣的響應時,它會自動解壓縮它。當你再檢查response.content的長度(未壓縮響應的位元組數)時,它很可能與Content-Length頭中指定的長度不同。這是我們沒有使用len(response.content)來獲取上述檢查中響應的實際長度的原因。相反,我們必須使用response.raw.tell,它返回讀取的位元組的實際數量(在解壓縮之前)。

要驗證的話,你可以運行content-encoding-gzip.py HTTP伺服器並通過client-with-check.py發送請求。伺服器的寫入方式使其返回的位元組數少於響應的Content-Length頭中所述的位元組數。

如果存在Transfer-Encoding:chunked?

另外,Content-Length頭可以省略,可以使用chunked的Transfer-Encoding頭。這種流式數據傳輸自HTTP 1.1起可用,通過將響應拆分為塊。響應體具有以下形式:

不完整的Http讀取和Python中的Requests庫

這與Content-Length相比有幾個優點,包括為動態生成的內容維護一個持久的HTTP連接的能力,這些動態生成的內容的完整大小事先是未知的。

當我們處理沒有Content-Length頭的分塊傳輸時,我們應該如何檢查是否收到了所有數據?幸運的是,在這種情況下,requests庫按預期工作。也就是說,如果伺服器發送不完整的數據,該庫會引發異常

不完整的Http讀取和Python中的Requests庫

要驗證的話,你可以運行transfer-encoding-chunked.py HTTP伺服器並通過client.py發送請求。伺服器的寫入方式使其返回的位元組數少於塊大小中所述的位元組數。

最終建議

始終驗證你收到的數據是否正確。驗證你已讀取的位元組數是第一步。例如,下載散列(例如SHA-256)已知的文件時,應檢查下載文件的散列是否匹配。否則,您可能會冒險處理損壞的數據,這可能會導致惡意錯誤。

全部源代碼

GitHub上提供了所有伺服器和客戶端的完整源代碼。

討論

你還可以在/r/Python和Hacker News上討論這篇文章。


英文原文:https://blog.petrzemek.net/2018/04/22/on-incomplete-http-reads-and-the-requests-library-in-python/
譯者:xiaocai

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python部落 的精彩文章:

Python 3.7的新內置斷點快速一覽
python的緩存庫:cacheout

TAG:Python部落 |