zl程序教程

您现在的位置是:首页 >  工具

当前栏目

BaseHTTPServer与CGIHTTPServer源码分析

源码 分析
2023-09-14 08:58:20 时间

最上面定义了类 HTTPServer,继承于 SocketServer.TCPServer,它不断接收数据,并将接收到的数据交给 RequestHandler 处理。

211910_u72c_243525.png

它没有在TCPServer的基础上添加大量的功能,只加了一个server_bind()成员函数。


1.2 BaseHTTPRequestHandler类

看到类 BaseHTTPRequestHandler,这个类负责处理接收到的HTTP请求,如POST,GET之类的。

212127_ceSv_243525.png

看到它的成员函数 handle()

212228_8caU_243525.png

请求处理就是调用handle()函数进行的。首先,它将类成员变量close_connection置为1,如果在handle_one_request()执行中没有将其置为0,那么handle()就返回了。

那在什么情况下close_connection会被置为0呢?如果请求的header里有 Connection:keep-alive时会被清0。见parse_request()中:

212804_sbFH_243525.png

从上面的while循环可以看到,如果close_connection为0,那么就继续执行handle_one_request(),直到close_connection为1为至。


那么 handle_one_request() 又在干什么呢?顾名思义,就是处理一个请求。

213022_bUex_243525.png

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,以保存连接。

212804_sbFH_243525.png


从对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 的。

215938_w5jg_243525.png

它实现了 do_POST() 函数:

220047_eh6L_243525.png

意思很简单,如果是CGI,那么就执行CGI,否则报错。


2.1 is_cgi()

那怎么才算是CGI呢?我们跟踪一下 is_cgi() 函数:

220434_fW6Z_243525.png

看起来很简单,也就是在目录 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)

221207_2W33_243525.png

其代码分两步:

(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() 函数代码。

223157_s0zo_243525.png

从self.cgi_info获得 (head_part, tail_part),比如:(/BB, hello.py?aa=12 bb=23)

223404_0Nih_243525.png

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

223841_KXBX_243525.png

我不知道为什么L126要判断一下,anyway,执行后的结果是:script="hello.py",rest=""。

224137_325Y_243525.png

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程序准备执行的环境变量:

225111_t8P1_243525.png

由于太多,我就不全部帖上来了。大家可以自己去看。我们重点注意的是:QUERY_STRING,HTTP_USER_AGENT,HTTP_COOKIE等。

225520_epoe_243525.png

最后还将当前的环境变量也加入env。

然后就开始调用 send_response() 响应请求了:

225706_hrBH_243525.png

至于为什么要将query中的+替换成空格,是协议中有说如果请求参数中如果有空格的要替换成+号吗?好嘛,那我就当是这样的。


下面分两种情况下进行,一种是在Linux下,用fork()创建一个新的进程,并execve()我们的脚本程序scriptname。另一种则是考虑到在非Linux环境下,如Windows下,没有fork(),那么就用subprocess进行操作。

由于博主才疏学浅,对Windows不熟,博主就讲解一下Linux下的处理流程。

230345_ASTx_243525.png

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


$ 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

得到的结果分别如下:

221736_cImu_243525.png

221824_9XgY_243525.png

221932_a0WH_243525.png

不管cgi是什么程序,只要是可执行的程序都可以。


4. 存在的问题

博主发现python2.6的CGIHTTPServer有bug。

在cgi-bin目录下的程序可以被当用cgi进行访问,但是如果在cgi-bin目录的子目录里的可执行文件就被当成了普通的文件。

例如访问 /cgi-bin/sub/hello.py,结果确是:

230303_VbuO_243525.png

原因在于 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")

220434_fW6Z_243525.png

这么一来,在 is_cgi() 中,splitpath[0] 则为 "/cgi-bin/sub",splitpath[0] 不在 cgi_directories 中。所以 "/cgi-bin/sub/hello.py"不被认为是CGI程序。

博主看过 python2.7中的实现。其是修复了这个bug的。博主跟据自己的想法,自己做了如下的修改:

225908_msmE_243525.png

结果自测,修复了上述的bug。

230123_dnuK_243525.png

这个bug算是修复~


但是,还有其它问题还不知道怎么解决:

(1)GET请求可以通过QUERY_STRING环境变量获得。然而POST的请求怎么办呢?



HashMap实现原理及源码分析 在java中,HashMap是很常用的一种数据结构,最近重新温习了一下,这里以源码层面来分析总结一下HashMap,如有不合理或疑问的地方,欢迎沟通交流。
Java集合源码分析之开篇 Java集合是我们使用最频繁的工具,也是面试的热点,但我们对它的理解仅限于使用上,而且大多数情况没有考虑过其使用规范。本系列文章将跟随源码的思路,分析实现的每个细节,以期在使用时避免各种不规范的坑。在这里,我们会惊艳于开发者优秀的设计,也会感激先辈们付出的艰辛努力,更重要的是知其所以然,少犯错误,写出优秀的代码。 许多人对集合类的理解是暴力的,当需要保存对象时就使用ArrayList,当需要保存键值对时就使用HashMap,当需要不可重复时就使用HashSet,等等。而且使用方式也比较单一:
RefreshScope源码分析 springcloud的git配置中可以使用@RefreshScope + POST [/actuator/refresh]调用actuator的refresh来实现配置的热更新。 @RefreshScope springcloud描述: A Spring @Bean that is marked.
fishhook源码分析 最早了解到[fishhook](https://github.com/facebook/fishhook)是看了下面两篇文章之后,顿时让我觉得这是一个非常好的东西。总共210行代码,收获了1500+个star,神作啊。 1. [iOS Lazy Binding](http://www.atatech.org/articles/68014),使用fishhook拦截NSSetUncaughtE
周末找了个 nlp 相关的工具,使用起来还不错,它就是 rasa_nlu, 具有实体识别,意图分类等功能,在加上一个简单的意图操作即可实现简单的 chatbot 功能,其类图如下所示:
李名赫 博主从事的是物联网行业,目前在某知名智能家居科技公司担任家庭智能中心研发主管。欢迎交流!