1. BaseHTTPServer浅析
打开 /usr/lib/python2.6/BaseHTTPServer.py 文件。
1.1 HTTPServer类
最上面定义了类 HTTPServer,继承于 SocketServer.TCPServer,它不断接收数据,并将接收到的数据交给 RequestHandler 处理。
它没有在TCPServer的基础上添加大量的功能,只加了一个server_bind()成员函数。
1.2 BaseHTTPRequestHandler类
看到类 BaseHTTPRequestHandler,这个类负责处理接收到的HTTP请求,如POST,GET之类的。
看到它的成员函数 handle()
请求处理就是调用handle()函数进行的。首先,它将类成员变量close_connection置为1,如果在handle_one_request()执行中没有将其置为0,那么handle()就返回了。
那在什么情况下close_connection会被置为0呢?如果请求的header里有 Connection:keep-alive时会被清0。见parse_request()中:
从上面的while循环可以看到,如果close_connection为0,那么就继续执行handle_one_request(),直到close_connection为1为至。
那么 handle_one_request() 又在干什么呢?顾名思义,就是处理一个请求。
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,以保存连接。
从对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()
...
2. CGIHTTPServer浅析
打开 /usr/lib/python2.6/CGIHTTPServer.py 文件。文件里只定义了一个 CGIHTTPRequestHandler 类,继承于 SimpleHTTPServerHandler。
其实 SimpleHTTPRequestHandler 是继承于 BaseHTTPRequestHandler 的。
它实现了 do_POST() 函数:
意思很简单,如果是CGI,那么就执行CGI,否则报错。
2.1 is_cgi()
那怎么才算是CGI呢?我们跟踪一下 is_cgi() 函数:
看起来很简单,也就是在目录 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')
其代码分两步:
(1)L311~L322,从path,中以'/'为分隔,初步获得tail_part。
(2)L323~L331,用head_parts,以栈的方式对 .. 进行分析。每遇到".."就head_parts.pop()一个,从而避免了出现"/../hello.html"这样的问题。
(3)L332,返回元组。
2.2 run_cgi()
那么怎么执行cgi的呢?我们一起跟一下 run_cgi() 函数。
前面在分析 _url_collapse_path_split(path) 函数里了解到它返回的是一个元组。而这个元组存放到了self.cgi_info中,见 is_cgi() 函数代码。
从self.cgi_info获得 (head_part, tail_part),比如:('/BB', 'hello.py?aa=12&bb=23')
从"hello.py?aa=12&bb=23"中找到"?",以之为分隔,将 rest="hello.py",query="aa=12&bb=23"。
我不知道为什么L126要判断一下,anyway,执行后的结果是:script="hello.py",rest=""。
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程序准备执行的环境变量:
由于太多,我就不全部帖上来了。大家可以自己去看。我们重点注意的是:QUERY_STRING,HTTP_USER_AGENT,HTTP_COOKIE等。
最后还将当前的环境变量也加入env。
然后就开始调用 send_response() 响应请求了:
至于为什么要将query中的+替换成空格,是协议中有说如果请求参数中如果有空格的要替换成+号吗?好嘛,那我就当是这样的。
下面分两种情况下进行,一种是在Linux下,用fork()创建一个新的进程,并execve()我们的脚本程序scriptname。另一种则是考虑到在非Linux环境下,如Windows下,没有fork(),那么就用subprocess进行操作。
由于博主才疏学浅,对Windows不熟,博主就讲解一下Linux下的处理流程。
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。
好了,读到这里算是讲解完了。
3. 测试CGI
我们写一个几个简单的程序来试试。
我们新建一个目录 test-cgi,在该目录下创建 cgi-bin
$ mkdir test-cgi
$ cd test-cgi
$ mkdir cgi-bin
$ cd cgi-bin
分别创建python, lua, shell 脚本程序:
文件:hello.py
#!/usr/bin/env python
page = '''<html>
<body>
<p>This is python script.</p>
</body>
</html>
'''
print("")
print(page)
文件:hello.lua
#!/usr/bin/env lua
page = [[<html>
<body>
<p>This is Lua script.</p>
</body>
</html>]]
print("")
print(page)
文件:hello.sh
#!/usr/bin/env bash
echo ""
echo '<html>'
echo '<body>'
echo '<p>This is shell script.</p>'
echo '</body>'
echo '</html>'
并赋于它们可执行权限。
$ chmod u+x cgi-bin/hello.*
然后我看开启CGIHTTPServer。
$ python -m CGIHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
服务的默认端口号为8000,如果要另行指定端口的话,可以在后面加端口号,如:
$ python -m CGIHTTPServer 8080
Serving HTTP on 0.0.0.0 port 8080 ...
现在是见证奇迹的时刻了!
我们打开浏览器,在地址栏分别输入:
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
得到的结果分别如下:
不管cgi是什么程序,只要是可执行的程序都可以。
4. 存在的问题
博主发现python2.6的CGIHTTPServer有bug。
在cgi-bin目录下的程序可以被当用cgi进行访问,但是如果在cgi-bin目录的子目录里的可执行文件就被当成了普通的文件。
例如访问 /cgi-bin/sub/hello.py,结果确是:
原因在于 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")
这么一来,在 is_cgi() 中,splitpath[0] 则为 "/cgi-bin/sub",splitpath[0] 不在 cgi_directories 中。所以 "/cgi-bin/sub/hello.py"不被认为是CGI程序。
博主看过 python2.7中的实现。其是修复了这个bug的。博主跟据自己的想法,自己做了如下的修改:
结果自测,修复了上述的bug。
这个bug算是修复~
但是,还有其它问题还不知道怎么解决:
(1)GET请求可以通过QUERY_STRING环境变量获得。然而POST的请求怎么办呢?