天天看點

BaseHTTPServer與CGIHTTPServer源碼分析

打開 /usr/lib/python2.6/basehttpserver.py 檔案。

最上面定義了類 httpserver,繼承于 socketserver.tcpserver,它不斷接收資料,并将接收到的資料交給 requesthandler 處理。

BaseHTTPServer與CGIHTTPServer源碼分析

它沒有在tcpserver的基礎上添加大量的功能,隻加了一個server_bind()成員函數。

看到類 basehttprequesthandler,這個類負責處理接收到的http請求,如post,get之類的。

BaseHTTPServer與CGIHTTPServer源碼分析

看到它的成員函數 handle()

BaseHTTPServer與CGIHTTPServer源碼分析

請求處理就是調用handle()函數進行的。首先,它将類成員變量close_connection置為1,如果在handle_one_request()執行中沒有将其置為0,那麼handle()就傳回了。

那在什麼情況下close_connection會被置為0呢?如果請求的header裡有 connection:keep-alive時會被清0。見parse_request()中:

BaseHTTPServer與CGIHTTPServer源碼分析

從上面的while循環可以看到,如果close_connection為0,那麼就繼續執行handle_one_request(),直到close_connection為1為至。

那麼 handle_one_request() 又在幹什麼呢?顧名思義,就是處理一個請求。

BaseHTTPServer與CGIHTTPServer源碼分析

l312:從rfile讀取請求的資料,也就是http封包資料。

l313~l315:如果讀失敗退出。

l316~l317:調用parse_request()對http封包header進行解析,如果失敗則退出。

l318:根據command,生成處理函數名,如get指令生成的是do_get。

l319~l323:檢查目前類是否有 do_xxx() 成員函數,如果存在 do_xxx() 這個成員函數。

再看一下 parse_request() 是如何分析http header的。主要分兩步:

(1)讀對封包資料的第一行,格式是:<命名> <路徑> <http版本>,通常是:“get / http/1.1”。

        分析版号是否正确,并解析出command, path, version,并儲存到對應的成員變量中。

(2)檢查headers是中的connection,如果是keep-alive,那麼就得将close_connection置為0,以儲存連接配接。

BaseHTTPServer與CGIHTTPServer源碼分析

從對basehttprequesthandler的分析可以得知,如果我們要響應post,get指令,那必須得繼承于basehttprequesthandler,并定義好do_get()與do_post()函數。

除了上述的三個重要的函數外,basehttprequesthandler 還提供了很多有用的成員函數:

send_error(code, message=none)

send_respond(code, message=none)

send_header(keyword, value)

end_headers()

...

打開 /usr/lib/python2.6/cgihttpserver.py 檔案。檔案裡隻定義了一個 cgihttprequesthandler 類,繼承于 simplehttpserverhandler。

其實 simplehttprequesthandler 是繼承于 basehttprequesthandler 的。

BaseHTTPServer與CGIHTTPServer源碼分析

它實作了 do_post() 函數:

BaseHTTPServer與CGIHTTPServer源碼分析

意思很簡單,如果是cgi,那麼就執行cgi,否則報錯。

那怎麼才算是cgi呢?我們跟蹤一下 is_cgi() 函數:

BaseHTTPServer與CGIHTTPServer源碼分析

看起來很簡單,也就是在目錄 cgi_directories 下的檔案,認為是cgi檔案。在l89定義了 cgi_directories,也就是在 /cgi-bin 或 /htbin 目錄下的都認為是 cgi。

_url_collapse_path_split(path) 函數是用于規整路徑的,防止路徑中出現過多 ./ 或 .. / 出現的防問漏洞。

比如用戶端發送惡意path,如:/aa/../../vital-file,這肯定是超出了防問權限了。還有就是濾掉 ./ 這樣的目錄,因為它沒有意義。

最後傳回一個元組(head_parts, tail_parts),比如輸入path為 /aa/../bb/./hello.py?aa=12&bb=23,傳回的是('/bb', 'hello.py?aa=12&bb=23')

BaseHTTPServer與CGIHTTPServer源碼分析

其代碼分兩步:

(1)l311~l322,從path,中以'/'為分隔,初步獲得tail_part。

(2)l323~l331,用head_parts,以棧的方式對 .. 進行分析。每遇到".."就head_parts.pop()一個,進而避免了出現"/../hello.html"這樣的問題。

(3)l332,傳回元組。

那麼怎麼執行cgi的呢?我們一起跟一下 run_cgi() 函數。

前面在分析 _url_collapse_path_split(path) 函數裡了解到它傳回的是一個元組。而這個元組存放到了self.cgi_info中,見 is_cgi() 函數代碼。

BaseHTTPServer與CGIHTTPServer源碼分析

從self.cgi_info獲得 (head_part, tail_part),比如:('/bb', 'hello.py?aa=12&bb=23')

BaseHTTPServer與CGIHTTPServer源碼分析

從"hello.py?aa=12&bb=23"中找到"?",以之為分隔,将 rest="hello.py",query="aa=12&bb=23"。

BaseHTTPServer與CGIHTTPServer源碼分析

我不知道為什麼l126要判斷一下,anyway,執行後的結果是:script="hello.py",rest=""。

BaseHTTPServer與CGIHTTPServer源碼分析

l132,将路徑與檔案名拼接起來,生成腳本程式的全名稱。執行結果為:scriptname="/bb/hello.py"。

在l133那裡進行了一次translate_path()是轉換路徑,比如在windows下,路徑應該是"\bb\hello.py"。

接下來,就是檢查scriptname是否存在l135,是否為檔案l137,是否為python腳本l141。當然,如果不是python腳本也沒關系,隻要系統有fork、popen2、popen3,且可執行也可以接受。

按道理說,隻要是在cgi-bin或htbin目錄下,可執行的程式都可以被認為是cgi程式。

接下來就是為cgi程式準備執行的環境變量:

BaseHTTPServer與CGIHTTPServer源碼分析

由于太多,我就不全部帖上來了。大家可以自己去看。我們重點注意的是:query_string,http_user_agent,http_cookie等。

BaseHTTPServer與CGIHTTPServer源碼分析

最後還将目前的環境變量也加入env。

然後就開始調用 send_response() 響應請求了:

BaseHTTPServer與CGIHTTPServer源碼分析

至于為什麼要将query中的+替換成空格,是協定中有說如果請求參數中如果有空格的要替換成+号嗎?好嘛,那我就當是這樣的。

下面分兩種情況下進行,一種是在linux下,用fork()建立一個新的程序,并execve()我們的腳本程式scriptname。另一種則是考慮到在非linux環境下,如windows下,沒有fork(),那麼就用subprocess進行操作。

由于部落客才疏學淺,對windows不熟,部落客就講解一下linux下的處理流程。

BaseHTTPServer與CGIHTTPServer源碼分析

l225~l226有點令部落客困惑。args為傳給腳本程式的參數,見l248。如果參數中沒有等号,那麼就将decode_query加入到args中。什麼意思?

如果我們的請求不是"aa=12&bb=23",而是"12",那麼"12"是不是就會被加入到參數清單中?好像是這個意思。部落客個人覺得,不管有沒有=号,都是可以加入到args中的。

然後在l229中開始fork()了,自fork()之後,l232~l239為父程序執行的内容,l242~251為子程序執行的内容。

父程序:

    在建立了子程序之後,就開始等子程序完成l232。l234~l236部落客也不知道是在幹什麼。

子程序:

    l246~l247,将 self.rfile檔案映射到stdin,self.wfile檔案映射到stdout。這很關鍵,這也解決了為什麼我們在腳本程式裡print的内容直接就成了網頁的正文。

    l248,調用execve()執行 scriptfile,并将args作為參數,将環境變量也交給 scriptfile。

好了,讀到這裡算是講解完了。

我們寫一個幾個簡單的程式來試試。

我們建立一個目錄 test-cgi,在該目錄下建立 cgi-bin

分别建立python, lua, shell 腳本程式:

檔案:hello.py

檔案:hello.lua

檔案:hello.sh

并賦于它們可執行權限。

然後我看開啟cgihttpserver。

服務的預設端口号為8000,如果要另行指定端口的話,可以在後面加端口号,如:

現在是見證奇迹的時刻了!

我們打開浏覽器,在位址欄分别輸入:

http://127.0.0.1:8000/cgi-bin/hello.py

http://127.0.0.1:8000/cgi-bin/hello.lua

http://127.0.0.1:8000/cgi-bin/hello.sh

得到的結果分别如下:

BaseHTTPServer與CGIHTTPServer源碼分析
BaseHTTPServer與CGIHTTPServer源碼分析
BaseHTTPServer與CGIHTTPServer源碼分析

不管cgi是什麼程式,隻要是可執行的程式都可以。

部落客發現python2.6的cgihttpserver有bug。

在cgi-bin目錄下的程式可以被當用cgi進行通路,但是如果在cgi-bin目錄的子目錄裡的可執行檔案就被當成了普通的檔案。

例如通路 /cgi-bin/sub/hello.py,結果确是:

BaseHTTPServer與CGIHTTPServer源碼分析

原因在于 is_cgi() 中,在 is_cgi() 中調用 _url_collapse_path_split(path) 傳回的是一個元組 (head_part, tail_part)。

比如 path="/cgi-bin/sub/hello.py?aa=12&bb=13",那麼傳回的元組是:("/cgi-bin/sub", "hello.py?aa=12&bb=13")

BaseHTTPServer與CGIHTTPServer源碼分析

這麼一來,在 is_cgi() 中,splitpath[0] 則為 "/cgi-bin/sub",splitpath[0] 不在 cgi_directories 中。是以 "/cgi-bin/sub/hello.py"不被認為是cgi程式。

部落客看過 python2.7中的實作。其是修複了這個bug的。部落客跟據自己的想法,自己做了如下的修改:

BaseHTTPServer與CGIHTTPServer源碼分析

結果自測,修複了上述的bug。

BaseHTTPServer與CGIHTTPServer源碼分析

這個bug算是修複~

但是,還有其它問題還不知道怎麼解決:

(1)get請求可以通過query_string環境變量獲得。然而post的請求怎麼辦呢?