MENU

浅谈 Fastcgi 协议分析

December 4, 2021 • Read: 989 • WEB Security Learning

0x00抛砖

1、Web Server传递数据的方法
正式说CGI之前,先来了解一下Web Server传递数据的另外一种方法∶PHP Module加载方式。相信都会想起Apache吧,初学php时,在windows上安装完php和Apache之后,为了让Apache能够解析php代码,我们会在Apache的配置文件(httpd.conf)中添加如下配置∶

#添加下面两行
LoadModule php5_module D: / php/ php5apache2_2.dll
AddType application/ x-httpd-php .php
#修改如下内容
<IfModule dir_module>
DirectoryIndex index.php index.html
</IfModule>

其实原理是,用LoadModule来加载php5_module,就是把php作为apache的一个子模块来运行。当通过web访问php文件时,apache就会调用php5_module来解析php代码。那么,php5_module是如何将数据传给php的解析器来解析php代码的呢?答案是: sapi用一张图来看apache、php、sapi三者之间的关系︰

从上面图中,我们看出了sapi就是这样的一个中间过程,sapi提供了一个和外部通信的接口,使得PHP可以和其他应用进行交互数据((apache,nginx等)。php默认提供了很多种sapi,常见的提供给apache和nginx的php5_module、CGl、FastCGI,给IIS的ISAPI,以及Shell的CLl。(httpd是Apache超文本传输协议(HTTP)服务器的主程序。被设计为一个独立运行的后台进程,它会建立一个处理请求的子进程或线程池)

0x01什么是CGI?

早期的Web服务器,只能响应浏览器发来的HTTP静态资源的请求,并将存储在服务器中的静态资源返回给浏览器。随着Web技术的发展,逐渐出现了动态技术,但是Web服务器并不能够直接运行动态脚本,为了解决Web服务器与外部应用程序(CGI程序)之间数据互通,于是出现了CGI(Common Gateway Interface)通用网关接口。简单理解,可以认为CGI是Web服务器和运行在其上的应用程序进行“交流”的一种约定。
工作原理:
1.CGI针对每个http请求都是fork一个新进程来进行处理,接着读取php.ini文件配置信息,初始化执行环境等。2.然后这个进程会把处理完的数据返回给web服务器,最后web服务器把内容发送给用户。3.刚才fork的进程也随之退出。4.如果下次用户还请求动态资源,那么web服务器又再次fork一个新进程,周而复始的进行。

0x02什么是FastCGI?

有了CGI,自然就解决了Web服务器与PHP解释器的通信问题,但是Web服务器有一个问题,就是它每收到一个请求,都会去Fork一个CGI进程,请求结束再kill掉这个进程,这样会很浪费资源。于是,便出现了CGI的改良版本——Fast-CGI。

维基百科对 FastCGI 的解释是:快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。FastCGI致力于减少网页服务器与CGI程序之间交互的开销,Fast-CGI每次处理完请求后,不会kill掉这个进程,而是保留这个进程,从而使服务器可以同时处理更多的网页请求。这样就会大大的提高效率
工作原理:
1、Fastcgi则会先fork一个master,解析配置文件,初始化执行环境,然后再fork多个worker。
2、当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。
3、而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是Fastcgi的对进程的管理。大多数Fastcgi实现都会维护一个进程池。注:swoole作为httpserver,实际上也是类似这样的工作方式。

0x03什么是PHP-FPM?

PHP-FPM 就是 PHP 版本的 FastCGI 协议实现,有了它,就是实现 PHP 脚本与 Web 服务器(通常是 Nginx)之间的通信,同时它也是一个 PHP SAPI,从而构建起 PHP 解释器与 Web 服务器之间的桥梁。
工作原理:
PHP-FPM 会创建一个主进程,控制何时以及如何将HTTP请求转发给一个或多个子进程处理。PHP-FPM主进程还控制着什
么时候创建(处理Web应用更多的流量)和销毁(子进程运行时间太久或不再需要了)
PHP子进程。PHP-FPM进程池中的每个进程存在的时间都比单个HTTP请求长,可以处
理10、50、100、500或更多的HTTP请求。

0x04FastCGI协议

FastCGI程序和web服务器之间通过可靠的流式传输(Unix Domain Socket或TCP)来通信,相对于传统的CGI程序,有环境变量和标准输入输出,而FastCGI程序和web服务器之间则只有一条socket连接来传输数据,所以它把数据分成以下多种消息类型:

#define FCGI_BEGIN_REQUEST     //表示一个请求的开始
#define FCGI_ABORT_REQUEST     //  表示服务器希望终止一个请求
#define FCGI_END_REQUEST  //*表示该请求处理完毕       
#define FCGI_PARAMS      //对应于CGI程序的环境变量,php $_SERVER 数组中的数据绝大多数来自于此        
#define FCGI_STDIN       //对应CGI程序的标准输入,FastCGI程序从此消息获取 http请求的POST数据       
#define FCGI_STDOUT      //*对应CGI程序的标准输出,web服务器会把此消息当作html返回给浏览器        
#define FCGI_STDERR      //*对应CGI程序的标准错误输出, web服务器会把此消息记录到错误日志中       
#define FCGI_DATA         //这里不做介绍       
#define FCGI_GET_VALUES   //*这里不做介绍     
#define FCGI_GET_VALUES_RESULT //这里不做介绍
#define FCGI_UNKNOWN_TYPE  //*FastCGI程序无法解析该消息类型     
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

注意:由FastCGI程序返回给web服务器的消息类型带有*,剩下的则是由web服务器向FastCGI程序传输的消息类型
然后就是web服务器和FastCGI程序每传输一个消息的时候,首先会传输一个8字节固定长度的消息头:

struct FCGI_Header {
    unsigned char version; //表示fastcgi协议版本
    unsigned char type; // 表示消息的类型,就是前面提到的多种消息类型之一
    unsigned char requestIdB1;//用ID值标识出当前所属的 FastCGI 请求
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;//为了使消息8字节对齐,提高传输效率,可以在消息上添加一些字节数来达到消息对齐的目的
    unsigned char reserved; //保留字段,暂时无用
    /* Body 消息主体 */
    unsigned char contentData[contentLength];
    unsigned char paddingData[paddingLength];
} FCGI_Record;

头由8个 uchar 类型的变量组成,每个变量一个字节。其中,requestId 占两个字节,一个唯一的标志id,以避免多个请求之间的影响;contentLength 占两个字节,表示 Body 的大小。可见,一个 Fastcgi Record 结构最大支持的 Body 大小是2^16,也就是 65536 字节。后端语言解析了 Fastcgi 头以后,拿到 contentLength,然后再在请求的 TCP 流里读取大小等于 contentLength 的数据,这就是 Body 体。

Body 后面还有一段额外的数据(Padding),其长度由头中的 paddingLength 指定,起保留作用。不需要该Padding的时候,将其长度设置为0即可。
这里我们详细了解一下字节type:type 就是指定该 Record 的作用。因为 Fastcgi 中一个 Record 的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个 Record,通过 type 来标志每个 Record 的作用,并用 requestId 来标识同一次请求的id。也就是说,每次请求,会有多个 Record,他们的 requestId 是相同的。

看了这个表格就很清楚了,服务器中间件和后端语言通信,第一个数据包就是 type 为1的 Record,后续互相交流,发送 type 为4、5、6、7的 Record,结束时发送 type 为2、3的 Record。
未完待续,有空再补!

Last Modified: December 24, 2021
Archives Tip
QR Code for this page
Tipping QR Code