BaseHTTPServer与CGIHTTPServer源码分析

简介:

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


目录
相关文章
|
网络协议 Linux Python
为什么基于TCP的应用需要心跳包(TCP keep-alive原理分析)
TCP keep-alive的三个参数 用man命令,可以查看linux的tcp的参数: man 7 tcp 其中keep-alive相关的参数有三个: tcp_keepalive_intvl (integer; default: 75; since Linux 2.
1290 0
|
4天前
|
存储 人工智能 安全
AI 越智能,数据越危险?
阿里云提供AI全栈安全能力,为客户构建全链路数据保护体系,让企业敢用、能用、放心用
|
7天前
|
域名解析 人工智能
【实操攻略】手把手教学,免费领取.CN域名
即日起至2025年12月31日,购买万小智AI建站或云·企业官网,每单可免费领1个.CN域名首年!跟我了解领取攻略吧~
|
6天前
|
数据采集 人工智能 自然语言处理
3分钟采集134篇AI文章!深度解析如何通过云无影AgentBay实现25倍并发 + LlamaIndex智能推荐
结合阿里云无影 AgentBay 云端并发采集与 LlamaIndex 智能分析,3分钟高效抓取134篇 AI Agent 文章,实现 AI 推荐、智能问答与知识沉淀,打造从数据获取到价值提炼的完整闭环。
412 93
|
7天前
|
SQL 人工智能 自然语言处理
Geo优化SOP标准化:于磊老师的“人性化Geo”体系如何助力企业获客提效46%
随着生成式AI的普及,Geo优化(Generative Engine Optimization)已成为企业获客的新战场。然而,缺乏标准化流程(Geo优化sop)导致优化效果参差不齐。本文将深入探讨Geo专家于磊老师提出的“人性化Geo”优化体系,并展示Geo优化sop标准化如何帮助企业实现获客效率提升46%的惊人效果,为企业在AI时代构建稳定的流量护城河。
403 156
Geo优化SOP标准化:于磊老师的“人性化Geo”体系如何助力企业获客提效46%
|
6天前
|
数据采集 缓存 数据可视化
Android 无侵入式数据采集:从手动埋点到字节码插桩的演进之路
本文深入探讨Android无侵入式埋点技术,通过AOP与字节码插桩(如ASM)实现数据采集自动化,彻底解耦业务代码与埋点逻辑。涵盖页面浏览、点击事件自动追踪及注解驱动的半自动化方案,提升数据质量与研发效率,助力团队迈向高效、稳定的智能化埋点体系。(238字)
296 158