PHP 5.3.0 DateTime 类的 BUG

观察以下代码:

setTimezone(new DateTimeZone($tz));

// 输出时间
echo $dt->getTimestamp();
echo “\n”;
echo $dt->format(“Y-m-d H:i:s”);
echo “\n”;

// 时间减7天
$dt->sub(new DateInterval(‘P7D’));

// 输出时间
echo $dt->getTimestamp();
echo “\n”;
echo $dt->format(“Y-m-d H:i:s”);
echo “\n”;

// 再次输出时间
echo $dt->getTimestamp();
echo “\n”;
echo $dt->format(“Y-m-d H:i:s”);
echo “\n”;

正确的输出结果应该是:

1303974000
2011-04-28 00:00:00
1303369200
2011-04-21 00:00:00
1303369200
2011-04-21 00:00:00

可是在 PHP 5.3.0 下测试,输出结果为:

1303974000
2011-04-28 00:00:00
1302764400
2011-04-14 00:00:00
1302159600
2011-04-07 00:00:00

真是不可思议!进一步测试发现每次调用 $dt->getTimestamp() 都会使 $dt 往前再减7天。
有在用 DateTime 类的同学们,赶紧把 PHP 版本升级下吧。
PHP 5.3.3 版本下测试正常。

Google 了一下关键词“php 5.3.0 datetime sub bug”,发现这个确实是一个 BUG。
http://bugs.php.net/49059

— EOF —

无废话 MongoDB 及 PHP 扩展编译安装

操作系统环境:Gentoo Linux

需要使用 scons 编译安装:

emerge scons

安装 mongodb 依赖库:

emerge boost
emerge libpcre
export CFLAGS=”-DJS_C_STRINGS_ARE_UTF8″ # 编译spidermonkey支持utf8
emerge spidermonkey

安装 mongodb:

# 1.6.5 版本

cd /work/setup
wget http://downloads.mongodb.org/src/mongodb-src-r1.6.5.tar.gz
tar zxf mongodb-src-r1.6.5.tar.gz
cd mongodb-src-r1.6.5
scons all
scons –full install

# 2.0.0 版本(2011-09-13 更新)

cd /work/setup
wget http://downloads.mongodb.org/src/mongodb-src-r2.0.0.tar.gz
tar zxf mongodb-src-r2.0.0.tar.gz
cd mongodb-src-r2.0.0
scons all
scons –full install

启动 mongodb 服务器:
创建默认的数据库目录,创建后启动 mongod:

mkdir -p /data/db
/usr/local/bin/mongod

在系统自动启动队列文件 /etc/conf.d/local.start 中添加:

/usr/local/bin/mongod –fork –logpath /var/log/mongodb.log –logappend

安装 PHP 扩展:

wget http://pecl.php.net/get/mongo-1.1.4.tgz
tar zxf mongo-1.1.4.tgz
cd mongo-1.1.4
/usr/local/php/bin/phpize
./configure –with-php-config=/usr/local/php/bin/php-config
make && make install

# 2011-09-13 更新

wget http://pecl.php.net/get/mongo-1.2.4.tgz
tar zxf mongo-1.2.4.tgz
cd mongo-1.2.4
/usr/local/php/bin/phpize
./configure –with-php-config=/usr/local/php/bin/php-config
make && make install

最后在 php.ini 尾部加上:

extension = mongo.so

— EOF —

关于持久连接和 FastCGI

PHP 作为一种脚本解释语言,其解析器在执行完脚本后会释放所有资源,包括数据库连接等等。这样下次需要再执行脚本时,就需要重新连接数据库了。
频繁地创建和关闭连接,无疑会造成不必要的开销。因此,在 PHP 中引入了持久连接的概念。具体可以看:
http://php.net/manual/en/features.persistent-connections.php

持久连接,不能工作在 CGI 模式下,因为 CGI 进程结束后,会释放所有资源。
PHP 当前主流的运行模式有两种,一种是以 web server 的 module 运行,另一种是 fastcgi。
在这两种模式下,都能够使用持久连接,因为创建连接后连接资源可以交给 web server 或 fastcgi server。

我个人更偏向于用 fastcgi,结构清晰,并且能提升 web server 的处理性能。
来看看一段简短地描述:

After all, Apache already has mod_php.
However, there are advantages to running PHP with FastCGI. Separating the PHP code from the web server removes ‘bloat’ from the main server, and should improve the performance of non-PHP requests. Secondly, having one permanent PHP process as opposed to one per apache process means that shared resources like persistent database connections are used more efficiently.

在 fastcgi 模式下,一个 PHP 进程固定地占用一个持久连接,只有可能连接数据库的 PHP 脚本才有可能装入这个 PHP 进程。
而 module 模式下,每个 apache 进程都会加载 PHP 模块,每个 apache 进程都会占用一个持久连接,这样不管是不是在处理 PHP ,都要占用连接。
因此,fastcgi 比 module 能够更好地利用持久连接。

在使用持久连接时,需要特别注意的是,数据库设置的允许的最大连接数不能小于 PHP 可能的实例数。

  • 在 module 模式下,数据库连接数要大于 apache 可能的最大进程数。
  • 在 fastcgi 模式下,数据库连接数要大于 php-cgi 最大进程数。

否则,可能出现连接数不够无法连接到数据库的错误。

来做个应用题:
一个网站的 PHP 运行在 fastcgi 模式下,流量大概是 1亿/日,需要持久连接的资源有 3 种。请估算下要几个 php-cgi 实例,几个持久连接?

1亿/日,估算高峰期每秒请求数为日均压力的 10 倍,为 (1亿/86400)*10 = 11574 requests/second。
假设每个请求平均耗时 0.3 秒,那么至少需要运行 11574/3 = 3858 个 php-cgi 实例。
采用持久连接,每个资源需要 1 个连接,需要 3858*3 = 11574 个持久连接。

挺险的,如果用 tcp 连接,数量级再大一倍,连接数都不够用了(当然我们还可以用 Unix Domain Socket),不过这种量也不可能跑在单台服务器上,所以连接数的问题也不愁啦。

— EOF —

Nginx+PHP 配置漏洞:静态文件都可以当作 PHP 解析

漏洞危险等级:毁灭性。

这个漏洞严格上说并不是 Nginx 和 PHP 本身的漏洞造成的,而是由配置造成的。在我之前写的许多配置中,都普遍存在这个漏洞。

简易检测方法:
打开 Nginx + PHP 服务器上的任意一张图片,如:
http://hily.me/test.png
如果在图片链接后加一串 /xxx.php (xxx为任意字符)后,如:
http://hily.me/test.png/xxx.php
图片还能访问的话,说明你的配置存在漏洞。

漏洞分析:

下面通过分析一个很常见的 Nginx 配置来解释下漏洞的成因:

server {
    listen       80;
    server_name  test.local;

    access_log  /work/www/logs/test.access.log  main;
    error_log  /work/www/logs/test.error.log;

    location / {
        root   /work/www/test;
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        root           /work/www/test;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_pass   unix:/tmp/php-fpm.sock;
    }
}

我们在 /work/www/test/ 目录下新建一个文件 test.png,内容如下:

那么访问 http://test.local/test.png 时,输出为文本内容:

但是当在后面加上 /xxx.php 时,即 http://test.local/test.png/xxx.php,可怕的事情发生了:

Array
(
    [HOSTNAME] => 
    [PATH] => /usr/local/bin:/usr/bin:/bin
    [TMP] => /tmp
    [TMPDIR] => /tmp
    [TEMP] => /tmp
    [OSTYPE] => 
    [MACHTYPE] => 
    [MALLOC_CHECK_] => 2
    [USER] => www
    [HOME] => /home/www
    [FCGI_ROLE] => RESPONDER
    [SCRIPT_FILENAME] => /work/www/test/test.png
    [QUERY_STRING] => 
    [REQUEST_METHOD] => GET
    [CONTENT_TYPE] => 
    [CONTENT_LENGTH] => 
    [SCRIPT_NAME] => /test.png/xxx.php
    [REQUEST_URI] => /test.png/xxx.php
    [DOCUMENT_URI] => /test.png/xxx.php
    [DOCUMENT_ROOT] => /work/www/test
    [SERVER_PROTOCOL] => HTTP/1.1
    [GATEWAY_INTERFACE] => CGI/1.1
    [SERVER_SOFTWARE] => nginx/0.7.62
    [REMOTE_ADDR] => 192.168.1.163
    [REMOTE_PORT] => 4080
    [SERVER_ADDR] => 192.168.1.12
    [SERVER_PORT] => 80
    [SERVER_NAME] => test.local
    [REDIRECT_STATUS] => 200
    [HTTP_ACCEPT] => image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/QVOD, application/QVOD, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
    [HTTP_ACCEPT_LANGUAGE] => zh-cn
    [HTTP_ACCEPT_ENCODING] => gzip, deflate
    [HTTP_USER_AGENT] => Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQPinyin 689; QQDownload 627; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.2; TheWorld)
    [HTTP_HOST] => test.local
    [HTTP_CONNECTION] => Keep-Alive
    [ORIG_SCRIPT_FILENAME] => /work/www/test/test.png/xxx.php
    [PATH_TRANSLATED] => /work/www/test
    [PHP_SELF] => /test.png/xxx.php
    [REQUEST_TIME] => 1274125615
)

环境变量中,SCRIPT_FILENAME 是 Nginx 传过来的:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

$fastcgi_script_name 变量说明请参考:
http://wiki.nginx.org/NginxHttpFcgiModule

Nginx 传给 PHP 的值为 /work/www/test/test.png/xxx.php,即 $_SERVER 中 ORIG_SCRIPT_FILENAME 的值,但是 $_SERVER 中 SCRIPT_FILENAME 却是 /work/www/test/test.png。

原因是,/work/www/test/test.png/xxx.php 并不存在,对于这些不存在的路径,PHP 会检查路径中存在的文件,并将多余的部分当作 PATH_INFO。
这里,/work/www/test/test.png 被 PHP 解析为 SCRIPT_FILENAME,/xxx.php 被 PHP 解析为 PATH_INFO 后被丢弃,因此并没有在 $_SERVER 中出现。

解决方法:

解决这个漏洞的方法很显然:关闭上面所述的解析即可。

这个解析可以在 PHP 的配置文件中设置,默认为开启。在这里我们需要将它关闭:

; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP’s
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://php.net/cgi.fix-pathinfo
;cgi.fix_pathinfo=1
cgi.fix_pathinfo=0

其中 cgi.fix_pathinfo=0 为新增的配置行,表示关闭 PHP 的自动 PATH_INFO 检测。关闭后,该配置漏洞即可消除。

更好的解决方案?
以上方案并不是最完美的,如果你先前有用到 cgi.fix_pathinfo 这个特性,影响会很大,比如关闭后,我的 Blog(Wordpress)文章的 URL 目录形式就得用 rewrite 来实现了。
如果可以将 PHP 设置成只解析 .php 为扩展名的文件,那么这个问题解决起来会更合理。
不过我没找到相关的设置项,或许今后应该出现在 php-fpm 的配置文件中?

总结:
这类问题基本上是无法预料的,但是如果架构设计良好的话,即使存在这个问题,也不会影响安全性。这里给出架构上的安全建议:
* 尽可能使动静内容分离,所有的静态内容存在于静态内容服务器,静态内容服务器上不解析PHP,这样静态文件就永远不能被解析了。

— EOF —

安装 php-rabbit: RabbitMQ 的 PHP 扩展

RabbitMQ 官方提供了三种 PHP 可用的扩展,分别是:
php-amqp
http://code.google.com/p/php-amqp/
php-rabbit
http://code.google.com/p/php-rabbit/
php-amqplib
http://code.google.com/p/php-amqplib/

这里我选择使用 php-rabbit,因为这个项目的 Activity 比较高,名字也比较权威 :)

安装步骤:

下载 RabbitMQ-C 客户端库 librabbitmq:
http://hg.rabbitmq.com/rabbitmq-c/
下载 RabbitMQ 协议代码生成工具:
http://hg.rabbitmq.com/rabbitmq-codegen/

注意 librabbitmq 的版本号,使用最新版本可能会导致下面编译 php-rabbit 时出错:

/bin/sh /work/setup/php-rabbit/libtool –mode=compile cc -I. -I/work/setup/php-rabbit -DPHP_ATOM_INC -I/work/setup/php-rabbit/include -I/work/setup/php-rabbit/main -I/work/setup/php-rabbit -I/usr/local/php/include/php -I/usr/local/php/include/php/main -I/usr/local/php/include/php/TSRM -I/usr/local/php/include/php/Zend -I/usr/local/php/include/php/ext -I/usr/local/php/include/php/ext/date/lib -I/usr/local/include -DHAVE_CONFIG_H -g -O2 -c /work/setup/php-rabbit/rabbit.c -o rabbit.lo
mkdir .libs
cc -I. -I/work/setup/php-rabbit -DPHP_ATOM_INC -I/work/setup/php-rabbit/include -I/work/setup/php-rabbit/main -I/work/setup/php-rabbit -I/usr/local/php/include/php -I/usr/local/php/include/php/main -I/usr/local/php/include/php/TSRM -I/usr/local/php/include/php/Zend -I/usr/local/php/include/php/ext -I/usr/local/php/include/php/ext/date/lib -I/usr/local/include -DHAVE_CONFIG_H -g -O2 -c /work/setup/php-rabbit/rabbit.c -fPIC -DPIC -o .libs/rabbit.o
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_class___construct’:
/work/setup/php-rabbit/rabbit.c:227: warning: ‘php_std_error_handling’ is deprecated (declared at /usr/local/php/include/php/main/php.h:295)
/work/setup/php-rabbit/rabbit.c:230: warning: ‘php_std_error_handling’ is deprecated (declared at /usr/local/php/include/php/main/php.h:295)
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_class_isConnected’:
/work/setup/php-rabbit/rabbit.c:329: warning: ‘php_std_error_handling’ is deprecated (declared at /usr/local/php/include/php/main/php.h:295)
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class___construct’:
/work/setup/php-rabbit/rabbit.c:363: warning: ‘php_std_error_handling’ is deprecated (declared at /usr/local/php/include/php/main/php.h:295)
/work/setup/php-rabbit/rabbit.c:368: warning: ‘php_std_error_handling’ is deprecated (declared at /usr/local/php/include/php/main/php.h:295)
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class_delete’:
/work/setup/php-rabbit/rabbit.c:504: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c:512: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class_purge’:
/work/setup/php-rabbit/rabbit.c:574: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c:580: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class_bind’:
/work/setup/php-rabbit/rabbit.c:646: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class_unbind’:
/work/setup/php-rabbit/rabbit.c:713: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class_consume’:
/work/setup/php-rabbit/rabbit.c:781: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_queue_class_get’:
/work/setup/php-rabbit/rabbit.c:917: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_exchange_class___construct’:
/work/setup/php-rabbit/rabbit.c:1169: warning: ‘php_std_error_handling’ is deprecated (declared at /usr/local/php/include/php/main/php.h:295)
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_exchange_class_delete’:
/work/setup/php-rabbit/rabbit.c:1318: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c:1326: error: unknown field ‘ticket’ specified in initializer
/work/setup/php-rabbit/rabbit.c: In function ‘zim_rabbit_exchange_class_bind’:
/work/setup/php-rabbit/rabbit.c:1534: error: unknown field ‘ticket’ specified in initializer
make: *** [rabbit.lo] Error 1

看了下是头文件 /usr/local/include/amqp_framing.h 中的结构体定义与源代码中冲突,因此怀疑是 librabbitmq 版本的问题。
使用 svn 上的代码也不行,很明显当前版本不适用。

看到官方下载页面上的声明:

Specification
The current RabbitMQ server and Java client library releases implement AMQP protocol version 0-8. The .NET/C# client implements AMQP protocol versions 0-8 and 0-9.

估计得用 0.8 版,我使用以下方式安装 librabbitmq 时可以编译通过:

wget http://hg.rabbitmq.com/rabbitmq-c/archive/ce1eaceaee94.tar.gz -O rabbitmq-c.tar.gz
tar zxf rabbitmq-c.tar.gz
wget http://hg.rabbitmq.com/rabbitmq-codegen/archive/c7c5876a05bb.tar.gz -O rabbitmq-codegen.tar.gz
tar zxf rabbitmq-codegen.tar.gz
mv rabbitmq-codegen-c7c5876a05bb/ rabbitmq-c-ce1eaceaee94/codegen
cd rabbitmq-c-ce1eaceaee94/
autoreconf -i && ./configure && make && make install

如果提示:

checking finding a python with simplejson installed… configure: error: could not find a python that can ‘import simplejson’

请安装 python 的 simplejson 库:

# wget http://pypi.python.org/packages/source/s/simplejson/simplejson-2.1.1.tar.gz
# tar zxf simplejson-2.1.1.tar.gz
# cd simplejson-2.1.1
# python setup.py install

下载并安装 php-rabbit:

# wget http://php-rabbit.googlecode.com/files/php-rabbit.r91.tar.gz
# tar zxf php-rabbit.r91.tar.gz
# cd php-rabbit
# /usr/local/php/bin/phpize
# ./configure –with-php-config=/usr/local/php/bin/php-config –with-rabbit
# make && make install

修改 php.ini,在尾部加上:
extension = rabbit.so

查看扩展是否安装成功:
# php -m | grep rabbit
rabbit

完工!!

— EOF —