博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
BaseHTTPServer与CGIHTTPServer源码分析
阅读量:5966 次
发布时间:2019-06-19

本文共 5035 字,大约阅读时间需要 16 分钟。

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 = '''

This is python script.

'''print("")print(page)
文件:hello.lua

#!/usr/bin/env luapage = [[

This is Lua script.

]]print("")print(page)
文件:hello.sh

#!/usr/bin/env bash echo ""echo ''echo ''echo '

This is shell script.

'echo ''echo ''
并赋于它们可执行权限。

$ chmod u+x cgi-bin/hello.*
然后我看开启CGIHTTPServer。

$ python -m CGIHTTPServerServing HTTP on 0.0.0.0 port 8000 ...
服务的默认端口号为8000,如果要另行指定端口的话,可以在后面加端口号,如:

$ python -m CGIHTTPServer 8080Serving 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的请求怎么办呢?

转载地址:http://evxax.baihongyu.com/

你可能感兴趣的文章
通过jsp请求Servlet来操作HBASE
查看>>
Shell编程基础
查看>>
Shell之Sed常用用法
查看>>
<气场>读书笔记
查看>>
Centos下基于Hadoop安装Spark(分布式)
查看>>
3D地图的定时高亮和点击事件(基于echarts)
查看>>
mysql开启binlog
查看>>
设置Eclipse编码方式
查看>>
分布式系统唯一ID生成方案汇总【转】
查看>>
并查集hdu1232
查看>>
Mysql 监视工具
查看>>
Linux Namespace系列(09):利用Namespace创建一个简单可用的容器
查看>>
博客搬家了
查看>>
Python中使用ElementTree解析xml
查看>>
jquery 操作iframe、frameset
查看>>
解决vim中不能使用小键盘
查看>>
jenkins权限管理,实现不同用户组显示对应视图views中不同的jobs
查看>>
我的友情链接
查看>>
批量删除用户--Shell脚本
查看>>
Eclipse Java @Override 报错
查看>>