低调大师

您现在的位置是: 首页 > BaseHTTPServer与CGIHTTPServer源码分析

文章详情

BaseHTTPServer与CGIHTTPServer源码分析

2015-1-28 22:12 34已围观 收藏 加入我们

今天学习了《Python核心编程》中Web服务器相关章节。于是走读了一下python的源码。

本人电脑上安装的是python2.6,相应的源码文件存放在 /usr/lib/python2.6/ 路径下。

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的请求怎么办呢?


文章转载至:https://my.oschina.net/hevakelcj/blog/372923
收藏 (0)

文章评论

共有0条评论来说两句吧...