考虑截获PHP读取源文件的接口。一开始,我考虑从Apache和PHP 之间的接口处处理,参见apache的src/modules/php4/mod_php4.c (这个是PHP用static方式编译进apache,make install 后的文件),在send_php()函数中截获文件指针,采用临时文件的方式,解密后替换文件指针。这种方法经过测试实践,证明是可行的。但是,必须使用两次文件操作,效率低下,而且对于DSO方式不可采用。 双缘敬老院
由此,重新考虑截获PHP读取文件并装载至缓存的过程,经过费力的寻找,发现在Zend引擎中zend-scanner.c是做此处理的。开始对此文件修改。照明工程
二、实现方法示意
采用libmcrypt作为加 密模块,现在采用的是DES方法ECB模式加密,
下面是文件加密的源代码:
C++代码
/* ecb.c-------------------cut here-----------*/
/* encrypt for php source code version 0.99 beta
we are using libmcrypt to encrypt codes, please
install it first.
compile command line:
gcc -O6 -lmcrypt -lm -o encryptphp ecb.c
please set LD_LIBRARY_PATH before use.
GNU copyleft, designed by wangsu , miweicong */
#define MCRYPT_BACKWARDS_COMPATIBLE 1
#define PHP_CACHESIZE 8192
#include < mcrypt.h >
#include < stdio.h >
#include < stdlib.h >
#include < math.h >
#include < sys/types.h >
#include < sys/stat.h >
#include < fcntl.h >
main(int argc, char** argv)
{
int td, i,j,inputfilesize,filelength;
char filename[255];
char password[12];
FILE* ifp;
int readfd;
char *key;
void *block_buffer;
void *file_buffer;
int keysize;
int decode=0;
int realbufsize=0;
struct stat *filestat;
if(argc == 3) {
strcpy(password,argv[1]);
strcpy(filename,argv[2]);
} else if(argc == 4 && !strcmp(argv[1],"-d")){
strcpy(password,argv[2]);
strcpy(filename,argv[3]);
decode=1;
printf("Entering decode mode ... n");
} else {
printf("Usage: encryptphp [-d] password filenamen");
exit(1);
}
keysize=mcrypt_get_key_size(DES);
key=calloc(1, mcrypt_get_key_size(DES));
gen_key_sha1( key, NULL, 0, keysize, password, strlen(password));
td=init_mcrypt_ecb(DES, key, keysize);
if((readfd=open(filename,O_RDONLY,S_IRUSR|S_IWUSR|S_IRGRP))==-1){
printf("FATAL: Can't open file to read");
exit(3);
}
filestat=malloc(sizeof(stat));
fstat(readfd,filestat);
inputfilesize=filestat- >st_size;
printf("filesize is %d n",inputfilesize);
filelength=inputfilesize;
inputfilesize=((int)(floor(inputfilesize/PHP_CACHESIZE))+1)*PHP_CACHESIZE;
if((file_buffer=malloc(inputfilesize))==NULL){
printf("FATAL: can't malloc file buffer.n");
exit(2);
}
if((block_buffer=malloc(PHP_CACHESIZE))==NULL){
printf("FATAL: can't malloc encrypt block buffer.n");
exit(2);
}
j=0;
while(realbufsize=read (readfd,block_buffer, PHP_CACHESIZE)){
printf(".");
if(!decode){
if(realbufsize< PHP_CACHESIZE){
for(i=realbufsize;i< PHP_CACHESIZE;i++){
((char *)block_buffer)[i]=' ';
}
}
mcrypt_ecb (td, block_buffer, PHP_CACHESIZE);
} else {
mdecrypt_ecb (td, block_buffer, realbufsize);
}
memcpy(file_buffer+j*PHP_CACHESIZE,block_buffer,PHP_CACHESIZE);
j++;
}
close(readfd);
if((ifp=fopen(filename,"wb"))==NULL){
printf("FATAL: file access error.n");
exit(3);
}
fwrite ( file_buffer, inputfilesize, 1, ifp);
free(block_buffer);
free(file_buffer);
free(filestat);
fclose(ifp);
printf("n");
return 0;
}
/*--- end of ecb.c ------------------------------------*/
因为ECB模式是块长度确定的块加密,这里填充了一 些空字符。国际展览
然后,修改php代码中 Zend/zend-scanner.c 如下:
(我的php版本是4.01pl2, SUNsparc/solaris 2.7, gcc 2.95;)
文件前加入:
#define MCRYPT_BACKWARDS_COMPATIBLE 1
#include < mcrypt.h >
然后,注释掉大约3510行前后的YY_INPUT的定义。
然后, 修改大约5150行前后的yy_get_next_buffer()函数:
函数头加上定义:
void *tempbuf;
char *key;
char debugstr[255];
int td,keysize;
int x,y;
FILE *fp;
然后 ,注释掉
YY_INPUT( (&yy_current_buffer- >yy_ch_buf[number_to_move]),
yy_n_chars, num_to_read );
这一句。
改为:
tempbuf=malloc(num_to_read);
if((yy_n_chars=fread(tempbuf,1,num_to_read,yyin))!=0){
/*decode*/
#define password "PHPphp111222"
#define debug 0
keysize=mcrypt_get_key_size(DES);
key=calloc(1, mcrypt_get_key_size(DES));
gen_key_sha1( key, NULL, 0, keysize, password, strlen(password));
td=init_mcrypt_ecb(DES, key, keysize);
mdecrypt_ecb(td, tempbuf, yy_n_chars);
memcpy((&yy_current_buffer- >yy_ch_buf[number_to_move]),tempbuf,yy_n_chars);
if(debug){
fp=fopen("/tmp/logs","wb");
fwrite("nstartn",7,1,fp);
fwrite(tempbuf,1,yy_n_chars,fp);
fwrite("nenditn",7,1,fp);
fclose(fp);
}
}
free(tempbuf);
然后,编译php,按正常方法安装即可,因为我对于libtool不太熟悉,因此我选择static方式,并在 configure时加入了--with-mcrypt,这样我就不用自己手工修改Makefile 电缆桥架
三、测试及结果
编译php,apache后,用ecb.c编译出来的encryptphp加密了几个文件,分别为< 1K,10K+,和40K+,在处理 40K大小文件时出错,别的文件均正常。塑胶地板
这是因为块的ECB加密方式决定了必须使用定长块,所以,请 诸位同好指点采用何种流加密方式可以兼顾到zend每次读取8192字节的缓存处理方式。(其他平台上 zend每次读取的块长度可能有所不同)
例如:通常用一个对象描述一个日期、一个数字或者货币。日期、整数或美元的类定义是都是便于使用的、快捷、便于封装的,并且方便进行拷贝,相互比较,甚至是创建。
从表面上看,这些描述简单的对象很容易被执行:它们的语句非常少,在构造类时无论是应用于Customer还是SKU都没有什么不同。这个想法似乎是正确的,但是所谓的"似乎正确"很容易产生一些bug。
请看下面的代码,这是一个关于以美元给员工发放工资的对象的定义和执行操作。多数情况下,它的运行是没有问题的。(这个类被命名为BadDollar,因为它还存在着bug)。考虑一下,看你是否能发现它的bug。
// PHP5
class BadDollar {
protected $amount;
public function __construct($amount=0) {
$this->amount = (float)$amount;
}
public function getAmount() {
return $this->amount;
}
public function add($dollar) {
$this->amount += $dollar->getAmount();
}
}
class Work {
protected $salary;public function __construct() {
$this->salary = new BadDollar(200);}
public function payDay() {
return $this->salary;
}
}
class Person {
public $wallet;
}
function testBadDollarWorking() {
$job = new Work;
$p1 = new Person;
$p2 = new Person;
$p1->wallet = $job->payDay();
$this->assertEqual(200, $p1->wallet->getAmount());
$p2->wallet = $job->payDay();
$this->assertEqual(200, $p2->wallet->getAmount());
$p1->wallet->add($job->payDay());
$this->assertEqual(400, $p1->wallet->getAmount());
//this is bad — actually 400
$this->assertEqual(200, $p2->wallet->getAmount());
//this is really bad — actually 400
$this->assertEqual(200, $job->payDay()->getAmount());
}
那么, bug是什么呢?如果不能上面的代码例子中直观地发现问题,这里有个提示:雇员对象$p1和对象$p2使用着同一个BadDollar对象实例。
首先,类Work和类Person的实例已经创建。那么,假设每一个雇员最初有一个空的电子钱包,雇员的电子钱包Person:wallet是通过Work::payDay()函数返回的对象资源变量赋值的,所以被设定为一个BadDollar类的对象实例。
还记得PHP5的对象赋值处理方式吗?因为PHP5的对象赋值的处理方式,所以$job::salary,、$p1::wallet和$p2::wallet这三个看上去不同的对象实例虽然使用着不同的“标识符”,但是事实上,它们全部都指定到同一个对象实例。
因此,接下来的发放工资的操作(PayDay表示发放工资的日子,这里表示发放工资的动作),使用$job->payDay()本来仅仅是想增加$P1的工资,却出乎意料地次给$P2也发放了。并且,这个动作还改变了工作的基本工资的额度。因此,最后两个值的检测报错。
Value Object PHP5 Unit Test
1) Equal expectation fails because [Integer: 200] differs from [Float: 400] by 200
in testBadDollarWorking
in ValueObjTestCase
2) Equal expectation fails because [Integer: 200] differs from [Float: 400] by 200
in testBadDollarWorking
in ValueObjTestCase
FAILURES!!!
在这里总结的许多编程惯用法都是很值得做为单独一个章节的,甚至一本书的。你应该把这章做为PHP模式设计使用惯用法的相关介绍,而且查看一些列出的参考书来进行更深入的学习。
测试你的代码
可能没有什么代码惯用法比测试代码更加重要了。好的测试可以提高开发速度。
可能一开始,这句格言会和你的直觉相矛盾。你可能会断言,测试是自由的障碍物。事实上恰恰相反,如果你十分完整的运行那些测试来检查你的软件的公共接口,你就可能在不改变(或者更加糟糕,破坏)原来的应用软件的前提下改变自己系统内在的执行。测试并检验你的公共接口的精确性和正确性,并且让自己随意改变一些代码的内在工作来确保你的软件是正确而且没有bug(错误)。
在讨论更多关于测试的好处之前,先让我们看一个示例。这本书里面所有的测试实例都使用了PHP测试框架——SimpleTest 。这个测试框架可以在 http://simpletest.org 获取到。
考虑下面的代码
<?php
// PHP4
// the subject code
define(‘TAX_RATE', 0.07);
function calculate_sales_tax($amount) {
round($amount * TAX_RATE,2);
}
// include test library
require_once ‘simpletest/unit_tester.php';
require_once ‘simpletest/reporter.php';
// the test
class TestingTestCase extends UnitTestCase {
function TestingTestCase($name='') {
$this->UnitTestCase($name);
}
function TestSalesTax() {
$this->assertEqual(7, calculate_sales_tax(100));
}
}
// run the test
$test = new TestingTestCase(‘Testing Unit Test');
$test->run(new HtmlReporter());
上面的代码首先定义了一个常量——TAX_RATE,和一个计算销售税的函数。接着,代码包含了使用SimpleTest框架的必备组件:单体测试本身和一个用来显示测试结果的“reporter”模块。
类TestingTestCase继承于SimpleTest框架的UnitTestCase类。通过扩展UnitTestCase,类TestingTestCase里面所有使用Test开头的方法都将被认为是测试实例——创造条件来调试你的代码并断言结果。
TestingTestCase定义了一个测试,TestSalesTax(),它包含了一个断言函数AssertEqual()。如果它的前两个输入参数是相等的,它将返回true,否则返回false。(如果你想显示assertEqual()失败的信息,你可以传入三个参数就像这样$this->assertEqual(7,calculate_sales_tax(100), “The sales tax calculation failed”))。
代码的最后两行创建了这个测试实例的实体并且使用一个HtmlReporter运行了它。你可以访问这个web页面来运行这个简单的测试。
运行这个测试将显示测试名称,失败断言的详细情况和一个总结条。(绿色的意味着成功(所有的断言都通过了),而红色的暗示着失败(至少有一个断言没有通过))
(assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,系统将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的。)
注:(assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,系统将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的。)
上面的代码有一个(有意的)错误,所以运行是不能通过了,显示结果如下:
Calculate_sales_tax()这么一个简单的才一行的函数哪里出错了呢?你可能已经注意到这个函数没有返回结果。下面是正确的函数:
function calculate_sales_tax($amount) {
return round($amount * TAX_RATE,2);
}
修改后运行,测试通过。
但是一个简单的测试并不能保证代码是稳定的。比如,你把calculate_sales_tax()改成 function calculate_sales_tax($amount) { return 7; },代码也会通过测试,但只有当1美元等价于100的时候才是正确的。你可以自己增加一些额外的测试方法来测试其他的静态值。
function TestSomeMoreSalesTax() {
$this->assertEqual(3.5, calculate_sales_tax(50));
}
或者改变函数TestSalesTax()来验证第二个(和第三个,等等)值,如下所示
function TestSalesTax() {
$this->assertEqual(7, calculate_sales_tax(100));
$this->assertEqual(3.5, calculate_sales_tax(50));
}
到目前为止还有一种更好的方法,就是新增加一个测试:选择随即值来测试你的代码。具体如下:
function TestRandomValuesSalesTax() {
$amount = rand(500,1000);
$this->assertTrue(defined(‘TAX_RATE'));
$tax = round($amount*TAX_RATE*100)/100;
$this->assertEqual($tax, calculate_sales_tax($amount));
}
TestRandomValuesSalesTax()引入了方法assertTrue(),如果传入的第一个变量等于于布尔真则assertTrue()通过。(和方法assertEqual()一样,方法assertTrue()在接受一个可选择性的、额外的后将返回一个失败的信息)。所以TestRandomValuesSalesTax()首先认为常量TAX_RATE已经定义了,然后使用这个常量来计算随机选择的的数量的税收。
但是TestRandomValuesSalesTax()也存在一个问题:它很大程度的依赖于方法calculate_sales_tax()。测试是应该和特殊的实现细节无关的。一个更好的测试应该只建立一个合理的分界线。接下来的这个测试假定销售税永远不会超过20%。
function TestRandomValuesSalesTax() {
$amount = rand(500,1000);
$this->assertTrue(calculate_sales_tax($amount)<$amount*0.20);
}
确保你的代码正常工作是测试的首要的目的,但是在测试你的代码时候,你应该认识到除此之外还有一些额外的,相对次要的目的:
最后,如果你的测试集——测试实例的集合——是非常彻底的,而且当所有的测试都通过的时候,你可以说你的代码是完备的。有趣的是,这个观点也恰好是Test Driven Development(测试驱动开发)的特征之一。
Test Driven Development(TDD)也被认为是Test First Coding(编码前测试)。Test First Coding是一种把测试更提前一步的方法:在你写任何代码之前先写好测试。你可以从http://xprogramming.com/xpmag/testFirstGuidelines.htm下载到一份很好的,简洁的关于TDD的摘要文章,同时下载到一本很好的关于策略的入门书——Kent Beck著作的《Test Driven Development:By Example》(这本书的例子都是用JAVA开发的,但其中代码的可读性是很好的,而且对主题的介绍和说明都做的很好的)。
注:敏捷开发(Agile Development)
最近,单体测试——特别是测绘驱动开发——已经和敏捷开发方法学紧密的联系起来了,比如说极限编程(XP)。极限编程的焦点关注于快速的反复的发步功能性的代码给客户,并把变化的客户需求做为开发过程中的必备部分。下面是一些关于学习敏捷编程的在线资源:
函数性测试
这本书里面的大部分测试例子都是用来测试面对对象的代码,但是所有形式的编程都可以从中得到收获的。单体测试框架,比如说PHPUnits和SimpleTest,也都能很容易的用来测试功能函数的。例如上面的SimpleTest例子,它就是用来测试calculate_sales_tax()函数的。世界各地的程序员们:把单体测试用例放到你的函数库里面吧!
我希望经过上面的讨论后,你也会被带动起来——“测试引导”(Test Infected)!(这个术语,原创于Erich Gamma,详细情况请见文章http://junit.sourceforge.net/doc/testinfected/testing.htm),就象Gamma所写的那样,刚开始你可能会感到测试是很繁琐的,但是当你为你的程序搭建好一个广阔的测试集后,你将你的代码更加自信!